/*------------------------------------------------------------------------------------------------------------------------------------------------------------- C ve Sistem Programcıları Derneği C++ Programlama Dili Sınıfta Yapılan Örnekler ve Özet Notlar Eğitmen: Kaan ASLAN Bu notlar Kaan ASLAN tarafından oluşturulmuştur. Kaynak belirtmek koşulu ile her türlü alıntı yapılabilir. (Notları sabit genişlikli font kullanan programlama editörleri ile açınız.) (Editörünüzün "Line Wrapping" özelliğini pasif hale getiriniz.) Son Güncelleme: 20/12/2024 - Cuma /*-------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 1. Ders 14/08/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Merhaba Dünya C++ programı --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { cout << "Hello World" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 2. Ders 16/08/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın ilk standardı ISO/IEC tarafından 1998 yılında oluşturuldu (ISO/IEC 14882: 1998).Bunu 2003 yılındaki standartlar izledi. 2003 standartları daha çok düzeltme niteliğinde idi. Daha sonra C++'ın 2011 yılında yeni bir standardı oluşturuldu. Bu standartlarla C++'a pek çok yenilik eklendi. 2011 standartlarını 2014, 2017 ve 2020 standartları izledi. Şu anda üzerinde çalışılmakta olan standart 2023'tür. Bu standartlar halk arasında sırasıyla C++98, C++03, C++11, C++14, C++17, C++20 ve C++23 olarak bilinmektedir. Klasik C++ olan C++98 ve C++03'e yapılan eklemeler iki kategoride ele ele alınabilmektedir. Bunlardan biri "doğrudan dile yapılan eklemelerdir (core language feaures)", diğeri ise C++'ın standart kütüphanesine yapılan eklemelerdir. C++'a en önemli eklemeler C++11 standartları ile yapılmıştır. C++11 ve sonrasına C++ dünyasında "Modern C++" da denilmektedir. C++11'den sonra artık standartların üç senelik periyotlarla oluşturulması kabul edilmiştir. Kanımızca üç semelik periyotlar C++ gibi bir dil için çok hızlı bir süreçtir. Bu hızlı gelişme çeşitli sancıları da beraberinde getirmiştir. C Progralama Dilinin ilk standartları "ISO/IEC 9899: 1990" ismiyle 1990 yılında ISO tarafından oluşturulmuştur. Buna halk arasında C90 denilmektedir. (Aslında C standartları önce 1989 yılında Amerika'nın standart kurumu olan ANSI tarafından aluşturulmuştu. 1990 ISO standartları bu ANSI standartlarının alınarak bazı bölüm numaralarının değiştirilmesiyle oluşturulmuşur.) C'nin 1999 yılında yeni bir standardı daha oluşturuldu. Buna da C99 denilmektedir. Daha sonra C'nin 2011 yılında yeni bir standardı oluşturulmuştur. Buna da C11 denilmektedir. Nihayet C'nin 2017 yılında son sürümü yayınlandı. Buna da C17 denilmektedir. Ancak bu C17'de yeni özellikler eklenmedi. Yalnızca C11'deki bozukluklar düzeltildi. C'nin üzerinde çalışılan son standart sürümü C23'tür. C23 standartlarının 2024'te yayınlanacağı düşünülmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarının üç senelik periyotlarla hızlı bir biçimde güncellenmesi bazı tasarım hatalarının ve pişmanlıkların oluşmasına da yol açmıştır. Dolayısıyla Modern C++'a yönelik bazı ince ayrıntılar C++'ın versiyonundan versiyonuna değişmiş olabilmektedir. Biz yeni öğrenen kişilere C++ standartların doğrudan okunmasını tavsiye etmemekteyiz. Çünkü standart metinleri (bazı diğer standartları da böyle) pedagojik metinler değildir. Olanı tam olarak betimlemek amacıyla oluşturulmuş metinelerdir. Son yıllarda "C++ Reference" isminde C++ standartlarını açıklamalı (annotated) bir biçimde dokümante eden bir girişim oldukça popüler hale gelmiştir. Biz de kursumuzda pek çok yerde doğrudan bu standartlara referans etmektense C++ Reference sitesinden faydalanacağız. Siteye aşağıdaki bağlantıdan ulaşabilirsiniz: https://en.cppreference.com/w/ --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir C++ programı IDE'ler yoluyla kolayca derlenip, link işlemi yapılarak çalıştırılabilir. Windows sistemlerinde en yaygın kullanılan IDE Microsoft'un "Visual Studio" isimli IDE'sidir. C++ için diğer bir IDE seçeneği "Qt-Creator" olabilir. Qt Creator "cross platform" biçimindedir. Yani Windows, macOS ve Linux sistemlerinde benzer biçimde kullanılabilmektedir. Tabii aslında derleyiciler komut satırından çalıştırılan programlar biçiminde oluşturulmuştur. IDe'ler aslında derleyicileri çalıştırarak derleme ve bağlama işlemlerini yapmaktadır. Microsoft'un C ve C++ derleyicisi "cl.exe" isimli programdır. Bu derleyici ile derleme komut satırında şöyle yapılabilir: cl sample.cpp Derleyici default durumda derleme işleminden sonra bağlayıcı (linker) programı da çalıştırır. Bu işlemden eğer programınızda bir hata yoksa "sample.exe" dosyasını elde edeceksinizç Tabii istersek çalıştırılabilen dosyanın ismini /Fe seçeneği ile de değiştirebiliriz: cl /Fe:test.exe sample.cpp UNIX/Linux ve macOS sistemlerinde GNU'nun g++ ve clang++ derleyicileri kullanılabilmektedir. g++ derleyicisi ile komut satırından derleme tipik olarak şöyle yapılmaktadır: g++ sample.cpp clang++ derleuyicilerinin kullanımları da g++ ile uyumludur: clang++ sample.cpp Burada derleyici derleme işleminden sonra yine bağlayıcı programı çalıştırmaktadır. Bu durumda çalıştırılabilen dosya "a.out" biçiminde oluşur. Çalıştırılabilen dosyanın ismini değiştirmek için -o seçeneği kullanılmaktadır. Örneğin: g++ -o sample sample.cpp ya da örneğin: clang++ -o sample sample.cpp UNIX/Linux sistemlerinde bir program dosyasını çalıştırmak için dosya isminin önüne ./ getirmeyi unutmayınız. Örneğin: ./sample C++'ın çeşitli standartları olduğuna göre derleme işlemi bu standartlar belirtilerek yapılabilir. Visual Stduio IDE'sinde bu durum menüler yoluyla ayarlanmaktadır. g++ ve clang derleyicinde -std seçeneği ile ayarlama yapılır. Örneğin: -std=c++11 -std=c++14 -std=c++17 -std=c++20 (-std=c++2a) -std=c++2b Örneğin: g++ -std=c++2a -o sample sample.cpp Burada -std=c++2a artık yeni derleyicilerde -std=c++20 ile değiştirilmiştir. -std=c++2b ise C++23'ün de bazı özelliklerini barındırmaktadır. gcc ile C++ programları da derlenebilir. (Aslında gcc önce GNU C derleyicisi olarak geliştirilmişti. Sonra diğer derleyicileri de çalıştıran bir program haline getirildi.) Bu durumda gcc zaten g++ derleyicisini çalıştırmaktadır. Ancak gcc ile derleme yapılırken libstdc++ kütüphanesinin "-lstdc++" komut satırı argümanı ile link aşamasında devreye sokulması gerekmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C'de ve C++'ta standartlarda belirtilen sentaks ve semantik kurallara uyulmadan yazılmış olan programlar yine de derleyici tarafından başarıyla derlenebilirler. Çünkü standartlar geçerli programların derlenmesi gerektiğini koşul olarak ifade etmiştir. Ancak geçesiz programların derlenip derlenmeyeceği konusunda bir hüküm belirtmemiştir. Yalnızca standartlarda geçersiz durumlar karşısında derleyicilerin bunu en az bir mesajla (diagnostic message) bildirimeleri zorunlu tutulmuştur. Yani bu durumda geçerli programlar her zaman başarıyla derlenmek zorundadır. Ancak geçersiz programlar başarıyla derlenebilir ya da derlenmeyebilir. Bu nedenle derleme işleminin başarısına bakarak dilin kurallarını öğrenmeye çalışmak iyi bir yöntem değildir. Ayrıca C ve C++'ta derleyiciler diğer derleyicilerde olmayan "eklentilere (extensions)" sahip olabilirler. Neyin bir eklenti olup olmadığının programcı tarafından bilinmesi gerekir. Eğer biz bir derleyicinin eklentisini kullanırsak o kodu başka bir derleyiciye götürdüğümüzde kod başarılı olarak derlenmeyebilir. Çalıştığınız derleyicilerin eklentilerini bilmelisiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın C'den farklılıkları genellikle "fazlalık" biçimindedir. Bu fazlalıkların bir bölümü C++'ı daha iyi bir C yapmak için dile eklenmiştir. Bunlara "C++'ın C'den Nesne Yönelimli Programlama Tekniği İle Doğrudan İlgisi Olmayan Fazlalıkları ve Farklılıkları" diyeceğiz. Biz kurusumuzda önce bunlar üzerinde duracağız. Sonra C++'ı "Nesne Yönelimli bir dil yapan C++ özgü konular üzerinde duracağız. Yani kursumuz kabaca üç bölümden oluşmaktradır: 1) C++'ın C'den Nesne Yönelimli Programlama Tekniği İle Doğrudan İlgisi Olmayan Fazlalıkları ve Farklılıkları 2) C++'ın C'den Nesne Yönelimli Programlama Tekniği İle Doğrudan İlgili Olan Fazlalıkları ve Farklılıkları 3) C++'ın Diğer Öenmli Özellikleri ve Standart Kütüphanesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bu bölümde "C++'ın C'den Nesne Yönelimli Programlama Tekniği İle Doğrudan İlgisi Olmayan Fazlalıkları ve Farklılıkları" maddeler halinde ele alınacaktır. Burada her maddeyi özet bir cümleyle başlatacağız ve sonra onun ayrıntılarına gireceğiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 1) C++'a // ile satır sonuna kadar yorumlama eklenmiştir. Bu özellik C90'da yoktu. Fakat C++98'den sonra sonra çıkan C99 ile birlikte C'ye de eklenmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { cout << "Hello World" << endl; // Merhaba dünya programı return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 2) C++'ta yerel değişkenler blokların herhangi bir yerinde bildirilebilirler. Halbuki C90'da yerel değişkenler blokların başlarında bildirilmek zorundaydı. Ancak C99 ile birlikte bu kural C'de de C++'taki gibi değiştirildi. C++'ta bir yerel değişkenin faaliyet alanı bildirim yerinden bildirildiği bloğun sonuna kadarki bölgededir. Yine C++'ta iç içe ya da ayrık bloklarlarda aynı isimli yerel değişkenler bildirilebilir. Ancak aynı blok içerisinde aynı isimli yerel değişkenler bildirilemez. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 3) C++'ta for döngülerinin birinci kısmında bildirimler yapılabilmektedir. Bu özellik C90'da yoktu. Ancak C99 ile birlikte C'ye de eklendi. Ayrıca C++'ta diğer deyimlerin parantezleri içerisinde de bildirimler yapılabilmektedir. for döngüsünün birinci kısmında bildirim yapılabilmesi sentaksını C++'tan almış olan Java ve C# gibi dillerde söz konusudur. Bu özellik döngü yazımını kolaylaştırmaktadır. Örneğin: for (int i = 0; i < 10; ++i) { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { for (int i = 0; i < 10; ++i) cout << i << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- for döngüsünün birinci kısmında bildirilen değişkenlerin faaliyet alanları for döngüsü ile sınırlıdır. Başka bir deyişle: for (bildirim; ifade; ifade) işleminin eşdeğeri aşağıdaki gibi düşünülmelidir: { bildirim; for (;ifade; ifade) } Yani "for döngüsünü kapsayan bir gizli blok" varmış gibi düşünmelisiniz. for döngüsünün diğer kısımlarında bildirim yapılamamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { for (int i = 0; i < 10; ++i) { cout << i << " "; } cout << endl; cout << i << endl; // error! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bu biçimdeki bir for döngüsünü aşağıya kopyalarsak faaliyet alanı bakımından bir sorun oluşmayacaktır.Örneğin: for (int i = 0; i < 10; ++i) { //... } for (int i = 0; i < 10; ++i) { //... } Buradaki i değişkenleri aslında ayrık bloklardaki i değişkenleri gibidir. Yukarıdaki kodun eşdeğerini aşağıdaki gibi düşünmelisiniz: { int i = 0; for (; i < 10; ++i) { ß//... } } { int i = 0; for (; i < 10; ++i) { //... } } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { for (int i = 0; i < 10; ++i) cout << i << " "; for (int i = 0; i < 10; ++i) // Buaraki i başka bir i cout << i << " "; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İç içe for döngülerinde de aynı isimli değişkenlerin bulunması bu bağlamda bir sorun oluşturmaz. Ancak iç for döngüsünde biz dış for döngüsünün birinci kısmında bildirilen değişkenleri kullanamayız. Örneğin: for (int i = 0; i < 10; ++i) for (int i = 0; i < 10; ++i) { //... } Burada bir sorun yoktur. Çünkü yukarıdaki kodun eşdeğeri aşağıdaki gibidir: { int i = 0; for (; i < 10; ++i) { int i = 0; for (; i < 10; ++i) { //... } } } Yani aslında buradaki söz konusu iki i değişkeni aynı blokta değildir, iç içe bloklardadır. Tabii biz iç döngüde dış döngüdeki i değişkenini artık kullanamayız. Her ne kadar iç içe for döngülerinde döngülerin birinci kısmında aynı isimli değişkenler bildirilebiliyorsa da aslında bu durum iyi bir teknik değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { for (int i = 0; i < 10; ++i) for (int i = 0; i < 10; ++i) cout << i << endl; // Buradaki i iç for döngüsündeki i, diğer i'ye erişemeyiz return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- for döngülerinin birinci kısmında "aynı türden olmak koşulu ile" birden fazla değişkenin bilfirimi de yapılabilir. Örneğğin: for (int i = 0, k = 100; i + k > 50; ++i, k -= 2) { //... } Ancak burada bildirilen değişkenler farklı türlerden olamamaktadır. Örneğin: for (int i = 0, long k = 100; ...) { // geçersiz!.. //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { for (int i = 0, k = 100; i + k > 50; ++i, k -= 2) cout << i << ", " << k << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- for döngüsünün birinci kısmında bildirilen değişkenlere ilkdeğer verilmesi gerekir. Ancak bu durum C++'ta standartlarında zorunlu tutulmamıştır. Genel olarak bu biçimde bildirilen değişkenlere ilkdeğer verilmemesinin bir anlamı yoktur. Örneğin: for (int i; i < 10; ++i) { // geçerli ama çöp değer kullanılıyor //... } Java ve C# gibi diğer dilelrde ilkdeğer verme bir zounluluktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 3. Ders 21/08/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta if, while switch gibi deyimlerin parantezleri içerisinde de bildirimler yapılabilir. Bu durumda o değişkenlere atanan değerler test işlemine girer. Örneğin: if (void *ptr = malloc(1024)) { // tahsisat başarılı ise //... } Burada ptr'ye atanan değer test işlemine sokulmaktadır. Örneğin: while (int a = foo()) { //... } Burada foo fonksiyonu sıfır dışı bir değerle geri döndüğü sürece döngü devam edecektir. Örneğin: switch (int a = rand() % 10) { //... } Böyle bir özellik C'de yoktur. Deyimlerin parantezleri içerisinde bildirim yapıldıktan sonra artık atanan değerin aynı ifadede işleme sokulması mümkün değildir. Örneğin: while ((int i = foo() < 10) { // geçersiz! böyle bir sentaks yok //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int foo() { static int i = 10; return --i; } int main() { while (int i = foo()) cout << i * i << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 4) C++'a bool türü ve bool türünden değer belirten true ve false anahtar sözcükleri de eklenmiştir. bool türü C90'da yoktu. Ancak C99 ile birlikte C'ye de _Bool ismiyle böyle bir tür eklendi. (C99'da içerisinde bool ismi, true ve false isimleri makrolar biçiminde de oluşturulmuşttr.) C++'a bool türü eklenince if gibi while gibi deyimlerdeki koşul ifadeleri de "bool türden olacak" biçimde değiştirilmiştir. Yani C++'ta if deyiminin ve while deyiminin koşul ifadesi artık bool türdendir. Eğer bu koşul ifadeleri bool türden değilse derleyici tarafından otomatik olarak (implictly) bool türüne dönüştürülmektedir. Aynı durum for döngüsünün ikinci kısmı için de geçerlidir. (Bu durum C99 ve sonrasında böyle değildir.) C++'ta karşılaştırma operatörlerinin ürettiği değerler de bool türdendir. (C'nin bütün standartlarında karşılaştırma operatörlerinin int türden değer ürettiğini anımsayınız.) Standartlar bool türü için ayrılacak yerin derleyiciye bağlı olarak değişebileceğini (implementation-defined) belirtmektedir (8.5.2.3-1). Tipik olarak derleyicileer bool türü için 1 byte yer ayırmaktadır. C++'a bool türünün C uyumunu koruyacak biçimde eklendiğine dikkat ediniz. Yani C++'taki bool türü adeta içerisinde 1 ve 0 değerleri bulunan bir tamsayı türü gibidir. Ancak adres türleri otomatik olarak bool türüne dönüştürülebilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a = 0; bool result; result = a > 10; // karşılaştırma operatörleri bool türden değer üretir cout << result << endl; result = true; // true ve false birer anahtar sözcüktür cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta bool türü aritmetik işlemlere sokulabilir. Bu durumda "int türüne yükselteme kuralı (integral promotions)" gereğince otomatik olarak int türüne dönüştürüldükten sonra işleme sokulmaktadır (7.6-6). Dönüştürme sırasında true değeri 1 olarak false değer 0 olarak dönüştürülmektedir. Yani örneğin int + bool gibi bir işlemin sonucu int türden, double + bool biçiminde bir işlemin sonucu double türden, bool + bool gibi bir işlemin sonucu int türden elde edilecektir. Skaler türler de bool türüne otomatik olarak (implicitly) dönüştürülebilmektedir. Sıfır dışı skaler değerler true olarak, 0 değeri ise false olarak dönüşürülmektedir. Ayrıca adres türlerinden bool türüne de otomatik dönüştürme vardır. NULL adres değerleri false olarak diğer adresler ise true olarak bool türüne dönüştürülürler. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a = 10, c; bool b = true; char s[10]; c = a + b; // geçerli bool türü işlem öncesinde otomatik olarak int türüne dönüştürülür cout << c << endl; b = 120; // geçerli, sıfır dışı değerler true olarak dönüştürülür cout << b << endl; b = 0; // geçerli, sıfır değeri false olarak dönüştürülür cout << b << endl.i b = s; // geçerli NULL pointer falsde olarak diğer adresler true olarak dönüştürülür cout << b << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 5) C++'ta bir fonksiyonun çağrılma noktasına kadar derleyicinin o fonksiyonun prototipiyle ya da tanımlamasıyla karşılaşmış olması gerekmektedir. C90'da fonksiyon çağrısını gören derleyici eğer daha önce fonksiyon hakkında bir bilgi edinmemişse onun int geri dönüş değerine sahip olarak tanımlandığını varsayıyordu. Gerçi bu kural da C99 ile birlikte C++'taki gibi değiştirilmiştir. Bu durumda C++'ta örneğin bir kütüphane fonksiyonu çağrılacaksa mutlaka o fonksiyonun prototipinin bulunduğu başlık dosyası da include edilmelidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { foo(); // C90'da geçerli ancak C++'ta ve C99'da geçersiz return 0; } int foo(void) { return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 6) C++'ta fonksiyon prototiplerinde parametre parantezinin içinin boş bırakılması ile parametre parantezlerinin içine void yazılması aynı anlamdadır. Ancak C'nin tüm standartlarında bunlar farklı anlamlara gelir. C'de prototiplerde parametre parantezinin içinin boş bırakılması "bu fonksiyon herhangi bir sayıda parametreye sahip olabilir, bu nedenle fonksiyon çağrılırken argümanlar sayıca ve türce kontrol edilmeyecek" anlamına geliyordu. Halbuki C++'ta artık parametre parantezinin içinin boş bırakılmasıyla void yazılması tamamen aynı anlama gelmektedir. Tabii C'de de bir fonksiyonun tanımlanması sırasında parametre parantezinin içinin boş bırakılmasıyla void yazılması aynı anlama geliyordu. C ile C++ arasındaki farklılık tanımlamada değil prototip bildiriminde ortaya çıkmaktadır. Tabii C++'ta programcı isterse yine prototiplerde ya da tanımalam sırasında parametre parantezlerinin içerisine void yazabilir. Aşağıdaki örnekteki kod C'de geçerli olduğu halde C++'ta geçersizdir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int foo(); int main() { foo(10, 20); // C'de geçerli, C++'ta geçersiz return 0; } int foo(int a, int b) { return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi hem C'de hemde C++'ta başından beri fonksiyon tanımlaması sırasında parametre parantezinin içinin boş bırakılması fonksiyonun parametreye sahip olmadığı anlamına gelmektedir. Biz Derneğimizde C kurslarında genel olarak parametresiz fonksiyonların tanımlamasında parametre parantezinin içini boş bırakmayıp void yazıyorduk. Ancak C++ kurslarımızda parametre almayan fonksiyonlarda tanımlama sırasında ve prototip bildiriminde parametre parantezinin içerisini boş bırakacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int foo() // C'de de C++'ta da foo fonksiyonun parametresi yok void yazmakla aynı { return 0; } int main() { foo(10, 20); // C'de ve C++'ta geçersiz! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 7) Hem C'de hem de C++'ta void göstericiye herhangi bir türden adres atanabilir. Ancak void bir adresin void olmayan göstericiye atanması C'de geçerli olduğu halde C++'ta geçersizdir. C++'ta void bir adres void olmayan bir göstericiye tür dönüştürmesi yapılarak atanmalıdır. Örneğin: void *pv; int *pi; //... pv = pi; // hem C'de hep de C++'ta geçerli pi = pv; // C'de geçerli fakat C++'ta geçersiz! pi = (int *)pv; // hem C'de hem de C++'ta geçerli C++'ta void adresin void olmayan bir göstericiye atanmasının (yani otomatik olarak dönüştürülmesinin) engellenmesinin temel nedeni aslıda C'deki bir açığı kapatmak içindir. C'de aşağıdkai gibi bir bir açık vardı: int *pi; char *pc; void *pv; ... pv = pi; // C'de v e C++'ta geçerli pc = pv; // C'de geçerli Burada biz aslında pc = pi işlemini yapmış olmaktayız. Normalde geçersiz olması gereken bu işlem araya bir void gösterici sokularak geçerli hale gelmektedir. Oysa C++'ta böylesi bir "arkadan dolaşma" yapılamamaktadır. Tabii C'de neden void bir adresin void olmayan bir göstericiye atanabildiğini sorgulayabilirsiniz. Bu tarihsel bir özelliktir. Zaten C++ fırsat bu fırsat C'nin böylesi açıklarını da kapatmak istemiştir. Bu tür durumlarda tür dönüştürmesi genel olarak "işlemin programcı tarafından yanlışlıkla değil bilinçli olarak yapıldığını" ifade etmektedir. Programlama dillerinde "kuvvetli tür kontrolü (strong type checking)" ve "zayıf tür kontrolü (weak (loose) type checking)" biçimibde bir kavram vardır. Eğer biz bir dilde farklı türleri serbestçe biribine atayıp onları birlikte işleme sokabiliyorsak o dilin tür kontrolü zayıftır. Eğer bir dilde biz farklı türleri birbirine atayamayıp onları birlikte işleme sokamıyorsak o dilin tür kontrolü kuvvetlidir. C ve C++'ın tür kontrolü kuvvetli değildir. Orta düzeydedir. Örneğin Java ve C#'taki tür kontrolü C ve C++'a göre daha kuvvetlidir. Swift gibi Rust gibi dillerde tür kontrolü çok daha kuvvetlidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a[10]; void *pv; double *pd; pv = a; // hem C'de hem C++'ta geçerli pd = pv; // C'de geçerli, C++'ta geçersiz pd = (double *)pv; // geçerli return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 4. Ders 23/08/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 8) C'de string'ler char türden bir dizi kabul edilmektedir. (Yani string'leri gören derleyici onun karakterlerini null karakter dahil olmak üzere char türden statik ömürlü bir diziye yerleştirir. String yerine o dizinin kullanıldığını varsayar.) Ancak string'lerin karakterlerinin güncellenmesi "tanımsız davranışa (undefined behavior)" yol açmaktadır. Ancak C++'ta string'ler const char türünden dizi anlamına gelmektedir. Dolayısıyla bir string'i char türden bir göstericiye atarken göstericinin de gösterdiği yer const olan bir gösterici olması gerekir. Örneğin: char *str; //... str = "ankara"; // C'de geçerli C++'ta geçersiz! Örneğin: const char *str; //... str = "ankara"; // C'de de C++'ta da geçerli char türden, signed char türden ve unsigned char türden bir diziye ilkdeğer verirken kullanılan iki tırnakların string belirtmediğini anımsayınız. Bu nedenle C++'ta da tıpkı C'de olduğu gibi char türden, signed char türden ve unsigned char türden diziler iki tırnak ifadesiyle ilkdeğer verilerek tanımlanabilirler. char s1[] = "ankara"; // C'de de C++'ta da geçerli signed char s2[] = "ankara"; // C'de de C++'ta da geçerli unsigned char s[] = "ankara"; // C'de de C++'ta da geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { const char *str = "ankara"; cout << str << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 9) C'de N elemanlı char türden, signed char türden ve unsigned char türden bir diziye iki tırnak içerisinde N karakterli bir yazı ile ilkdeğer verilebilir. Bu durumda derleyici null karakteri diziye yerleştirmez. Ancak bu durum C++'ta geçerli değildir. C++'ta N karakterlik bir yazının ilkdeğer verildiği dizinin en azından N + 1 karakter uzunluğunda olması (ya da uzunluk belirtilmemesi) gerekir. Örneğin: char s[6] = "ankara"; // C'de geçerli ancak C++'ta geçersiz! --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 10) C++11 ile birlikte UNICODE string'ler ve karakter sabitleri de dile eklenmiştir. UNICODE UTF-8 için u8 öneki, UNICODE UTF-16 için u öneki ve UNICODE UTF-32 için U öneki kullanılmaktadır. u önekli string'ler const char16_t türünden, U önekli string'ler const char32_t türünden ve u8 önekli string'ler de const char8_t türünden bir dizi olarak ele alınmaktadır. C++20'ye kadar char8_t biçiminde bir tür yoktu. Bu tür C++20 ile eklenmiştir. Bu tür isimleri typedef değil anahtar sözcüklerdir. Bu türlerin hepsi işaretsiz bir tamsayı türü belirtmektedir. Bunların işaretli biçimleri de yoktur. C++'ta u önekli string'ler const char16_t türünden, U önekli string'ler const char32_t türünden ve u8 önekli string'ler ise const char8_t türünden dizi kabul edilmektedir. Örneğin: const char *s = "Ankara"; // default karakter tablosu const char16_t *k = u"Ağrı Dağı"; // UTF-16 const char32_t *t = U"Ağrı Dağı"; // UTF-32 const char8_t *m = u8"Ağrı Dağı"; // UTF-8 char8_t türünden, char16_t türünden ve char32_t türünden dizilere u8 önekli, u önekli ve U önekli iki tırnaklı ifadeler ilkdeğer olarak verilebilmektedir. Tabii bu durumda dizilerin const olması gerekmez. Örneğin: char8_t s1[] = u8"ankara"; // geçerli char16_t s2[] = u"ankara"; // geçerli char32_t s3[] = U"ankara"; // geçerli C++20'ye kadar char8_t türü yoktu. Dolayısıyla C++20'ye kadar u8 önekli string'ler unsigned char türünden diziler gibi ele alınıyordu. C++20'de u8 önekli string'ler için de char8_t türü eklenmiştir. C++20 satndartları C++20'ye eklenen char8_t türünün unsigned char türü ile aynı uzunlukta olması gerektiği belirtilmiştir. (Başka bir deyişle C++20'de eklenen char8_t türü adeta unsigned char türü gibidir ancak fakat farklı bir türdür.) char8_t türü, char16_t türü ve char32_t türü cout ile stdout dosyasına yazdırılamamaktadır. Tabii nasıl u önekli, U önekli ve u8 önekli stringler varsa aynı zamanda bu önekli karakter sabitleri de vardır. Bu karakter sabitleri de sırasıyla char16_t, char32_t ve char8_t türünden sabitler kabul edilmektedir. Örneğin: char8_t a = u8'a'; char16_t b = u'b'; char32_t c = U'c'; Karakter kodlamaları (character encoding) ayrıntıları olan uygulamada çetrefil bir konudur. Bir karakter tablosu "glyph", "code point" ve "encoding" denilen üç kavramla ilgilidir. Karakter tablosundaki karakter görüntülerine "glyph" denilmektedir. Karakter tablosunu oluşturanlar her karaktere bir numara verirler. İlgili glyph'in numarasına "code point" denilmektedir. Bir code point'in ikilik sistemde byte'lar haline ifade edilme biçimine de "encoding" denilmektedir. Örneğin ASCII tablosunda 'A' glyph'inin code point'i 65'tir. ASCII tablosu tamamne düz binary dönüştürmeyle encode edilmektedir. Son 20 yıldır ASCII tablosunun ve bu tablonun "code page" varyasonları çeşitli bakımlardan yetersiz kaldığı için UNICODE denilen (ISO 10646) her karakterin kabaca 2 byte ile ifade edilebildiği dünyanın bütün karakterlerinin içinde bulunduğu karakter tablosu oldukça yaygınlaşmıştır. Pek çok yeni programlama dilinde "char" türü UNICODE karakterleri tutmak için oluşturulmuştur. UNICODE tablonun UTF-16, UTF-32 ve UTF-8 denilen encoding'leri vardır. UTF-16 UNICODE için en doğal encoding'tir. Bu encoding'te kabaca her UNICODE code point WORD biçimde (2 bytelık sayı olarak) kodlanmaktadır. UTF-32'de ise her code point 4 byte ile kodlanmaktadır. Ancak UNICODE tablonun en yaygın encoding'i UTF-8 denilen encoding'tir. UNICODE tablonun ilk 128 karakteri standart ASCII tablosu ile aynıdır. Sonraki 128 karakteri de ISO 8859-1 denilen Latin1 code page'i ile aynıdır. UTF8-8 encoding'inde standart ASCII karakterler 1 byte ile diğer karakterler duruma göre 2 byte, 3 byte 4 byte ve 5 byte ile kodlanmaktadır. Bugün kullandığımız programalama editörlerinin büyük kısmının default encoding'i UNICODE UTF-8'dir. Ancak bu konu sanıldığından daha çetrefildir. Çünkü bir C/C++ programı yazarken değişik aktörler devreye girmektedir. Bu aktörlerden birincisi kodu yazdığımız editördür. Bu editörün default bir encoding'i vardır. Örneğin kursun yapıldığı bilgisayardaki Visual Studio IDE'sinin default encoding'i ASCII 1254 code page'idir. Visual Studio Code IDE'sinin defaulşt encoding'i UNICODE UTF-8'dir. İkinci aktör bizzat derleyicinin kendisidir. Derleyici kaynak kodu aldığında o kaynak koddaki yazının da belli bir encoding'e göre kodlandığını varsaymaktadır. Eğer bizim kaynak kodda kullandığımız encoding derleyicinin varsaydığı encoding'ten farklıysa burada da potansiyel bir problem vardır. Buna derleyicinin "kaynak karakter kümesi (source character set)" denilmektedir. Kaynak karakter kümesi derleycilerde komut satırı seçenekleriyle değiştirilebilmektedir. Üçüncü aktör ise programın çalıştırıldığı ortamdaki aygıtların (stdout ve stdin aygıt sürücülerinin) varsaydığı encoding'tir. Buradaki uyuşmazlık karakterlerin doğru gözükmemesine yol açacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 11) C++'ta sınıflarla temel türlerin şablon uyumunun sağlanması için temel türlere de parantezler ile ilkdeğer verilebilmesi mümkün hale getirilmiştir. Örneğin: int a = 10; Biz bu tanımlamayı şöyle de yapabilirdik: int a(10); // C++'a özgü, C'de böyle bir ilkdeğer verme sentaksı yok Ancak parantezlerin içini boş bırakmak "fonksiyon prototipi" anlamına gelmektedir. Örneğin: int b(); // bu ilkdeğer verme sentaksı değil! Fonksiyon prototipi Örneğin: const char *name("ali"); Pekiyi bir diziye ya da yapıya da bu biçimde ilkdeğer verebilir miyiz? Örneğin: int a[3](10, 20, 30); Bu durum C++20'ye kadar geçerli değildi. Yani C++20 öncesinde dizilere ve yapılaba normal parantezlerle ilkdeğer verilemiyordu. Ancak C++20 ile birlikte bu durum da geçerli hale getirilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 12) C++'ta çeşitli biçimlerde ilkdeğer verme (initialization) sentaksları bulunmaktadır. Örneğin bir değişkene bir ifade ile '=' atomu kullanılarak ilkdeğer verilebilir: int a = 10; Ayrıca C++'ta normal parantezlerle de ilkdeğer verilebilmektedir. Örneğin: int a(10); Bu biçimde ilkdeğer verme C'de yoktur. Bu normal parantez sentaksı C++'ta sınıflar için düşünülmüştür. Ancak temel türler için de kullanılabilmektedir. Bir diziye küme parantezleriyle ilkdeğer verilir: int a[] = {1, 2, 3}; C'de ve C++'ta skaler türlere de küme parantezi ile ilkdeğer verilebilmektedir. Örneğin: int a = {10}; Bir sınıf nesnesi normal parantezlerle ilkdeğerlenir: Sample s(10, 20); Yukarıda da belirttiğimiz C++'ta sınıflar için düşünülmüş olan (...) sentaksı skaler türler için de kullanılabilmektedir. Örneğin: int a(10); İşte C++11 ile birlikte her durum için geçerli olan ilkdeğer verme sentaksı oluşturulmuştur. Buna "Uniform Initializer Syntax" denilmektedir. Bu sentaksta hiç '=' atomu kullanılmadan doğrudan küme parantezleri içerisinde ilkdeğer verilir. Örneğin: int a{10}; const char *b{"ali"}; int c[]{1, 2, 3}; int d{}; // d = 0 Bu ilkdeğer verme sentaksına "Uniform Initializer Syntax" denilmesinin nedeni her türden değişkene bu biçimde ilkdeğer verilebilmektedirilmesidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a{10}; const char *b{"ali"}; int c[]{1, 2, 3}; int d{}; // d = 0 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Uniform Initializer Syntax ile ilkdeğer verme sırasında bilgi kaybına yol açabilecek dönüştürmeler geçerli değildir. Bilgi kaybına yol açabilecek dönüştürmelere C++11 standartlarında "daraltıcı dönüştürmeler (narrowing conversions)" denilmektedir. Büyük tamsayı türünden küçük tamsayı türüne yapılan dönüştürmeler, gerçek sayı türlerinden tamsayı türlerine yapılan dönüştürmeler bilgi kaybı oluşturabildiği için "daraltıcı dönüştürmeler (narrowing conversions)" durumundadır. Örneğin: int a{3.14}; // geçersiz! int b = 3.14; // geçerli double c{3.14}; // geçerli float d{c}; // geçersiz! --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a{10.2}; // geçersiz! int b = 10.2; // geçerli int c[]{1, 2, 3.4, 5}; // geçersiz! long d{100}; // geçerli int e{d}; // geçersiz return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 5. Ders 28/08/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki dönüştürmeler daraltıcı dönüştürmeler olarak kabul edilmektedir: - Gerçek sayı türlerinden tamsayı türlerine yapılan dönüştürmeler (örneğin long double, double ve float türünden int türüne yapılan dönüştürmeler). Örneğin: double d{3.0}; int a{d}; // geçersiz! int b{3.0}; // geçersiz! - Büyük gerçek sayı türünden küçük gerçek sayı türüne yapılan dönüştürmeler. Örneğin double türünden float türüne yapılan dönüştürmeler daraltıcı dönüştürmelerdir. Ancak büyük gerçek sayı türünden değer bir sabit ifadesi biçiminde verildiyse ve bu sabit ifadesi hedef tür ile tam olarak ifade edilebiliyorsa bu daraltıcı dönüştürme kabul edilmemektedir. Örneğin: double d{3.14}; float f{d}; // geçersiz! float e{3.14} // geçerli, çünkü 3.14 bir sabit ifadesidir ve float türüyle tam olarak temsil edilebilmektedir. float g{3.14 + 1}; // geçerli, çünkü 4.14 bir sabit ifadesidir ve float türüyle tam olarak temsil edilebilmektedir. - Bir tamsayı türünden gerçek sayı türüne dönüştürmeler de bilgi kaybına yol açabilme potansiyeline sahip olduğu için daraltıcı dönüştürmelerdir. Ancak tamsayı türünden değer bir sabit ifadesi biçiminde belirtilmişse ve bu sabit ifadesi hedef tür tarafından tam olarak ifade edilebiliyorsa bu bir daraltıcı dönüştürme değildir. Örneğin: int a{10}; double b{a}; // geçersiz! char c; double e{c}; // geçersiz! float f{1234}; // geçerli - Bir tamsayı türünden başka bir tamsayı türüne dönüştürme yapılırken eğer hedef tür "o sistemdeki" kaynak türün tüm değerlerini içeriyorsa bu bir daraltıcı dönüştürme değildir. Ancak içermiyorsa bu bir daraltıcı dönüştürmedir. Örneğin: int a{10}; unsigned b{a}; // geçersiz! unsigned c{10}; int d{c}; // geçersiz! long e{10}; int f{e}; // int ile long türünün aynı uzunlukta olduğu sistemlerde geçerli (örneğin Microsoft derleyicilerinde) // ancak int ve long türünün farklı uzunluklarda olduğu sistemlerde geçersiz (örneğin 64 bit Linux derleyicilerinde) Ancak kaynak tamsayı türündeki değer bir sabit ifadesi ise ve hedef türün sınırları içerisinde kalıyorsa bu bir daraltıcı dönüştürme değildir. Örneğin: unsigned a{10}; // geçerli, 10 int türden fakat unsigned int sınırları içerisinde unsigned b{-10}; // geçersiz! -10 int türden fakat unsigned int sınırları içerisinde değil int c{123L}; // geçerli, 123 long türden ancak int sınırları içerisinde long d{123}; // geçerli, 123 int türden ve long türünün sınırları içerisinde - Anımsanacağı gibi adres türlerinden bool türüne otomatik dönüştürme vardı. Ancak adres türlerinden bool türüne yapılan dönüştürmeler daraltı dönüştürmelerdir. Örneğin: char s[10]; bool b = s; // geçerli bool c{s}; // geçersiz! --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında C++11 ile birlikte küme parantezleri ile ilkdeğer vermelerin hepsinde daraltıcı dönüştürme yasaklanmıştır. Örneğin aşağıdaki ilkdeğer vermeler C++11 öncesi geçerli olduğu halde C++11 ile birlikte artık geçersizdir: int a[] = {10.2, 1, 2}; // geçersiz! int b = {3.14}; // geçersiz! short c[]{10L, 100, 1000}; // geçerli Görüldüğü gibi her ne kadar daraltıcı dönüştürmeler C++11'in Uniform Initializer Syntax'ı ile kavramsal olarak dile eklendiyse de yalnızca Uniform Initializer Syntax ile değil tüm küme parantezleri ile ilkdeğer verilirken etkili olmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a[] = {10.2, 1, 2}; // C++11 ile birlikte artık geçerli değil return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 13) C++'ta const nesneler onlara verilen ilkdeğerler "sabit ifadesi (constant expression)" ise sabit ifadesi belirtirler. Halbuki C'de const nesneler hiçbir zaman sabit ifadesi belirtmezler. Ayrıca C++'ta global const nesneler "internal linkage"a sahiptir. (Yani sanki static global nesneler gibi düşünülmelidir.) Tabii bir sabit ifadesi sabit ifadeleriyle de oluşturulabilmektedir. Örneğin: const int a = 10; // a sabit ifadesi olarak kullanılabilir const int b = a + 20; // b sabit ifadesi olarak kullanılabilir const int c = foo(); // c sabit ifadesi olarak kullanılamaz! Anımsanacağı gibi C'de (ve C++'ta) bazı durumlarda sabit ifadesi kullanımı zorunludur. Örneğin case ifadelerinin sabit ifadesi olması gerekir. Örneğin dizi tanımlanırken dizi uzunluklarının sabit ifadesi olması gerekmektedir. (C99'da yerel diziler için bu zorunluluk ortadan kaldırılmıştır.) Örneğin C++'ta şablonların tür olmayan (none-type) parametreleri sabit ifadesi olarak girilmek zorundadır. Sabit ifadelerinin diğer önemli bir işlevi de onların değerlerinin derleme zamanında hesaplanabilmesi dolayısıyla programın çalışma zamanı sırasında gereksiz işlemlerin elimine edilmesidir. Bu optimizasyon temasına "constant folding" denilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; const int SIZE = 10; int main() { int a[SIZE]; // geçerli SIZE sabiti ifadesi ile ilkdeğer verilmiş bir const nesne int val; const int AA = 2; int b = 10; const int BB = b; cout << "Bir değer giriniz:"; cin >> val; switch (val) { case AA: // geçerli AA bir sabit ifadesi break; case BB: // geçersiz! BB bir sabit ifadsi değil, çünkü ona verilen ilkdeğer sabit ifadesi değil break; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta global const nesnelerin "internal linkage" özelliğine sahip olması onların başka bir modülden extern yapılsa bile kullanılamayacağı anlamına gelmektedir. Bu durumda derleyiciler global const nesnelere verilen ilkdeğerler sabit ifadesi ise onlar için hiç yer ayırmayabilirler. Çünkü aslında sabit ifadeleri ile ilkdeğer verilmiş olan global const nesneler kod içerisinde kullanıldığında derleyici zaten onların değerlerini koda enjekte edecektir. Bunlar başka bir modülden de kullanılamayacağına göre onlar için yer ayrılmasının da bir anlamı kalmayacaktır. Şüphesiz aynı durum yerel const nesneler için de geçerlidir. Anımsanacağı gibi yerel değişkenlerin zaten "linkage" özelliği yoktur. Tabii programcı const nesnelere sabit ifadesi ile ilkdeğer verdikten sonra onların adreslerini alıp kod içerisinde kullanırsa derleyiciler mecburen onlar için yer ayırmak durumunda kalır. O halde C++'ta sabit ifadeleriyle ilkdeğer verilmiş global const nesneler adeta #define sembolik sabitleri gibi kullanılabilmektedir. Biz global const nesneleri (özellikle sabit ifadeleriyle ilkdeğer verilmiş olanları) başlık dosyalarına (header files) yerleştirip birden fazla kaynak dosyadan include edebiliriz. Bu durumda herhangi bir problem ortaya çıkmayacaktır. C++ programcıları #define sembolik sabitler yerine global const nesneleri kullanmayı tercih edebilmektedir. Anımsanacağı gibi iki yer belirleyici (storage class specifier) anahtar sözcük bildirimde bir arada kullanılamamaktadır. Örneğin: extern static int g_x; // geçersiz! Ancak tür niteleyicileriyle (const ve volatile) yer belirleyicileri birlikte kullanılabilirler. Örneğin: extern const int g_x = 10; C++'ta const nesneler default durumda "inernal linkage" özelliğine sahiptir. Ancak özellikle extern belirlemesi yapılırsa "external linkage" özelliğine sahip olmaktadır. Ayrıca C++'ta const nesnelere artık ilkdeğer verilmek zorundadır. Halbuki C'de anlamsız olsa da const nesnelere ilkdeğer vermek zorunlu değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 14) C++'ta statik ömürlü değişkenlere (yani global ve statik yerel nesnelere) verilen ilkdeğerler sabit ifadesi olmak zorunda değildir. Halbuki C'de global değişkenlere ve statik yerel değişkenlere verilen ilkdeğerlerin sabit ifadesi olması zorunludur. C'de bu zorunluluk derleyicinin statik ömürlü değişkeni ilkdeğeri ile birlikre amaç koda yazma gerekeliliğinden kaynaklanmaktadır. Bu gereklilik C++'ta oratadan kaldırılmıştır. (Çünkü C++'ta main fonksiyonu çağrılmadan önce derleyici başka kodlar da çalıştırabilmektedir.) Örneğin: int foo() { return 10; } int g_x = foo(); // C'de geçersiz! C++'ta geçerli Ancak C++'ta static yerel değişkenlere ilkdeğer verilme işlemi akış ilk kez o noktaya geldiğinde ve toplamda yalnızca bir kez yapılmaktadır. Örneğin: int foo() { cout << "foo" << endl; return 0; } void bar() { static int x = foo() + 1; //... } Burada bar çağrılmadan static yerel değişkene ilkdeğer verme işlemi yapılmamaktadır. Bu işlem ilk kez bar çağrıldığında ve toplamda yalnızca bir kez yapılır. Halbuki C'de static yerel nesnelere verilen ilkdeğerler derleme aşamasında ele alınmaktadır ve static yerel nesneler program yüklendiğinde ilkdeğerleriyle birlikte yaratılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 15) C++11 ile birlikte C++'a ismine "constexpr" denilen bir belirleyici (specifier) da eklenmiştir. constexpr bir değişken her zaman bir sabit ifadesi biçiminde kullanılabilir. (const bir değişkenin sabit ifadesi olarak kullanılabilmesi için ona verilen ilkdeğerin sabit ifadesi belirtmesi gerektiğini anımsayınız.) Ancak constexpr değişkenlere verilen ilkdeğerlerin sabit ifadesi olması zorunludur. Global constexpr değişkenler de yine "internal linkage" özelliğine sahiptir. constexpr değişkenler aynı zamanda const nesneler olarak ele alınmaktadır. Dolayısıyla örneğin bir constexpr değişkenin adresi ancak const bir göstericiye atanabilir. Örneğin: constexpr int a = 10; // geçerli constexpr int b = a + 1; // geçerli int c = 20; constexpr int d = c + 1; // geçersiz! constexpr değişkene verilen ilkdeğerin sabit ifadasi olması gerekir const int e = 10 constexpr int f = e + 1; // geçerli constexpr değişkenler const değişkenlerin daha katı bir biçimi gibidir. Biz programımız içerisindeki sembolik sabitleri #define ile değil constexpr değişkenlerle de oluşturabilmekteyiz. Örneğin: constexpr int SIZE = 10; Derleyiciler eğer onların adresleri alınmadıysa constexpr değişkenler için hiç yer ayırmayabilirler. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; constexpr int SIZE = 10; int main() { int a[SIZE]; // geçerli, SIZE bir sabit ifadesi constexpr int AA = SIZE + 2; // geçerli, SIZE + 2 bir sabit ifadesi int b = 10; constexpr int CC = b + 2; // geçersiz! b + 2 bir sabit ifadesi değil return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 6. Ders 04/09/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- constexpr belirleyicisi fonksiyonlarda da kullanılabilmektedir. Bu tür fonksiyonlara constexpr fonksiyonlar denilmektedir. constexpr fonksiyonların dile sokulmasından amaç bir fonksiyon çağrısının sabit ifadesi biçiminde derleme zamanında ele alınabilmesini sağlamaktır. const niteleyicisinin böyle bir yeteneği yoktur. Örneğin: constexpr int foo() { return 10; } Bu fonksiyon çağrıldığında derleyici CALL makine komutu ile fonksiyonu çağırmayabilir. Doğrudan sanki çağrıdan 10 değeri elde edilmiş gibi 10 değerini kullanabilir. Örneğin: constexpr int x = foo(); Bu işlem için derleyici aşağıdakine eşdeğer bir kod üretecektir: constexpr int x = 10; constexpr fonksiyonların bazı ayrıntıları vardır. İzleyen paragraflarda bu ayrıntılar üzerinde duracağız. Örneğin: constexpr int x = foo(); // geçerli Yukarıdaki foo fonksiyonu sabit ifadesi oluşturduğu için constexpr bir değişkene ilkdeğer vermede kullanılabilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int foo() { return 10; } constexpr int bar() { return 10; } int main() { int a[foo()]; // geçersiz! fonksiyon çağrıları sabir ifadesi oluşturmaz int b[bar()]; // geçerli, bar çağrısı sabit ifadesi oluşturur return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- constexpr fonksiyonlar aynı zamanda inline fonksiyonlar olarak kabul edilmektedir. inline fonksiyonlar izleyen paragraflarda ele alınmaktadır. Bu durumda her constexpr fonksiyon aynı zamanda bir inline fonksiyondur. Ancak inline fonksiyonlar constexpr fonksiyonlar değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11'de constexpr fonksiyonların birkaç önemsiz bildirimin dışında tek bir satırdan oluşması gerekiyordu. Yani C++11'de constexpr fonksiyonlar yalnızca return içerebiliyordu. Ancak daha sonra bu constexpr fonksiyonların sentaks ve semantiğinde neredeyse her standartta biraz oynama yapılmıştır. C++20'yi dikkate alırsak artık constexpr fonksiyonların içerisinde if gibi for gibi deyimler kullanılabilmektedir. Yani constexpr fonksiyonlar artık neredeyse normal bir fonksiyon gibi yazılabilir duruma getirilmiştir. C++11'de constexpr fonksiyonların çağrıldıklarında sabit ifadesi belirtmesi gerekiyordu. Ancak C++14 ile birlikte constexpr fonksiyonların sabit ifadesi belirtme zorunluluğu da ortadan kaldırılmıştır. Ancak constexpr fonksiyonların sabit ifadesi belirtmesi için onlar çağrılırken argüman olarak sabit ifadelerinin kullanılıyor olması ve return ifadelerinin de sabit ifadesi oluşturuyor olması gerekmektedir. (Tabii return ifadelerinde parametreler kullanılabilir. Eğer fonksiyon sabit ifadesi argümanlarla çağrılmışsa parametrelerin de sabit ifadesi belirttiği kabul edilmektedir.) Başka bir deyişle constexpr fonksiyonların sabit ifadesi belirtmesi için onların "derleme aşamasında çağrılabilir" olması gerekmektedir. Örneğin: constexpr int square(int a) { return a * a; } //... constexpr int a = square(10); // geçerli constexpr int b = square(a + 2); // geçerli int c = 10; constexpr int d = square(c); // geçersiz! square çağrısı sabit ifadesi belirtmiyor. Yukarıda da belirttiğimiz gibi artık constexpr fonksiyonların sabit ifadesi belirtmesi gerekmemektedir. Eğer constexpr bir fonksiyon sabit ifadesi olmayan argümanlarla çağrılmışsa ya da return ifadesinde sabit ifadesi yoksa bu durumda sabit ifadesi belirtmez. Ancak sabit ifadesi belirtmeyen constexpr fonksiyonlar tamamen normal inline fonksiyon gibi işleme sokulmaktadır. Örneğin: int a = 10; int b = square(a); // geçerli Burada contstexpr fonksiyon sabit ifadesi belirtmemektedir. Normal bir fonksiyon olarak ele alınacaktır. constexpr fonksiyonların geri dönüş değerleri ve parametre türleri "literal türlere (literal types)" ilişkin olmak zorundadır. Literal tür kavramı birtakım yeniliklerin sonucunda dile eklenmiştir. Skaler türler ve void türü literal türlerdir. Literal tür tanımına giren diğer türler başka konuların içerisinde ele alınacaktır. C++23'e kadar (henüz resmi basımı yapılmadı) constexpr fonksiyonların en azından bir argümanlı çağrısı sabit iafdesi oluşturabilecek potansiyelde olması gerekiyordu. Ancak bu kural da gevşetilmiştir. constexpr fonksiyonun tüm bildirimlerinde constexpr niteleyicisinin kullanılması gerekir. Ancak tabii constexpr fonksiyonun sabit ifadesi olarak ele alınabilmesi için derleyicinin çağrılma noktasına kadar constexpr fonksiyonunun tanımlaması ile karşılaşmış olması gerekmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ using namespace std; constexpr int square(int a) { return a * a; } int main() { int a[square(10)]; // geçerli, burada square(10) çağrısı sabit ifadesi belirtiyor int b{10}; int c[square(b)]; // geçersiz artık square bir sabit ifadesi belirtimiyor return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi constexpr bir fonksiyon yan etkiye sahip olabilir mi? Örneğin: using namespace std; void foo() { //... } constexpr int bar(int a) { foo(); return a; } Böyle bir bar fonksiyonu constexpr olarak tanımlanabilir mi? İşte C++23'e kadar bu durum mümkün değildi. Çünkü C++23'e kadar constexpr fonksiyonların yalnızca sabit ifadesi üretmesi esastı. Buradaki sorun bar fonksiyonunun aslında hiçbir biçimde sabit ifadesi üretememesidir. bar fonksiyonun sabit ifadesi üretmesi yalnıca return ifadesinde sabit ifadesi oluşturması anlamına gelmemektedir. bar fonksiyonun sabit ifadesi oluşturabilmesi demek her şeyiyle derleme aşamasında yalnızca bir sabit değerin elde edilebilmesi demektir. Ancak C++23 ile birlikte bu durum da gevşetilmiştir. C++23'te bar fonksiyonun tanımlaması geçerlidir. Ancak fonksiyon hiçbir zaman sabit ifadesi oluşturamayacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyonun constexpr olması ve onun sabit ifadesi biçiminde açılabiliyor olması derleyicinin "eğer bir zorunluluk yoksa" onu derleme aşamasında sabit ifadesi olarak ele almasını zorunlu hale getirmemektedir. Örneğin: constexpr int square(int a) { return a * a; } //... int result = square(4); Burada derleyici square çağrısını derleme aşamasında ele alıp oraya 16 yazmak zorunda değildir. Gerçekten de derleyicilerin bir bölümü optimizasyon seçeneklerini açmadıktan sonra bu çağrıyı sabit ifadesi olarak ele almamaktadır. Ancak C++ standratlarına göre buradaki square çağrısı sabit ifadesi olarak kullanılabilir. Dolayısıyla biz bu square çağrısını sabit ifadesi gereken bir yerde kullanırsak artık derleyici kesinlikle onu bir sabit ifadesi gibi ele alacak ve çağrı yerine doğrudan çağrı sonucunda elde edilecek değeri yerleştirecektir: constexpr int square(int a) { return a * a; } //... constexpr int result = square(4); Burada derleyici kodu aşağıdaki gibi ele alacaktır: constexpr int result = 16; Özetle "constexpr bir fonksiyonun çağrısı sabit ifadesi koşullarını sağlıyor olsa bile eğer çağrı bir sabit ifadesi gereken yerde kullanılmamışsa" fonksiyon derleme aşamasında çağrılmak zorunda değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Mademki artık son standartlarda neredeyse constexpr fonksiyonlar normal fonksiyonlar gibi olmuştur. Bu durumda biz her fonksiyonu mümkün olduğunca constexpr yapmalı mıyız? Yukarıda da belirttiğimiz gibi hala constexpr fonksiyonlar için bazı kısıtlar bulunmaktadır. Bu kısıtları ayrıntı olduğu gerekçesiyle burada şimdi ele almayacağız. Ancak olabilecek her fonksiyonu constexpr fonksiyon olarak tanımalamaya çalışmak da iyi bir fikir de değildir. Bunun en önemli nedeni constexpr fonksiyonların default olarak inline kabul edilmesidir. Biz henüz inline fonksiyonları görmemiş olsak da inline fonksiyonların bazı kısıtları ve dezavantajları söz konusu olabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- constexpr fonksiyonlarda fonksiyonun tüm prototip ve tanımlamasında constexpr belirleyicisinin kullanılması gerekmektedir. Örneğin: constexpr int square(int a); //... int square(int a) // geçerli değil! burada da constexpr anahtar sözcüğünün kullanılması gerekirdi. { return a * a; } Tabii constexpr fonksiyonlarda fonksiyon çağrılana kadar derleyicinin fonksiyonun tanımlamasını görmesi gerekmemektedir. Ancak burada constexpr etkisi oluşmaz. Örneğin: constexpr int square(int a); //... int main() { constexpr int a = square(10); // geçersiz! tanımalam görülmediği için constexpr etkisi oluşmaz int a = square(10); // geçerli! constexpr fonksiyonların sabit ifadesi belirtmesi zorunlu değildir. //... return 0; } int square(int a) // geçerli değl! burada da constexpr anahtar sözcüğünün kullanılması gerekirdi. { return a * a; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında constexpr fonksiyonlarla ilgili bazı ayrıntılar da vardır. Örneğin bir sınıfın yapıcı fonksiyonu (constructor) constexpr olabilmektedir. Sınıfın üye fonksiyonları da constexpr olabilmektedir. Konun bu ayrıntıları ilgili bölümlerde ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 16) constexpr fonksiyonlar C++11 ile dile eklendiğinde amaç bu fonksiyonların her zaman sabit ifadesi oluşurması biçimindeydi. Daha sonra bu koşul gevşetildi ve constexpr fonksiyonlar "koşula bağlı olarak sabit ifadesi oluşturur" hale getirildi. İşte C++20 ile birlikte her zmaan sabit ifadesi oluşturması gereken (yani C++11'deki constexpr fonksiyonlar gibi) yeni bir fonksiyon biçimi oluşturuldu. Bunlara consteval fonksiyonlar ya da "immediate fonksiyonlar" denilmektedir. consteval bir fonksiyonun her çağrısının sabit ifadesi oluşturması zorunludur. Örneğin: constexpr int square1(int a) { return a * a; } consteval int square2(int a) { return a * a; } ... int x = 10; int y = square1(x); // geçerli int z = square2(x); // geçersiz! Burada hem square1 hem de square2 çağrıları sabit ifadesi oluşturmamaktadır. Ancak constexpr fonksiyonların zaten çağrıldıklarında sabit ifadesi oluşturma zorunluluğu yoktur. Fakat consteval fonksiyonların çağrıldıklarında sabit ifadesi oluşturma zorunluluğu vardır. Biz consteval fonksiyonları hangi ifadede ve hangi nesneye ilkdeğer vermek için çağırırsak çağıralım çağrının sabit ifadesi oluşturması zorunludur. consteval fonksiyonların constexpr fonksiyonların içerisinde çağrılması bu bakımdan bir farklılık yaratmamaktadır. Örneğin: consteval int square1(int a) { return a * a; } constexpr int square2(int a) { return square1(a); // geçersiz! } Burada square2 fonksiyonu square1 fonksiyonunu çağırmıştır. Ancak square1 çağrısında sabit ifadesi kullanılmamış durumdadır. Bu işlemin tersini yapalım: constexpr int square1(int a) { return a * a; } consteval int square2(int a) { return square1(a); // geçerli } Burada consteval fonksiyon olan square2, constexpr fonksiyon olan square1 fonksiyonunu çağırmıştır. Bu tanımlama derleme aşamasında bir sorun oluşturmayacaktır. Tabii square2 çağrılırken yine sabit ifadesi oluşturması gerekmektedir. Örneğin: int x = square2(10); // geçerli int z = 10; int k = square2(z); // geçersiz! Buradan da görüldüğü gibi önemli olan consteval fonksiyonun çağrısının derleme aşamasında sabit ifadesi oluşturma zorunluluğudur. Yani consteval fonksiyon çağrıldığında her zaman çağrının derleme aşamasında yapılabilir olması gerekmektedir. consteval bir fonksiyon için prototip bulundurulabilir. eğer fonksiyonun prototipinde consteval anahtar sözcüğü kullanılmışsa tanımlamasında da consteval anahtar sözcüğünün kullanılması zorunludur. consteval bir fonksiyonun henüz tanımalama görülmeden kullanılması geçerli bir durum oluşturmamaktadır. Örneğin: consteval int square(int a); int main() { int result = square(5); // geçersiz! henüz tamlama görülmedi //... return 0; } consteval int square(int a) { return a * a; } consteval fonksiyonların adresleri alınamamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 17) C'de ifadeler ya "lvalue" ya da "rvalue" biçimindedir. C'de nesne belirtilen ifadelere lvalue, nesne belirtmeyen ifadelere rvalue denilmektedir. C++ da C'nin bu lvalue ve rvalue terimlerini kullanmıştır. Ancak C++11'e gelindiğinde dile bazı yeni özellikler eklenenince bu lvalue ve rvalue terimleri yetersiz kalmıştır. Böylece C++11 ile birlikte bu konu "value categories" ismiyle genişletilmiştir. C++11'de C'deki ve C++11 öncesindeki C++'taki "lvalue" yine "lvalue" olarak kalmıştır. Ancak "rvalue" artık "prvalue (pure rvalue)" olarak değiştirilmiştir. C++11'deki bu konuda yapılmış olan asıl yenilik "xvalue (expiring value)" biçiminde yeni bir değer kategorisinin oluşturulmasıdır. Yukarıda da belirttiğimiz gibi bu kategorinin oluşturulmasının nedeni C++11 ile birlikte dile eklenen bazı özelliklerdendir. C++11'le birlikte eklenen "xvalue" kategorisi duruma göre hem bir "lvalue" gibi hem de "rvalue" gibi kullanılabilen ifadeleri temsil eder. Bu durumda C++11'e "glvalue (generalized value)" ve "rvalue" biçiminde iki üst kategori de eklenmiştir. glvalue kategorisi lvalue ve xvalue kategorilerinin birleşiminden oluşmaktadır. rvalue kategorisi ise prvalue ve xvalue kategorilerinin birleşiminden oluşmaktadır. Bu durum aşağıdaki şekil ile özetlenebilir: glvalue rvalue / \ / \ / \ / \ lvalue xvalue prvalue C++ standartlarında rvalue denildiğinde artık prvalue ve xvalue anlaşılmaktadır. glvalue denildiğinde ise lvalue ve xvalue anlaşılmaktadır. Eski lvalue terimi lvalue olarak kaldığına göre ve eski rvalue terimi de prvalue haline geldiğine göre o halde xvalue nedir? xvalue terimi yukarıda da belirttiğimiz gibi "hayatını kaybetmek üzere olan" nesneler için kullanılmaktadır. Konunun ayrıntıları başka konularla ilgili olduğu için burada bu ayrıntılara henüz girmeyeceğiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 18) C++'ta C'de olmayan ismine "referans (reference)" denilen göstericilere benzer adres tutan yeni bir tür de bulunmaktadır. C++11'e kadar C++'ta yalnızca referans kavramı ve terimi vardı. Ancak C++11 ile birlikte "move semantic" denilen bir özelliğin C++'a eklenebilmesi için ismine "sağ tafa değeri referansı (rvalue referance)" denilen yeni bir referas türü daha dile eklenmiştir. Artık C++11 ve sonrasında eski referanslar "sol taraf değeri referansı (lavlue reference)" biçiminde isimlendirilmektedir. Yani eski referanslara artık "sol taraf değeri referansları" denilmektedir. C++11 ile eklenen referanslar ise "sağ taraf değeri referanlarıdır". Biz "referans" dediğimizde hem sol taraf değeri referanslarını hem de sağ taraf değeri referanslarını kastedeceğiz. Ancak bunları ayırmamız gerektiğinde açıkça "sol taraf değeri referansı" ya da "sağ taraf değeri referansı" diyeceğiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sol taraf değeri referansı dekleratörde & atomuyla tanımlanır. Referanslara tanımlama sırasında ilkdeğer verilmesi zorunludur. Bir sol taraf değeri referansı tipik olarak aynı türden nesne belirten bir ifade ile (yani bir bir sol taraf değeri ile) ilkdeğer verilerek tanımlanmaktadır. Örneğin: int a{10}; int &r = a; Sol taraf değerine ilişkin referansa ilkdeğer olarak verilen nesneye ilkdeğer verilmiş olması gerekmez. Örneğin: int a; int &r = a; // gayet normal Bir referans ilkdeğer verilerek tanımlandığında derleyici ilkdeğer olarak verilen nesnenin adresini referansın içerisine yerleştirmektedir. Örneğin: double d{3.14}; double &r = d; Burada derleyici d nesnesinin adresini r referansına yerleştirir. Aslında C++ standartlarında referans için bir yer ayrılmayabileceği söylenmiştir. Yani eğer derleyici referansın refere ettiği nesneyi doğrudan kullanbilirse optimizasyon mekanizması ile referans için hiç yer ayırmayabilir. Ancak tipik olarak derleyiciler referans için bir yer ayırırlar ve referanslar da adres tutarlar. Bir referans ilkdeğer verilmeden tanımlanamaz. Örneğin: int &r; // geçersiz! İlkdeğer verme zorunlu Yukarıda da belirttiğimiz gibi sol taraf değeri referansına verilen ilkdeğerin referansla aynı türden nesne belirten bir ifade olması (lavlue) gerekir. Çünkü ancak nesne belirten ifadelerin adresleri alınabilir. Örneğin: int a = 10; int &r = a; // geçerli ilkdeğer verilmiş, r referansının içerisinde a'nın adresi var. Örneğin: int &r = 10; // geeçersiz! Verilen ilkdeğer bir nesne belirtimiyor long a = 10; int &r = a; // geçersiz verilen ilkdeğer farklı türden int a = 10; int &r = a; // geçerli aynı türden bir nesne ile ilkdeğer verilmiş. Bir sol taraf değeri referansına aynı türden bir sol taraf değeri ile ilkdeğer verilmesi durumuna "sol taraf değeri referansına bağlama (binding)" yapmak da denilmektedir. Yani bir sol taraf değeri referansını biz aynı türden bir nesne ile ilkdeğer vererek tanımladığımızda onu sol taraf o nesne ile bağlamış (bind etmiş) oluruz. Örneğin: int a = 10; int &r = a; Burada r referansı a nesnesine bağlanmıştır (bind edilmiştir). --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Teknik olarak referanslarla göstericiler çok benzerdir. Referanslar "düzeyi yüksek bir gösterici" gibidir. Yani hem referanslar hem de göstericiler adres tutarlar. Ancak referanslara adresler onlara ilkdeğer verilirken yerleştirilmektedir. Oysa göstericilere istenildiği zaman adresler atanabilmektedir. Örneğin: int a = 10; int &r = a; Burada a'nın adresini derleyici elde edip r referansına yerleştirmektedir. Oysa örneğin göstericilere nesnelerin adresleri yerleştirilirken adres alma işlemini programcı yapmak zorundadır. Örneğin: int a = 10; int *pi = &a; Göstericilere atama yaparken nesnenin adresini & operatörüyle bizim elde etmemiz gerekir. Bir referans ilkdeğer verildikten sonra kullanıldığında artık referansın içerisindeki adreste bulunan nesnenin (referansın refere ettiği nesnenin) kullanıldığı kabul edilmeketedir. Örneğin: int a = 10; int &r = a; r = 20; Burada aslında r referansının içerisindeki adreste bulunan nesneye 20 yerleştiriliyor. Görüldüğü gibi bir referansa tanımlanma sırasında bir adres yerleştirilmekte ve artık referans o nesneyi göstermektedir (refere etmektedir). Bir referans tanımlandıktan sonra artık başka bir nesneyi gösterir hale getirilemez. Yani referanslar adeta bir nesneye çivilenmektedir. Referans içeren kodların eşdeğer gösterici karşılıkları konunun daha iyi anlaşılmasına yardımcı olmaktadır. Referans içeren bir kodun eşdeğer gösterici karşılığı demekle ""referans yerine gösterici kullanılarak oluşturulan kodu"" kastediyoruz. Mademki referans içerisindeki adres ilkdeğer verildikten sonra bir daha değiştirilememektedir bu durumda referanslar "kendisi const olan const göstericilere" benzetilebilir. Örneğin: int a = 10; int &r = a; Bu işlemin eşdeğer gösterici karşılığı şöyledir: int a = 10; int * const r = &a; Görüldüğü gibi bir referansa bir nesne ile ilkdeğer verildliğinde derleyici o nesnenin adresini kendisi referansa yerleştirmektedir. Ancak göstericilere biz nesnenin adresini elde ederek yerleştirmekteyiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 7. Ders 06/09/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi bir referans ilkdeğer verildikten sonra kullanıldığında her zaman artık refere ettiği nesnenin kullanıldığı kabul edilir. Örneğin: int a = 10; int &r = a; r = 20; Bu işlemin eşdeğer gösterici eşdeğeri şöyledir: int a = 10; int * const r = &a; *r = 20; Tabii C++11 ve sonrasında referanslara da "uniform initializer syntax" ile ilkdeğer verebiliriz. Örneğin: int a{10}; int &r{a}; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi bir referans ilkdeğer olarak verilen nesneyi gösterir (refere eder). Artık referansın başka bir nesneyi göstermesi sağlanamaz. (Çünkü referansı tanımladıktan sonra artık ona değer atadığımızda değer referansın kendisine değil onun refere ettiği nesneye atanmış olmaktadır. Halbuki göstericiler eğer const değillerse herhangi bir zaman başka bir nesneyi gösterebilirler. Referanslar C++'a "güvenli gösterici oluşturmak için" sokulmuştur. Yani referansın içeisindeki adres ilkdeğer verildikten sonra değiştirilemediği için referansın tahsis edilmemiş bir nesneyi gösterme olasılığı kalmamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a{10}; int &r = a; cout << r << endl; r = 20; cout << a << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir referansın adresi alındığında biz referansın kendi adresini almış olmayız. Zaten referansın adresini almanın bir yolu da yoktur. Biz referansın adresini aldığımızda referansın refere ettiği nesnenin adresini almış oluruz. Örneğin: int a = 10; int &r = a; int *pi; pi = &r; // a'nın adresi alınıyor Bu işlemin eşdeğer gösterici karşılığı şöyledir: int a = 10; int * const r = &a; int *pi; pi = &*r; // a'nın adresi alınıyor --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a = 10; int &r = a; int *pi; pi = &r; *pi = 20; cout << a << endl; // 20 cout << r << endl; // 20 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Konunun girişinde de belirttiğimiz gibi C++ standartlarında eğer derleyici bunun bir yolunu bulabilirse referans için hiç yer ayırmayabilir. Örneğin: int a = 10; int &r = a; r = 20; Burada derleyici isterse hiç r referansı için bir yer ayırmadan doğrudan 20 değerini a'ya aatayabilir. Yani derleyici burada aşağıdakine eşdeğer bir kod üretebilir: int a = 10; a = 20; Tabii derleyicilerin referanslar için bir yer ayırmayacağı durumlarda aslında referans kullanmanın gerekliliği yoktur. (inline fonksiyonlarda da derleyici buna benzer optimizasyonlar yaoabilmektedir.) Dolayısıyla biz örneklerimizde referans için bir yer ayrıldığını varsayarak konuları ele alacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- const bir referans tanımlanabilir. const referanslar gösterdiği yer const olan const göstsericiler gibidir. Yani const bir referansın refere ettiği nesnenin değerini değiştiremeyiz. (Referansın kendi içerisindeki adresin referans tanımlandıktan sonra değiştirilemeyeceğini anımsayınız.) Örneğin: int a = 10; const int &r = a; Bu işlemin eşdeğer gösterici karşılığı şöyledir: int a = 10; const int * const pi = &a; const bir referans ile o referansın refere ettiği nesneyi değiştiremeyiz. Örneğin: int a = 10; const int &r = a; r = 20; // geçersiz! referans const --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi C'de (ve tabii C++'ta da) const bir nesnenin adresi gösterdiği yer const olmayan bir göstericiye atanamaz. Benzer durum C++'taki sol taraf değeri referansları için de söz konusudur. Bir sol taraf değeri referansına aynı türden const bir nesneyi bind edemeyiz. Örneğin: const int a{10}; int &r = a; // geçersiz! Buradaki sorun programcının nesneyi const yaptığı halde onun referans tarafından değiştirilmesine olanak tanımasıdır. const bir nesne ancak const bir sol taraf değeri referansına bind edilebilir. Örneğin: const int a{10}; const int &r = a; // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İlkdeğer verme (initialization) bir nesne yaratılır yaratılmaz ona değer yerleştirme işlemine denilmektedir. C'de ve C++'ta (ve diğer bazı dillerde) ilkdeğer verme işlemi anlamına gelen üç temel durum vardır: 1) Tanımlama sırasında '=' ile ya da "Uniform İnitilizer Syntax" ile ilkdeğer verme. Örneğin: int a = 10; int b{10}; 2) Anımsanacağı gibi fonksiyonların parametre değişkenleri fonksiyon çağrıldığında yaratılmakta ve fonksiyon sonlandığında yok edilmektedir. İşte parametreli bir fonksiyonun çağrılması aslında parametre değişkenlerine argümanlarla ilkdeğer verme anlamına gelmektedir. Örneğin: void foo(int a) // int a = 10 gibi düşünülmeli { // .... } foo(10); Burada aslında a parametre değişkenine 10 ile ilkdeğer verilmiştir. 3) return işlemi sırasında bir geçici nesne yaratılıp return ifadesi o geçici nesneye ilkdeğer olarak verilmektedir. Yani return işlemi de aslında bir ilkdeğer verme işlemidir. Örneğin: int foo() { // ... return ifade; } //... x = foo() * 2; Aslında burada şu işlemler arka planda yapılmakatadır: 1) int temp = return ifadesi; 2) x = temp * 2; 3) temp yok yok ediliyor --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyonların parametre değişkenleri sol taraf değeri referansı olabilir. Böyle fonksiyonlar referansla aynı türden bir sol taraf değeri ile (yani nesne belirten ifadeyle) çağrılmalıdır. Bu çağırma sırasında derleyici argümanda belirtilen nesnenin adresini alarak referansa yerleştirir. Artık fonksiyonda referansı kullandığımızda argümanda belirtilen nesneye erişmiş oluruz. Böylece "call by reference" işlemi C++'ta referanslarla da sağlanmış olmaktadır. Örneğin: void foo(int &r) { r = 20; } //... int a = 10; foo(a); Burada aslında fonksiyon çağrıldığında r referansına a'nın adresi yerleştirilmektedir. Fonksiyonda r'yi kullandığımızda aslında r'nin refere ettiği nesne olan a'yı kullanmış oluruz. Buradaki kodun eşdeğer gösterici karşılığı şöyle olacaktır: void foo(int * const r) { *r = 20; } //... int a = 10; foo(&a); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int &r); int main() { int a = 10; foo(a); cout << a << endl; // 20 return 0; } void foo(int &r) // int &r = a { cout << r << endl; // 10 r = 20; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıdaki örneğin göstericilerle oluşturulmuş eşdeğer gösterici karşılığı aşağıdaki gibidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int * const r); int main() { int a = 10; foo(&a); cout << a << endl; return 0; } void foo(int * const r) // int * const r = &a { cout << *r << endl; *r = 20; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İki int nesne içerisindeki değerleri yer değiştiren swap isimli fonksiyonu C'de göstericileri kullanarak yazmıştık. C++'ta aynı fonksiyonu referansları kullanarak da yazabiliriz. Örneğin: void swap(int &a, int &b) { int temp = a; a = b; b = temp; } //... int x = 10, y = 20; swap(x, y); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void swap(int &a, int &b) { int temp = a; a = b; b = temp; } int main() { int x = 10, y = 20; cout << "x = " << x << ", y = " << y << endl; swap(x, y); cout << "x = " << x << ", y = " << y << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta sol taraf değeri referanslarının göstericilerden en önemli farkı şudur: Bir sol taraf değeri referansına aynı türden bir nesne ile ilkdeğer verdiğimizde (yani onu bir nesneye bağladığımızda) artık derleyici o nesnenin adresini referansa yerleştirir. Böylece referans o nesnesyi gösterir hale gelir. O referansın başka bir nesneyi göstermesi artık mümkün değildir. Halbuki göstericilere farklı nesnelerin adreslerini atayarak onların farklı nesneleri göstermesini sağlayabiliriz. Böylece refeanslar daha güvenli bir gösterici olarak kullanılabilmektedir. Bir referansın * ya da [...] operatörleriyle kullanılamayacağına dikkat ediniz. p bir gösterici olmak üzere biz p göstericisinin gösterdiği yerden n ileriye p[n] ifadesi ile erişebiliriz. Ancak r bir referans ise böyle bir kullanım geçerli değildir. Bunun geçerli olması için referansın da dizi referansı olması gerekir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta C'de olduğu gibi göstericiyi gösteren gösterici (pointer to pointer) söz konusu olabilir. Ancak referansı refere eden referans olamaz. Tabii bunun olamamasının en açık nedeni aslında referansın kendi adresinin alınamamasıdır. Örneğin: int a = 10; int &r = a; int &k = r; Burada biz k referansına r referansının adresini yerleştirmiş olmamaktayız. a nesnesinin adresini yerleştirmiş olmaktayız. Çünkü bir referans ilkdeğer verildikten sonra kullanıldığında artık refere ettiği nesne anlamına gelmektedir. Yukarıdaki kodun eşdeğer gösterici karşılıği şöyledir: int a = 10; int * const r = &a; int * const k = &*r; C++'a C++11 ile birlikte eklenen "sağ taraf değeri referansları (rvalue references)" C++'a yeni başlayan C programcıları tarafından sanki "referansın referansı" sanılmaktadır. Oysa sağ taraf değeri referansının bununla hiçbir ilgisi yoktur. Örneğin: int &&r = 10; // bu tamamen başka bir anlama gelmektedir. Bu bildirim tamamen başka bir anlama gelmektedir. İzleyen paragraflarda sağ taraf değeri referansları ele alınacaktır. C++'ta referans dizileri diye de bir kavram yoktur. Örneğin: int &r[3] = {a, b, c}; // böyle bir kavram yok! dolayısıyla derleme zamanında error oluşacaktır. Ancak tabii göstericileri refere eden referanslar oluşturulabilir. Örneğin: int *pi; int *&r = pi; Burada r referansı int * türünden bir referanstır. Yani bir r referansını kullandığınımızda aslında pi göstericisini kullanmış oluruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a{10}; int *pi{&a}; int *&r = pi; // geçerli cout << *r << endl; // 10 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi C'de yapıları genel olarak adres yoluyla fonksiyonlara aktarıyorduk. Bu durumda fonksiyonun parametre değişkeni bir gösterici oluyordu. Biz de fonksiyonu aynı türden bir yapı nesnesinin adresiyle çağırıyorduk. Fonksiyon içerisinde yapı elemanlarına bu gösterici yoluyla -> operatörünü kullanarak erişiyorduk. İşte C++'ta bir yapıyı referans yoluyla da fonksiyona aktarabiliriz. Bu durumda yapının elemanlarına referans yoluyla erişirken -> operatörü değil nokta operatörü kullanılmaktadır. Örneğin: struct DATE { int day; int month; int year; }; //... void disp_date(const struct DATE &r) { cout << r.day << '/' << r.month << '/' << r.year << endl; } //... struct Date date = {6, 9, 2023}; disp_date(date); Bir referansı ilkdeğer verdikten sonra kullandığımızda artık referansın refere ettiği nesneyi kullanmış olmaktayız. Yani biz bu örnekte disp_date içerisinde r referansını kullandığımızda aslında bütünsel olarak date nesnesini kullanmış gibi oluruz. Bir yapı nesnenin bütününü kullanarak elemanlarına erişmek için nokta operatörünün kullanıldığına dikkat ediniz. Bir yapı nesnesinin C++'ta fonksiyona gösterici yoluyla aktarılmasıyla referans yoluyla aktarılması arasında hiçbir etkinlik farklılık yoktur. İkisi de aynı derecede etkindir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; struct DATE { int day; int month; int year; }; void disp_date(const DATE &r) { cout << r.day << '/' << r.month << '/' << r.year << endl; } int main() { struct DATE d = { 10, 12, 2001 }; disp_date(d); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıdaki kodun eşdeğer gösterici kaşılığı aşağıdaki gibidir: --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; struct DATE { int day; int month; int year; }; void disp_date(const struct DATE * const r) { cout << (*r).day << '/' << (*r).month << '/' << (*r).year << endl; } int main() { struct DATE d = {10, 12, 2001}; disp_date(&d); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++2ta Fonksiyonun geri dönüş değeri bir sol taraf değeri referansı olabilir. Bu durumda return ifadesinin aynı türden bir nesne belirten bir ifade olması gerekir. Biz böyle fonksiyonları çağırdığımızda elde ettiğimiz ifade return ifadesindeki nesneyi temsil eder haline gelmektedir. Örneğin: int g_a = 10; int &foo() // int &temp = g_ a { return g_a; } //... foo() = 20; // temp = 20 Burada artık fonksiyon çağrısı bir sol taraf değeri belirtmektedir. Görüldüğü gibi C'de fonksiyon çağrıları her zaman sağ taraf değeri belirtir. Ancak C++'ta fonksiyonun geri dönüş değeri sol taraf değeri referansı ise artık fonksiyon çağrısı sol taraf değeri belirtir. Fonksiyon çağrısına bir değer atadığımızda aslında biz return ifadesindeki nesneye değer atamış oluruz. Tabii nasıl bir fonksiyon yerel bir nesnenin adresi ile geri dönmemeli ise benzer biçimde aynı durum referanslar için de geçerlidir. Yani fonksiyonun geri dönüş değeri bir referans ise biz return ifadesinde yerel bir nense kullanmamalıyız (fonksiyon bittiğinde yerel nesnelerin stack'ten boşaltıldığını anımsayınız.) Örneğin: int &foo() { int x = 10; return x; // geçerli ama tanımsız davranış oluşturur } //... foo() = 20; // dikkat! değer artık olmayan bir nesneye atanmaya çalışılıyor --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int g_a = 10; int &foo() { //... return g_a; // int &temp = g_a } int main() { foo() = 20; cout << g_a << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıdaki kodun eşedeğer gösterici karşılığı aşağıdaki gibidir: --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int g_a = 10; int *foo() { //... return &g_a; } int main() { *foo() = 20; cout << g_a << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta "geçici nesne (temporary object)" denildiğinde "derleyicinin kendisi tarafından yaratılan ve yok edilen isimsiz nesneler" anlaşılmaktadır. Örneğin fonksiyonun geri dönüş değerinin oluşturulması bir geçici nesne yoluyla yapılmaktadır. İşte C++'ta bazı durumlarda derleyici geçici nesneler yaratılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- const bir sol taraf değeri referansını biz bir sağ taraf değeri ile ilkdeğer vererek tanımlayabiliriz. Bu durumda referansın const olması gerektiğine dikkat ediniz. Örneğin: int &r = 10; // geçersiz! referans const değil const int &k = 10; // geçerli, referans const Tabii bu durumda ilkdeğer olarak verilen sağ taraf değerinin referansla aynı türden olması da gerekmez. Örneğin: const int &r = 3.14; // geçerli const bir referansa sağ taraf değeri bind edildiğinde derleyici önce referans türüyle aynı türden geçici bir nesne yaratır. Sonra sağ taraf değerini bu geçici nesneye yerleştirir. Sonra da bu geçici nesnenin adresini referansa yerleştirir. Örneğin: const int &r = 10; Burada aslında şu işlemler yapılmaktadır: int temp = 10; const int &r = temp; Burada geçici nesnenin referans türünden olduğuna dikkat ediniz. Örneğin: const int &r = 3.14; Bu kodun eşdeğeri de şöyledir: int temp = 3.14; // dikkat bilgi kaybı oluşacak ama geçerli const int &r = temp; Burada artık r referansının gösterdiği int nesnede 3.14 değeri değil 3 değeri olacaktır. Bu örneklerde r referansının artık derleyici tarafından yaratılmış geçici nesneleri gösterdiğine dikkat ediniz. Referans const olduğu için o nesneler üzerinde değişklik yapamayacağız. C++ standartlarına göre bu biçimde yaratılmış olan geçici nesneler referansın ömrü boyunca yaşamaya devam eder. Referans yaşamını kaybederken bu geçici nesne de derleyici tarafından yok edilecektir. Bu geçici nesnenin yok edilmesi ile programcının uğraşmadığına dikkat ediniz. Bu biçimde const bir sol taraf değeri referansına sağ taraf değeri ile ilkdeğer verilirken eğer "uniform initializer syntax"" kullanılıyorsa ya da küme parantezi sentaksı kullanılıyorsa bu durumda daraltıcı dönüştürmelere izin verilmemektedir. Örneğin: const int &r{3.14}; // geçersiz! double -> int dönüştürmesi daraltıcı bir dönüştürme const int &k = {3.14}; // C++11'den sonra geçersiz! double -> int dönüştürmesi daraltıcı bir dönüştürme --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { const int &r = 10; cout << r << endl; // geçici nesne kullanılıyor r = 20; // error! Referans const return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sol taraf değeri referansına bir sol taraf değeri ile ilkdeğer verildiğinde derleyicinin tipik olarak bu ilkdeğer olarak verilen nesnenin adresini referansa yerleştirdiğini söylemiştik. Bu durumda bir sol taraf değeri referansına bir dizi ile ilkdeğer verdiğimizde derleyici dizinin adresini referansa yerleştiecektir. Anımsanacağı gibi C'de bir dizinin adresi alınırsa bu adres "dizi göstericisi (pointer to array)" denilen bir göstericiye yerleştirilebilir. İşte buradan hareketle C++'ta eğer biz sol taraf değeri referansına bir diziyi bind edersek referansın da "dizi referansı (reference to array)" olması gerekmektedir. Örneğin: int a[10]; int &r = a; // geçersiz! int (&r)[10] = a; // geçerli Burada artık r referansını kullandığımızda tamamen a dizisini kullanmış gibi olmaktayız. Yukarıdaki işlemin eşdeğer gösterici karşılığı şöyledir: int a[10]; int (*r)[10] = &a; Konunun daha iyi anlaşılması için C'de "dizi göstericileri (pointer to array)" kavramının biliniyor olması gerekmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 8. Ders 11/09/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi bir sol taraf değeri referansını farklı türden bir nesneyle ilkdeğer vererek tanımlayamayız. Örneğin: double a = 10.2; int &r = &a; // geçersiz! Ancak sol taraf değeri referansı const ise bu mümkündür. Bu durumda yine derleyici ilkdeğer olarak verilen nesneyi referansla aynı türden bir geçici nesnenin içine yerleştirir, bu geçici nesneyi referansa bind eder. Örneğin: double a = 10.2; const int &r = a; Bu işlemin eşdeğeri şöyledir: double a = 10.2; int temp = a; const int &r = temp; Tabii eğer ilkdeğer verme yine "uniform initializer" sektasıyla ya da küme parantezi sentaksıyla yapılıyorsa bilgi kaybına yol açabilen daraltıcı dönüştürmelere izin verilmemektedir. Örneğin: double a = 10.4; const int &r{a}; // geçersiz! cont int &k = {a}; // geçersiz! --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ve sonrasında dil hepten karmaşık hale geldiği için daha önce kullanılmayan bazı terimler de uydurulmuştur. Örneğin C++11'de bir prvalue ifadesinin bu tür durumlarda bir geçici nesneye dönüştürülmesine "temporary materializaition" denilmektedir. Biz kursumuzda yukarıda "sol taraf değeri (lvalue)" ve "sağ taraf değeri (rvalue)" terimlerini C++11 ve sonrasındaki "value category" tanımına göre kullanıyoruz. Örneğin "const bir referansa bir sağ değeri ile ilkdeğer verilebilir" derken bu sağ taraf değeri "prvalue" ya da "xvalue" anlamına gelmektedir. Ancak "bir sol değeri referansına bir aynı türden bir sol taraf değeri ile ilkdeğer verilebilir" derken buradaki "sol taraf değeri" glvalue olmayan sol taraf değeri anlamına gelmektedir. C++11 ile birlikte eklenen "value category" terimlerini aşağıda yeniden vermek istiyoruz: glvalue rvalue / \ / \ / \ / \ lvalue xvalue prvalue --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi C++11 ile birlikte ismine "sağ taraf değeri referansları (rvalue references)" denilen başka bir referans türü daha dile eklenmiştir. Böylece eski referansların da isimleri "sol taraf değeri referansları (lavlue references)" biçiminde değiştirilmiştir. Sağ taraf değeri referansları dekleratörde && atomuyla belirtilir. (Burada && atomları referansın referansı gibi bir anlama gelmemektedir. Zaten refereransın adresi alınamamaktadır.) Bir sağ taraf değeri referansı bir sağ taraf değeri ile (yani nesne belirtmeyen bir ifade ile) ilkdeğer verilerek tanımlanmak zorundadır. Biz nasıl bir (const olmayan) sol taraf değeri referansına bir sağ taraf değerine ilişkin ifadeyi bind edemiyorsak bir sağ taraf değeri referansına da bir sol taraf değerine ilişkin ifadeyi bind edemeyiz. Örneğin: Örneğin: int a = 10; int &&r = a; // geçersiz! a bir sağ taraf değeri değil int &&r = 10; // geçerli, a bir sağ taraf değeri Örneğin: int a = 10, b = 20; int &&r = a + b; // geçerli, a + b sağ taraf değeri belirtiyor Tabii normal olan durum bir sağ taraf değeri referansına aynı türden bir sağ taraf değeri ile ilkdeğer vermektir. Ancak bir sağ taraf değeri referansı farklı bir sağ taraf değeri türü ile de ilkdeğer verilerek de tanımlanabilir. Örneğin: int &&r = 3.14; // tuhaf ama geçerli long a = 10, b = 20; int &&k = a + b; a + b long türden ama geçerli Tabii yine eğer ilkdeğer verme "uniform initializer" sentaksıyla yapılıyorsa ya da küme parantezleri ile yapılıyorsa bu durumda verilen ilkdeğerin daraltıcı dönüştürme oluşturmaması gerekir. Örneğin: int &&r{3.14}; // geçersiz! --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sağ taraf değeri referansına bir sağ taraf değeri ile ilkdeğer verdiğimizde derleyici o sağ taraf değerini referansla aynı türden bir geçici nesnenin içerisine yerleştirir ve referansa o geçici nesnenin adresini atar. Örneğin: int &&r = 10; Bu işlemin eşdeğeri şöyledir: int temp = 10; int &&r = temp'in adresi Pekiyi aynı işlemi zaten const olan sol taraf değeri referansı ile de yapabiliyorduk. O halde değişen ne vardır Buradaki tek fark sağ taraf değeri referansının const olmaması ve dolayısıyla o geçici nesneyi değiştirebilmesidir. Bu biçimde bir prvalue ifadesi için geçici nesne yaratılmasına C++17'de "temporary materialization" dendiğini anımsayınız. Yine bir sağ taraf değeri referansını kullandığımızda referansın refere ettiği nesneyi kullanmış oluruz. Yani kullanım bakımından sağ taraf değeri referansının sol taraf değeri referansından bir farkı yoktur. Örneğin: int &&r = 10; r = 20; // geçerli, geçici nesne değiştiriliyor const int &k = 10; k = 20; // geçersiz! referans const Sağ taraf değeri referansları kullanıldıklarında artık sol taraf değeri belirtirler. Örneğin: int &&r = 10; int &k = r; // geçerli k = 20; // geçerli, geçici nesneye değer atanıyor int &&m = r; // geçersiz! sağ taraf değeri referansına sol taraf değeri bind edilemez Burada artık biz k referansını kullandığımızda yaratılan geçici nesneyi kullanıyor duudmda oluruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int &&r = 10; cout << r << endl; // 10 r = 20; // geçerli cout << r << endl; // 20 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sağ taraf değeri referansına biz farklı türden bir sol taraf değeri ile de ilkdeğer verebiliriz. Bu durumda yine farklı türden sol taraf değeri derleyici tarafından referans ile aynı türden geçici geçici bir nesne yaratılarak o geçici nesneye yerleştirilmekte ve geçici nesnenin adresi referansa atanmaktadır. Örneğin: double d = 3.14; int &&r = d; // geçerli! Bu işlemin eşdeğeri şöyledir: double d = 3.14; int temp = d; int &&r = temp'in_adresi; Tabii burada biz referans yoluyla referansın refere ettiği yeri deştirdiğimizde d'yi değiştirmiş olmayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { double d = 3.14; int &&r = d; cout << r << endl; // 3 r = 10; cout << d << endl; // 3.14 cout << r << endl; // 10 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi sağ taraf değeri referansı da const olabilir mi? Tabii sağ taraf değeri referansları da const olabilir. Ancak sağ taraf değeri referanslarının dile eklenmesinin asıl nedeni zaten sağ taraf değerini güncelleyebilmesidir. Yani const bir sağ taraf değeri referansının anlamlı bir kullanımı yoktur. const bir sağ taraf değeri referası zaten const bir sol taraf değeri referansıyla aynı işleve sahiptir. Özetle sağ taraf değeri referansları const olabilse de bunun aslında anlamı yoktur. Bu nedenle programlarda böyle bir kullanım görmezsiniz. Örneğin: const int &&r = 10; // geçerli r = 20; // geçersiz! r const --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yine eğer sağ taraf değeri referansına ilkdeğer verme işlemi ""uniform initializer" sentaksı ile ya da küme parantezleri kullanılarak yapılıyorsa daraltıcı dönüştürmelere izin verilmemektedir. Örneğin: int &&r{3.14}; // geçersiz! double -> int dönüştürmesi daraltıcı dönüştürme int &&k = {3.14}; // geçersiz! geçersiz! double -> int dönüştürmesi daraltıcı dönüştürme --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyonun parametre değişkeni sağ taraf değeri referansı olabilir. Bu durumda fonksiyon bir sağ taraf değeri argüman yapılarak ya da farklı türden sol taraf değeri argüman ile çağrılmak zorundadır. Örneğin: void foo(int &&r) { cout << r << endl; } //... int a = 10, b = 20; double d = 3.14; foo(10); // geçerli foo(a + b); // geçerli foo(d); // geçerli foo(a); // geçersiz! --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int &&r) { cout << r << endl; } int main() { int a = 10, b = 20; foo(10); // geçerli foo(a + b); // geçerli foo(a); // geçersiz! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyonun geri dönüş değeri sağ taraf taraf değeri referansı olabilir. Böyle fonksiyonlar çağrıldığında "xvalue" belirtirler. Bu konu ileride ele alınacaktır. Ancak bu tür durumlarda yaratılacak geçici nesne fonksiyonun yerel değişkeni biçiminde yaratılır. Dolayısıyla aşağıdaki gibi bir tanımlama "tanımsız davranışa (undefined behavior)" yol açmaktadır: int &&foo() { return 10; // dikkat temporary yerel bir nesne olarak yaratılacak } Bu tanımlamanın eşdeğeri şöyledir: int &&foo() { int temp = 10; return temp'in_adresi; } Görüldüğü gibi fonksiyondan çıkıldığında bu geçici nesne yok edilecektir. Böyleci geçici nesnelerin fonksiyon içerisinde onun stack'inde yaratıldığına dikkat ediniz. Yukarıda da belirttiğimiz gibi geri dönüş değeri sağ taraf değeri referansı olan fonksionların çağrı ifadeleri "xvalue" belirtmektedir. "xvalue" bir "glavlue" olmasına karşın "lvalue" değildir. Dolayısıyla bir "xvalue" sol taraf değeri referansına bind edilemez. Örneğin: int &r = foo(); // geçersiz! xvalue bir lvalue değildir. Ancak "xvalue" bir "rvalue" olduğuna göre sağ taraf değeri referansına bind edilebilir. Örneğin: int &&r = foo(); // geçerli Pekiyi bu durumda fonksiyonun geri dönüş değerinin sağ taraf değeri referansı olmasının ne anlamı vardır? İşte aslında ilerideki konularda da görüleceği gibi bir sol tafaf değeri "sanki bir sağ taraf değeri imiş gibi" geri döndürülmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int &&foo() { return 10; // geçerli ama tanımsız davranışa yol açar } int main() { const int &r = foo(); cout << r << endl; // tanımsız davranış! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Geri dönüş değeri sağ taraf değeri referansı olan fonksiyonların return ifadelerindeki sağ taraf değeri için yaratılacak yerel değişkeninin o fonksiyonun yerel değişkeni gibi olması böyle bir fonksiyonun anlamsız olduğunu akla getirmektedir. Ancak aslında bu tür fonksiyonlar belli bir amaç doğrultusunda kullanılabilmektedir. Bu tür fonksiyonların kullanımına ilşkin anlamlı örneklerle ilerideki konularda karşılaşacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 19) C++'a dinamik bellek yönetimi için new ve delete isimli iki operatör eklenmiştir. Tabii C++ C'nin standart kütüphanesini de kapsadığı için biz C++'ta C'de kullandığımız malloc, calloc, realloc ve free fonksiyonlarını da kullanbiliriz. Ancak C++'ta dinamik bellek tahsisatı için C++'a özgü new ve delete operatörlerinin kullanılması iyi bir tekniktir. new operatörü iki sentaks biçiminde kullanılabilir: 1) new 2) new <[uzunluk ifadesi]> Birinci biçimde heap'te ilgili türden tek bir elemanlık yer tahsis edilir. İkinci biçimde ilgili türden belirtilen uzunlukta yer tahsis edilir. new operatörü tahsis edilen alanın başlangıç adresini ilgi türden bize verir. Örneğin: int *pi; char *pc; pi = new int; pc = new char[10]; Burada birinci satırda heap'te bir int uzunluğunda alan tahsis edilip onun başlangıç adresi pi göstericisine atanmıştır. İkinci satırda ise heap'te char türden 10 elemanlı bir dizi tahsis edilmiş dizinin başlangıç adresi pc göstericisine atanmıştır. new operatörü ilgili tür türünden bir adres vermektedir. (malloc, calloc ve realloc fonksiyonlarının void adres verdiğini anımsayınız.) Örneğin: int *pi; pi = new char[10]; // geçersiz! char türden adres int türden göstericiye atanmış! new operatörü ile tahsis etmiş olduğumuz alan içerisinde çöp değerler vardır. new operatörü normal kullanımda başarıszlık durumunda bad_alloc isimli bir exception fırlatmaktadır. Dolayısıyla programcı tahsisatın başarısını NULL gösteri ile kontrol etmez. Böyle bir kontrol yapmayınız. Exception sözcüğü programın çalışma zamanı sırasında oluşan problemli durumları anlatmaktadır. C++'ta bir exception oluştuğunda programcı oluşan exception'ı ele alabilir (handle edebilir) bu durumda çalışma normal bir biçimde başka yerden devam eder. Ancak programcı oluşan exception'ı ele almazsa program çökmektedir. C++'ta exception ayrıntıları olan geniş bir konudur. Biz de ilerideki bçlümlerde bu konuyu ayrı başlık altında ele alacağız. new her ne kadar bir operatörse de aslında tahsisat işlemi yine programın çalışma zamanı sırasında akış new işleminin yapıldığı yere geldiğinde yapılmaktadır. new operatörü ""operator new" denilen bir tahsisat fonksiyonunu çağırarak tahsisatını yapmaktadır. new operatörünün bir operatör olmasının nedeni sınıflar konusuyla ilgilidir. new operatörüyle bir sınıf türünden tahsisat yapıldığında new operatörü tahsisat yapıldıktan sonra sınıfın "yapıcı fonksiyonunun (constructor)" çağrılmasına da yol açmaktadır. T bir tür belirtmek üzere new operatörü ile bir elemanlık yer tahsisatını iki biçimde yapabiliriz: new T new T[1] Bu iki new ifadesi de bir elemanlık yer tahsis ediyor olsa da bu iki ifadenin arasında semantik bazı farklılıklar vardır. İzleyen paragraflarda bu durum ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int *pi; pi = new int[10]; for (int i = 0; i < 10; ++i) pi[i] = i * i; for (int i = 0; i < 10; ++i) cout << pi[i] << ' '; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 9. Ders 13/09/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new ile yapılan dinamik tahsisatlar delete operatörü ile serbest bırakılır. delete operatörünün de iki sentaktik biçimi vardır: 1) delete 2) delete[] Eğer new ile köşeli parantezsiz tahsisat yapılmışsa bunun boşaltımı köşeli parantezsiz delete yapılmalıdır. Eğer new ile köşeli parantezli bir tahsisat yapılmışsa bunun boşaltımı da köşeli parantezli delete ile yapılamlıdır. Örneğin: int *pi; char *pc; //... pi = new int; pc = new char[32]; //... delete[] pc; delete pi; Tahis edilen alan 1 elemanlık bile olsa eğer tahsisat new operatörünün köşeli parantezli biçimi ile yapılmışsa alanı serbest bırakmak için delete operatörünün köşeli panatezli biçimi kullanılmalıdır. Örneğin: int *pi = new int[1]; //... delete[] pi; delete anahtar sözcüğünün yanındaki köşeli parantezin içi her zaman boş olmalıdır. Buraya yanlışlıkla boşaltılacak eleman sayısı belirten bir ifade yazmayınız. new operatörü ile tahsis edilen alan delete operatörü ile boşaltılmazsa Windows, macOS, Linux gibi yaygın işletim sistemlerinin hepsinde program bittiğinde bu alanlar otomatik olarak boşaltılmaktadır. Çünkü işletim sistemlerinde genel olarak "heap alanı prosese özgüdür". Yani her prosesin heap alanı diğerlerinden farklıdır. Başka bir deyişle bir programda yapılan dinamik tahsisatın diğer çalışmakta olan programlara hiçbir etkisi yoktur. Ancak heap alanının prosese özgü olduğu C ve C++ standartlarında garanti edilmemiştir. (Yani başka işletim sistemlerinde heap alanı prosese özgü olmayabilir.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int *pi; pi = new int[1]; delete [] pi; // doğru pi = new int[10]; delete[] pi; // doğru pi = new int; delete pi; // doğru return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tabii tıpkı malloc fonksiyonunda olduğu gibi new operatörü ile tahsisat yapılırken de uzunluğun sabit ifadesi ile belirtilmesi gerekmez. Örneğin: char *pc; int size; //... cin >> size; pc = new char[size]; //... delete[] pc; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte new operatörü ile tahsis edilen alana küme paramtezleriyle ilkdeğer verilebilmektedir. Örneğin: pi = new int[3] {10, 20,30}; Tabii tahsis edilen dizideki az sayıda elemana ilkdeğer verilebilir. Bu durumda geri kalan elemanların hepsi sıfırlanmaktadır. Örneğin: pi = new int[10] {1}; // geri kalan elemanlar 0 Küme parantezinin içi boş da bırakılabilir. Bu durumda da tüm elemanlar sıfırlanır. Örneğin: pi = new int[10] {}; // tüm elemanlar sıfırlanır İlkdeğer verilirken köşeli parantezlerin içi boş bırakılabilir. Bu durumda derleyici verilen ilkdeğerleri sayar ve dinamik dizinin o kadar elemandan oluştuğnu kabul eder. Örneğin: pi = new int[] {10, 30, 40}; // 3 elemanlık tahsisat yapılıyor İlkdeğer verme sentaksında köşeli parantezlerin içerisinin sabit ifadesi olması gerekmemektedir. (Bazı dillerde benzer sentakslarda sabit ifadesi zorunluğu vardır.) Ancak bu durumda tahsisat uzunluğundan fazla ilkdeğer verilirse "tanımsız davranış" oluşmaktadır. Örneğin: int size; cout << "size:"; cin >> size; int *pi = new int[size] {1, 2, 3, 4, 5}; Burada size değeri 5'ten küçükse tanımsız davranış oluşacaktır. size değeri 5'ten büyükse tahsis edilen alandaki diğer değerler sıfırlanacaktır. Tabii C++11 ve sonrasında bütün küme parantezi sentakslarında "daraltıcı dönüştüre (narrowing conversion)" yasaklanmıştır. Örneğin: int *pi = new int[] = {1, 2, 3.14}; // geçersiz! double -> int dönüştürmesi daaraltıcı dönüştürme --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int *pi; pi = new int[3]{ 10, 20, 30 }; // C++11 ile birlikte for (int i = 0; i < 3; ++i) cout << pi[i] << endl; delete [] pi; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new operatörü ile biz bir gösterici dizisi de tahsis edebiliriz. Tabii bu durumda tahsis edilen gösterici dizisinin adresinin göstericiyi gösteren göstericye atanması gerekir. Örneğin: int a = 10, b = 20, c = 30; int **ppi; ppi = new int *[] {&a, &b, &c}; //... delete[] ppi; Burada tahsisatın new int * biçiminde yapıldığına dikkat ediniz. Tabii boşaltım yine delete operatörünün köşeli parantezli versiyonu ile yapılmalıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { const char **names; names = new const char *[] {"ali", "veli", "selami", NULL}; for (int i = 0; names[i] != NULL; ++i) cout << names[i] << endl; delete[] names; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new operatörü ile çok boyutlu diziler için tahsisatlar yapabiliriz. Ancak çok boyutlu dizilerin başlangıç adreslerinin yerleştirileceği göstericilerin dizi göstericileri türünden olması gerekir. Örneğin: int *pi; pi = new int[3][4]; // geçersiz! tür uyuşmazlığı var! new int[3][4] bir tahsisatın adresinin atanacağı göstericisinin int (*)[4] türünden olması gerekmektedir. Örneğin: int (*pa)[4]; pa = new int[3][4]; // geçerli ------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int (*pa)[4]; pa = new int[3][4]{{1, 2, 3, 4}, {5, 6, 7, 8}, {0, 1, 2, 3}}; for (int i = 0; i < 3; ++i) { for (int k = 0; k < 4; ++k) cout << pa[i][k] << " "; cout << endl; } delete[] pa; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ C'nin standart kütüphanesini de desteklediğine göre biz C++ programları içerisinde C'nin malloc, calloc, realloc ve free fonksiyonlarını kullanabiliriz. Ancak bir C++ programı içerisinde hem "new ve delete" hem de "malloc, calloc, realloc ve free" fonksiyonları birlikte kullanılmamalıdır. Tahsisat işlemleri için tahsisat algoritmaları kullanılır. İşte C++'ın new ve delete operatörlerinin kullandığı tahsisat algaoritması ile C'nin dinamik bellek fonksiyonlarının kullandığı tahsisat algoritması farklı olabilmektedir. Bunun sonucu olarak da C++'ın new operatörü ile tahsis edilmiş olan bir alan C'nin tahsisat fonksiyonları tarafından tahsis edilmemiş olarak gözükebilir.. Tabii tersi de söz konusu olabilmektedir. Bazı C++ derleyicilerinde new işlemi sırasında çağrılan operator new fonksiyonu kendi içerisinde malloc fonksiyonu ile tahsisatı yapıyor olabilir. Benzer biçimde delete işlemi ile tahsis edilen alan serbest bırakılırken operator delete fonksiyonu free fonksiyonu ile bunu yapıyor olabilir. Bu sistemlerde bu iki dilin tahsisat mekanizmasının birlikte kullanılması soruna yol açmayabilir. Ancak C++ standartları bunun garantisini vermemiştir. Bu nedenle aynı programda bu iki grup tahsisat sistemini birlikte kullanmak sorunlara yol açabilmektedir. ------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new ve delete operatörlerine ilişkin bazı ayrıntılar vardır. Örneğin bu operatörlerin "placement" versiyonları bulunmaktadır. Biz bu placement versiyonlar sayesinde örneğin new operatörünün bellek tahsisatı yapılamadığı durumda bad_alloc ile exception fırlatmak yerine NULL adres ile geri dönmesini sağlayabiliriz. Bu operatörlerin "placement" versiyonları "operatör fonksiyonları" konusunda ele alınacaktır. ------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 20) C++'ta C'nin tüm standart fonksiyonları kullanılabilir. Ancak bu konuda birkaç ayrıntı vardır. Birincisi C++'ta C'nin başlık dosyalarının ismi değiştirilmiştir. C'nin standart bir başlık dosyası olmak üzere bunun C++'taki ismi biçimindedir. Uzantının olmadığına ve başlık dosyalarının başına 'c' harfi getirildiğine dikkat ediniz. Örneğin başlık dosyasının C++'taki ismi biçimindedir. İkinci farklılık standart C fonksiyonlarının std isim alanına taşınmış olmasıdır. Biz henüz isim alanları (name space) konusunu görmedik. Ancak C++'ın tüm kütüphanesindeki isimler std isim alanında ya da o isim alanın içerisindeki bir isim alanında bulunmaktadır. Bir isim alanındaki bir ismi kullanmak için :: sentaksı kullanılmaktadır. Burada :: operatörüne "çözünürlük operatörü (scope resolution operator)" denilmektedir. Biz örnek programlarımızda bu çözünürlük operatörünü kullanmamak için programın yukarısına aşağıdaki direktifi yerleştirdik: using namespace std; Bu direktif bizim std isim alanındaki isimleri çözünürlük operatörü olmadan doğrudan kullanmamızı sağlamaktadır. Eğer bu operatör olmasaydı cout gibi endl gibi isimleri çözünürlük operatörü ile kullanmak zorunda kalırdık. Örneğin: std::cout << "test" << std::endl; İsim alanları konusu ayrıntıları olan ayrı bir konudur. Kursumuzun ilerleyen bölümlerinde bu konu zaten ele alınacaktır. C++'ta biçiminde belirttiğimiz başlık dosyalarındaki isimlerin aynı zamanda global isim alanında bulunup bulunmayacağı derleycicileri yazanların isteğine bırakılmıştır. Ancak C++'ta biz biçimindeki başlık dosyalarını da C'de olduğu gibi include edebiliriz. Bu başlık dosyalarındaki isimler bu durumda global isim alanına yerleştirilmiş olmak zorundadır. Fakat dosyalarının aynı zamanda std isim alanına da yerleştirilip yerleştirilmeyeceği derleyicileri yazanların isteğine bırakılmıştır. Ancak C++'ta C'nin başlık dosyalarının biçiminde include edilmesi kötü bir tekniktir. Biz kurusumuzda C'nin başlık dosyalarını biçiminde include edeceğiz ve oradaki isimlerin std isim alanında olduğunu varsayacağız. Her ne kadar C++ C'nin standart fonksiyonlarını destekliyorsa da ayrıca C++'ın kendine özgü "template" tabanlı bir standart kütüphanesi de vardır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { printf("This is a test\n"); // printf std isim alanının içeisinde return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 21) C++'ta makrolara benzer ismine "inline fonksiyon" denilen fonksiyonlar da bulunmaktadır. Aslında inline fonksiyon kavramı C'de de çok uzun süredir çeşitli derleyicilerde birer eklenti (extension) biçiminde bulunuyordu. Ancak inline fonksiyonlar C ailesine ilk kez C++'ta resmi olarak eklenmiştir. C++'tan sonra inline fonksiyonlar C99 ile birlikte C'ye de sokulmuştur. Ancak C++'ın inline fonksiyonlarıyla C99'un inline fonksiyonları arasında semantik farklılıklar vardır. Ayrıca C99 ile C'ye eklenmden önce derleycilerin eklenti biçiminde bulundurduğu inline fonksiyon semantiklerinde de farklılıklar bulunmaktadır. Tabii biz burada C++'taki inline fonksiyonları ele alacağız. Bir fonksiyon "inline" belirleyici (function specifier) kullanılarak inline yapılabilir. Örneğin: inline void foo() { //... } inline bir fonksiyon çağrıldığında derleyici fonksiyon çağrısı yerine inline fonksiyonun iç kodunu çağrılan yere enjekte edebilmektedir. Böylece fonksiyon çağrısının bazı maliyetleri (function call overhead) ortadan kaldırılmış olmaktadır. Bir fonksiyon çağrıldığında derleyiciler normal olarak fonksiyonu CALL makine komutuyla çağırmaktadır. CALL işleminden önce argümanlar fonksiyonun parametre değişkenlerine kopyalanır. Fonksiyonda da geri dönüşü sağlamak için RET makine komutu gerekir. Yine fonksiyonun stack üzerinde işlemler yapabilmesi için "stack frame düzenlemesine" gereksinim duyulabilmektedir. Bu düzenleme de birkaç makine komutu ile yapılmaktadır. İşte fonksiyonların CALL işlemi yerine iç kodlarının çağrılan yere enjekte edilmesi bu ekstra makine komutlarını elimine edebilmektedir. Örneğin aşağıdaki gibi bir fonksiyon söz konsu olsun: int add(int a, int b) { return a + b; } Bu fonksiyonu biz şöyle çağırmış olalım: result = add(10, 20); 32 Bit Intel işlemcilerinin kullanıldığı sistemlerde add fonksiyonu için tipi olarak şu makine komutları üretilmektedir:Ç _add: push ebp mov ebp, esp mov eax, [ebp + 8] add eax, [ebp + 12] mov esp, ebp pop evp ret Burada aslında asıl toplamayı yapan aşağıdaki iki makine komutudur: mov eax, [ebp + 8] add eax, [ebp + 12] Diğer makine komutları ekstra maliyete yol açmaktadır. Fonksiyon çağrılması sırasında da tipik olarak şu makine komutları üretilmektedir: push 20 push 10 call _add add esp, 8 Görüldüğü gibi aslında iki makine komutu ile yapılabilecek işlem 11 makine komutuyla yapılabilmiştir. Şimdi add fonksiyonunun inline olduğunu düşünelim: inline int add(int a, int b) { return a + b; } ... result = add(10, 20); İşte bu durumda derleyici fonksiyonun iç kodunu çağrılan yere enjekte ederek çağırma işlemini elimine edebilmektedir. inline açımda (inline expansion) üretilecek komutlar muhtemelen aşağıdaki gibi olacaktır: mov eax, 10 add eax, 20 mov result, eax Klasik olarak C'de inline fonksiyon öncesinde bu tür optimizasyonlar parametreli makrolarla sağlanıyordu. Örneğin: #define add(a, b) ((a) + (b)) Böylece kod enjekte işlemi derleme modülü tarafından değil önişlemci modülü tarafından yapılıyordu. Örneğin: result = add(10, 20); Önişlemci bu kodu aşağıdaki biçime dönüştürüyordu: result = ((10) + (20)) Ancak parametrelerin makroların şu dezavantajları vardır: 1) Yazımları oldukça zordur. 2) Tanımsız davranışa yol açabilirler. Bu konuda dikkat edilmesi gerekir. 3) Birkaç satırlık makroların yazımları problemlidir. 4) Önişlemci C'yi bilmemektedir. Dolayısıyla pek kontrolü önişlemci yapamamaktadır. Örneğin: #define square(a) ((a) * (a)) Böyle bir makroyu aşağıdaki gibi çağırmış olalım: int x = 10; result = square(++x); Burada square bir fonksiyon olsaydı kod gayet normal olarak ele alınacaktı. Ancak makrolarda açım aşağıdaki gibi yapılacağından tanımsız davranış oluşacaktır: result = ((++x) * (++x)) Ancak tabii inline bir fonksiyonun çokça çağrılması neticesinde bir kod büyümesi söz konusu olabilir. O halde kısa (örneğin bir iki satırlık) fonksiyonların inline yapılması uygun bir yaklaşımdır. inline belirleyicisi "bir emir değil rica" niteliğindedir. Yani biz bir fonksiyonu inline yaptığımızda derleyici onun iç kodunu enjekte etmek zorunda değildir. inline fonksiyona yine derleyici normal fonksiyon muamelesi yapabilir. Bu konuda herhangi bir uyarı mesajı da vermeyebilir. inline konusu derleyicilerde genel olarak optimizasyon konusu ile ilişkilendirilmiştir. Yani derleyicimizin optimizasyon seçeneklerini yeteri kadar açmazsak derleyicimiz inline açım yapmaz. Microsoft derleyicilerinde en azından /O2, g++ ve clang++ derleyicilerinde -O2 seçeneği "inline açım (inline expansion) için uygulanmalıdır. Biz derleyicilerde optimizasyon seçeneklerini açsak bile derleyiciler "döngü içeren, iç içe çok fazla if deyiminin bulunduğu, uzun kodları" inline olarak açmak istemezler. Bazı kodlar (örneğin özyineleme içeren) yine inline olarak açılamamaktadır. Derleyiciler kodu inline olarak genellikle açmamışsa herhangi bir uyarı mesajı vermemektedir. Pekiyi biz fonksiyonun inline açılıp açılmadığını nasıl anlayabiliriz? Bunun en sağlam yolu kodun sembolik makine dili çıktısını incelemek ya da programı debugger altında çalıştırmaktır. Microsoft derleyicilerinde "assembly listing" oluşturmak için /FA seçeneği g++ ve clang++ derleyicilerinde ise -S seçeneği kullanılmaktadır. (Visual Studio IDE'sinde bu işlem) proje ayarlarından "Properties/C-C++/Output Files/Assembly Output" menüsüyle le yapılbilmektedir.) Örneğin: g++ -S -masm=intel sample.cpp -o seçeneği kullanılmamışsa üretilen dosya ".s" uzantılı olacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; inline int square(int a) { return a * a; } int main() { int result; int val; cout << "Bir değer giriniz:"; cin >> val; result = square(val); // result = val * val cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 10. Ders 18/09/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- inline fonksiyonlar kütüphanelere yerleştirilemezler (yerleştirilmeye çalışılmaları hata oluşturmayabilir ancak anlamlı değildir.) Çünkü her derleme işleminde inline açım yapabilmesi için derleyicinin inline fonksiyonun tanımlamasını görmesi gerekir. C++ standartlarına göre bir inline fonksiyon farklı kaynak dosyalardan kullanılacaksa her kaynak dosyada inline olarak ve tamamen özdeş biçimde tanımlanmalıdır. Örneğin aşağıdaki gibi bir inline fonksiyon olsun: inline int square(int a) { return a * a; } Biz bu fonksiyonu projemizi oluşturan "a.cpp", "b.cpp" ve "c.cpp" dosyalarından kullanmak isteyelim. İşte bu fonksiyonun -eğer kullanacaksak- bu kaynak dosyalarda inline olarak aynı biçimde tanımlanmış olması gerekir. Bir inline fonksiyonun projenin farklı kaynak dosyalarında inline olarak aynı biçimde kullanılabilmesinin en pratik yolu bir başlık dosyası oluşturmak, inline fonksiyonu o başlık dosyasının içerisine yerleştirmek ve dosyayı kullanılacak olan kaynak dosyalardan include etmektir. Yani inline fonksiyonlar tipik olarak projelerde başlık dosyalarının içerisine yerleştirilirler. inline fonksiyonlar başlık dosyalarına yerleştirildiğinde ve birden fazla modülde o başlık dosyası include edildiğinde inline fonksiyon da birden fazla modülde tanımlanmış olacaktır. Pekiyi inline fonksiyonların birden fazla modülde tanımlanmış olması link aşamasında bir problem oluşturmaz mı? Yukarıda da belirttiğimiz gibi standartlara göre bir fonksiyon bir modülde inline olarak tanımlanmışsa o fonksiyonu kullanan her modül fonksiyonu yine inline olarak aynı biçimde tanımlamak zorundadır. Bu "aynı biçimde tanımlama" tipik olarak inline fonksiyonun bir başlık dosyasına yerleştirilip modüllerden include edilmesi ile sağlanmaktadır. İşte derleyici eğer inline fonksiyonu inline olarak açarsa ve fonksiyonun adresi de herhangi bir biçimde kodda kullanılmadıysa bu durumda derleyici fonksiyonun derlenmiş halini hiç object dosyaya yazmayabilir. Ancak derleyici inline fonksiyon için inline açım yapmamışsa ya da fonksiyonun adresi kullanılmışsa mecburen fonksiyonu object dosyaya yazacaktır. Bu biçimde projeyi oluşturan tüm modüllerde bu fonksyion object dosyaya yazılacağına göre link aşamasında bir sorun oluşmayacak mıdır? İşte linker farklı object dosyalarda aynı inline fonksiyonun tanımlaması ile karşılaştığında onun herhangi bir object dosyadaki tek bir kopyasını çalıştırılabilen dosyaya yazmaktadır. Bunun için modern object dosya formatlarında "common" bölümler ya da attribute'lar bulunmaktadır. C++'ta inline fonksiyonlar (static inline yapılmadıktan sonra) yine "external linkage'a" sahiptirler. Biz bir inline fonksiyonun adresini alabiliriz. Bu durumda derleyici inline açım yapıyor olsa bile mecburen fonksiyon tanımlamasını yine object dosyaya yerleştirir. Benzer biçimde biz inline bir fonksiyonun farklı modüllerde adresini alsak bile bu adreslerin hepsi aynı olmak zorundadır. Bu durum static olmayan inline fonksiyonun "external linkage'a" sahip olduğunu açık biçimde göstermektedir. Bir inline fonksiyonun farklı modüllerde adresinin alınması durumunda bütün adreslerin aynı olması nasıl sağlanabilmektedir? İşte "linker" programları object dosyaları birleştirirken kodun bazı bölümlerinde düzeltmeler de yapmaktadır. Özetle programcı birkaç satırlık fonksiyonların hızlı çalışmasını istiyorsa onu inline yapabilir. inline fonksiyonlar tipik olarak başlık dosyalarına yerleştirilirler. Böylece projeyi oluşturan her modülde onlar aynı biçimde bulunurlar. Eğer inline açım yapılırsa çalıştırılabilir dosyada bu inline fonksiyon hiç bulunmayabilir. Eğer inline açım yapılmamışsa ya da inline fonksiyonun adresi kullanılmışsa bu fonksiyonların çalıştırılabilen dosyada tek kopyası bulundurulmaktadır. constexpr fonksiyonların otomatik olarak inline kabul edildiklerini anımsayınız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- inline anahtar sözcüğü fonksiyon prototiplerine yerleştirilebilir. Bu durumda fonksiyon tanımlanırken inline anahtar sözcüğü yazılabilir ya da yazıalmayabilir. Örneğin: inline void foo(); //... void foo() // burada inline yazılmamış olsa bile prototipte inline belirtildiği için fonksiyon inline durumdadır. { //... } Derleyici bir fonksiyon prototipini inline olarak gördükten sonra henüz fonksiyonun tanımlamasını görmeden fonksiyon çağrılırsa onu inline olarak açabilir ya da açmayabilir. Genel olarak derleyiciler bu durumda fonksiyonu inline açmamaktadır. Örneğin: inline void foo(); int main() { foo(); // derleyici inline açım yapamayabilir return 0; } void foo() { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 22) C++17 ile birlikte dile "inline değişkenler (inline variables)" denilen yeni bir özellik daha eklenmiştir. C++17'de global bir değişken (isim alanlarının içerisinde de olabilir) inline belirleyicisi ile inline yapılabilir. Örneğin: inline int g_x; Normal olarak proje ilişkin birden fazla kaynak dosyada aynı isimli global değişkenler varsa bu durum link aşamasında error oluşturmaktadır. Bu nedenle bir global değişken birden fazla kaynak dosyada kullanılacaksa kaynak dosyaların yalnızca birinde global tanımlama yapılıp global değişkenin kullanıldığı diğer kaynak dosyalarda extern bildirmi uygulanmalıdır. Ancak farklı kaynak dosyalarda aynı isimli inline global değişkenler bir soruna yol açmamaktadır. Tıpkı inline fonksiyonlarda olduğu gibi derleyiciler inline değişkenleri amaç dosyaya yazmakta linker ise bunların yalnızca tek bir kopyasını çalıştırılabilir dosyaya yerleştirmektedir. Bu kural sayesinde başlık dosyalarında global değişkenler tanımlanabilmektedir. Başlık dosyaları birden fazla kaynak dosyadan include edilebildiği için başlık dosyalarında global tanımlamaların yapılmaması gerektiğini anımsayınız. Ancak C++17'deki bu yenilikle başlık dosyalarında inline global değişkenler tanımlanabilmektedir. Örneğin ""project.hpp"" isimli bir başlık dosyası içerisinde aşağıdaki gibi bir inline global değişken tanımlanmış olsun: inline int g_x; Bu başlık dosyasının "a.cpp", "b.cpp" ve "c.cpp" dosyalarından include edildiğini varsayalım. Burada link aşamasında hiçbir sorun çıkmayacaktır. Eğer buradaki global değişken inline olmasaydı link aşamasında error oluşurdu. inline global değişkenlere ilkdeğer verilebilir. Ancak her kaynak dosyada yine aynı ilkdeğerin verilmesi gerekir. Bir global değişken bir kaynak dosyada inline olarak tanımlanmışsa onu kullanan tüm kaynak dosyalarda inline olarak tanımlanmalıdır. (Bu kural ihlal edilirse linker bunu tespit edemeyebilir.) inline değişkenler sınıfların static veri elemanı olarak da tanımlanabilmektedir. Bu konu ileride ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 23) C++11 ile birlikte auto anahtar sözcüğü otomatik tür belirleme için bir "tür belirleyicisi (type specifier)" haline getirilmiştir. Eskiden bu auto anahtar sözcüğü (hala C'de böyle) gerçek bir işlevi olmayan "storage class specifier" olarak bulundurulan bir anahtar sözcüktü. C++11 ile bu atıl durumdaki auto anahtar sözcüğüne yeni bir işlev yüklenmiştir. Bir değişken auto anahtar sözcüğü ile tanımlanıyorsa ona ilkdeğer verilmek zorundadır. Örneğin. auto int a; // geçersiz! Derleyici auto anahtar sözcüğü ile tanımlanmış bir nesnenin türünü ona verilen ilkdeğerden hareketle belirler. Ona verilen ilkdeğer hangi türdense auto yerine sanki o türün yazılmış olduğunu kabul eder. Örneğin: int foo() { //... } //... auto a = 123; // int a = 123; auto b = 12.3; // double b = 12.3; auto c = foo; // void (*c)() = foo; auto d = foo(); // int d = foo(); auto tür belirleyici ile birden fazla değişken tanımlanabilir. Ancak onlara verilen ilkdeğerin hepsinin aynı türden olması gerekir. Örneğin: auto a = 10, b = 20; // geçerli auto c = 10, d = 2.3; // geçersiz! auto tür belirleyicisi ile birden fazla değişkenin bildiriminin yapıldığı durumda onların herbirinin türünün verilen ilkdeğerlere göre tespit edildiğine ve bu türlerin aynı türler olması gerektiğine dikkat ediniz. Örneğin: auto a = 10, b = "ankara"; // geçersiz! Burada a için auto int türünü, b için const char * türünü temsil etmektedir. Bu türler farklı olduğundan bildirim geçersizdir. auto özellikle karmaşık kullanıcı tanımlı türlerin ifade edilmesi konusunda programcıya pratiklik sağlamaktadır. Örneğin: vector v; auto iter = v.begin(); // vector::iterator iter = v.begin(); Bildirimdeki auto belirleyicisinin bir tür belirttiğine dikkat ediniz. Örneğin: auto a = 10; Burada auto tür belirleyicisi int türünü temsil etmektedir. auto belirleyici ile yer belrleyicileri ve tür niteleyicileri (cv qualifers) ve karmaşık dekleratörler birlikte kullanılabilir. Bu durumda tür tespiti "şablon türlerin tespit edilmesi (template argument deduction)" sürecinde olduğu gibi yapılmaktadır. Örneğin: int a = 10; auto &r = a, b = 20; // auto = int const auto *s = "ali", ch = 'a'; // auto = char auto k = "veli" // auto = const char * Örneğin: auto a = "ankara"; // auto = const char * auto *b = "ankara"; // auto = const char const auto *c = "ankara"; // auto = char Yukarıda da belirttiğimiz gibi ilkdeğerden hareketle auto tür belirleyicisinin belirttiği türün tespit edilmesi şablonlardaki tür tespitindeki kurallarla yapılmaktadır. Şablonlar (templates) konusu çok sonra ele alacağımız bir konu olsa da burada yine auto belirleyicisinin şablon eşdeğeri üzerinde bir açıklama yapmak istiyoruz. Aşağıdaki gibi bir auto bildirimi yapılmış olsun: auto x = ifade; Buradaki tür tespiti aşağıdaki şablon parametrelerinin türlerinin tespit eedilmesindeki gibi yapılmaktadır: template void foo(T x) { //.... } foo(ifade); /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 11. Ders 20/09/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ auto belirleyici ile tür tespiti yapılırken dekleratöre göre kabaca üç farklı durum oluşmaktadır: 1) auto ile bildirilen değişkenin referans ya da gösterici olmaması durumu (yani dekleratörde &, && ya da * kullanılmaması durumu) 2) auto ile bildirilen değişkenin sol taraf değeri referansı ya da gösterici olması durumu (yani dekleratörde & ya da * kullanılması durumu) 3) auto ile bildirilen değişkenin sağ taraf değeri referansı olması durumu (yani dekleratörde && kullanılması durumu) auto ile bildirilen değişken bir referans ya da gösterici değilse ilkdeğer olarak verilen nesnenin const ya da volatile olması tür üzerinde etkili olmamaktadır. Örneğin: const int a = 10; auto b = a; // auto = int Burada b int türdendir, const int türünden değildir. Örneğin: const volatile int a = 10; auto b = a; // auto = int Burada b int türdendir, const volatile int türden değildir. auto ile bildirilen değişken bir sol taraf değeri referansı ya da bir gösterici ise ilkdeğer verilen ifadedeki const ve volatile niteleyicileri türde etkili olmaktadır. Örneğin: const int a = 10; auto &b = a; // auto = const int Burada b const int türünden bir referanstır. Bu durumda tür tespiti yapılırken üst düzey (top level) olmayan tür niteleyicilerinin de dikkate alındığına dikkat ediniz. Örneğin: const volatile int a = 10; auto &b = a; // auto = const volatile int Burada b const volatile int türünden referanstır. Örneğin: const int a = 10; auto *p = &a; // auto = cont int * Burada p gösterdiği yer const olan int türden bir göstericidir (yani const int * türündendir). Eğer auto ile bildirilen değişken bir sağ taraf değeri referansı ise bu özel bir durum belirtmektedir. Bu tür referanslara "forwarding reference" ya da "universial reference" denilmektedir. Bu konu şablon işlemlerinin anlatıldığı bölümde ayrıntılarıyla ele alınacaktır. Ancak burada yine de bazı açıklamalar yapmak istiyouz. Normal olarak bir sağ taraf değeri referansına bir sol taraf değeri bind edilememektedir. Ancak "forwarding reference" özel ve istisnai bir durumdur. auto belirleyicisi ile bildirilmiş olan bir sağ taraf değeri reeferansına bir sol taraf değeri ile ilkdeğer verilirse bildirilen değişken bir sol taraf değeri referansı, bir sağ taraf değeri ile ilkdeğer verilirse bildirilen değişken bir sağ taraf değeri referansı olur. Örneğin: int a = 10; auto &&b = a; Burada b bir sol taraf değeri referansıdır. Yani b int & türündendir. b referansının içerisine a'nın adresi yerleştirilmektedir. Örneğin: const int a = 10; auto &&b = a; Burda b const bir sol taraf değeri referansıdır. Yani b const int & türündendir. Ancak örneğin: auto &&b = 10; Burada b bir sağ taraf değeri referansıdır. Yani b int && türündendir. Forward reference konusunun ayrıntıları ileride diğer konular içerisinde ele alınacaktır. auto belirleyicisi bildirimde tek başına kullanılmak zorunda değildir. auto belirleyicisi ile birlikte yer ve tür belirleyicileri de kullanılabilir, dekleratörde başka atomlar da bulundurulabilir. Bu durumda auto belirleyicisinin hangi türü temsil ettiği yukarıda da belirttiğimiz gibi "şablon parametrelerinin türlerinin belirlenmesi (template argument deduction)" kuralına göre yapılmaktadır. Bu konunun ayrıntıları şablonların (templates) anlatıldığı bölümde ele alınacaktır. Örneğin: int a = 10; const auto &b = a, c = 10; // auto = int Burada b const int & türünden, c ise const int türündendir. Buradaki const belirleyicisinin dekleratöre değil türe ilişkin olduğuna dikkat ediniz. Örneğin: int a[10]; auto b = a; // auto = int * auto c = &a; // auto = int (*)[10] Burada b int türünden bir gösterici (yani b int * türünden), c de 10 elemanlı int türden bir dizi göstericisidir (yani c int (*)[10] türündendir). Örneğin: int a[10]; auto &b = a; // auto = int[10] Burada b int türden 10 elemanlı bir dizi referansıdır (yani b int (&r)[10] türündendir). Örneğin: int a[10]; auto (&b)[10] = a, c = 20; // auto = int Burada auto int türünü temsil etmektedir. Böylece b bir 10 elemanlı bir diziyi temsil eden referans (yani b int (&r)[10] türündendir) c de int türünden bir nesne belirtmektedir. auto belirleyicisi ile bildirilmiş olan değişkenlere küme parantezleri ile ilkdeğer verildiğinde özel bir durum söz konusu olmaktadır. Bu durumu birkaç madde ile açıklamak istiyoruz: a) C++17'ye kadar auto ile küme parantezleri ile ilkdeğer verilmesi durumunda değişkenin initializer_list türünden olduğu kabul ediliyordu. C++17'ye kadarki durumu aşağıdaki örnekle açıklamak istiyoruz. auto a{10}; // C++17'ye kadar a initilizer_list türünden auto b = {10}; // b initializer_list türünden auto c = {10, 20 , 30}; // c initializer_list türünden auto d{10, 20, 30}; // C++17 öncesi ve sonrasında ve C++geçersiz! auto e(10, 20, 30); // C++17 öncesi ve sonrasında ve C++geçersiz! b) C++17 ile birlikte küme parantezi ile tek elemanlı doğrudan (yani '=' olmadan) ilkdeğer vermelerde artık tür initializer_list olarak değil doğurdan T olarak belirlenmektedir. Örneğin: auto a{10}; // a C++17'ye kadar a initializer_list türünden ancak C++17 ve sonrasında int türden auto b = {10}; // b C++17 sonrasında ve öncesinde initializer_list türünden C++17'den sonraki durum için aşağıdaki örnekleri vermek istiyoruz: auto a{10}; // C++17'ye kadar a initializer_list türünden ancak C++17 ve sonrasında int türden auto b = {10}; // b C++17 sonrasında ve öncesinde initializer_list türünden auto c = {10, 20 , 30}; // b C++17 sonrasında ve öncesinde initializer_list türünden auto d{10, 20, 30}; // geçersiz! auto e(10, 20, 30); // geçersiz! Özetle bu konudaki kurallarda C++17 öncesi ve sonrasındaki tek farklılık "uniform initilizer syntax" ile küme parantezleri kullanıldığındaki durumdur. C++17 öncesiinde bildirilen değişken initializer_list kabeul edilirken C++17 ve sonrasında artık T kabul edilmektedir. C++14 ile birlikte fonksiyonun geri dönüş değeri için de auto kullanılabilir hale gelmiştir. Fonksiyonun geri dönüş değerinin türü yerine auto belirleyicisi kullanılırsa derleyici geri dönüş değerinin türünü return ifadesindeki türe bakarak belirler. Örneğin: auto foo() // auto = int { return 10; } Burada foo fonksiyonun geri dönüş değerinin int türden olduğu anlaşılmaktadır. Tabii fonksiyon içerisinde birden fazla kez return deyimi kullanılmışsa tür tespitinin yapılabilmesi için return ifadelerinin aynı türden olması gerekir. Örneğin: auto foo(int a) { if (a > 0) return 3.14; return 3; // geçersiz! } auto void tür tespiti için de kullanılabilir. Eğer fonksiyonda hiç return kullanılmamışsa ya da tüm return deyimlerinde bir ifade bulundurulmamışsa bu durumda fonksiyonun geri dönüş değerinin void olduğu tespit edilir. Örneğin: auto foo(int a) // auto = void { if (a < 0) return; cout << "foo" << endl; } C++20 ile birlikte fonksiyonun parametre değişkeninde de auto artık kullanılabilmektedir. Ancak bu kullanım fonksiyonun "gizli bir biçimde şablon olduğu" anlamına gelmektedir. Örneğin: auto foo(auto a) { cout << a << endl; } int main() { foo(10); // auto = int foo(20.2); // auto = double foo("ankara"); // auto = const char * return 0; } Burada aslında derleyici bir tane foo değil üç farklı foo oluşturmaktadır. Bu kavrama C++'ta "şablon (template)", Java ve C# gibi diğer dillerde "generic" denilmektedir. Şablon konusu ileride ayrı bir bölüm olarak ele alınacaktır. Buradaki foo fonksiyonunun fonksiyon şablonu olarak eşdeğeri şöyledir: template void foo(T a) { cout << a << endl; } C++11 ile eklenen auto tür belirleyicisinin ayrıntıları vardır. Ancak bu ayrıntılar üzerinde daha ileride ilgili konuların içerisinde duracağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 24) C'de yazılmış olan fonksiyonların ve global nesnelerin C++'ta kullanımını sağlamak için C++'ta extern "C" bağlantı biçimi (linkage) özelliği bulunmaktadır. Bu özellik ilk zamanlardan beri C++'ta vardır. Eğer siz bir C dosyasındaki bir fonksiyonu ya da global değişkeni C++'tan kullanacaksanız extern anahtar sözcüğünün yanına ""C" ibaresini yerleştirmelisiniz. Örneğin: extern "C" int g_g; extern "C" void foo(int a, int b); Pekiyi neden bu extern "C" bağlantı biçimine (linkage) gereksinim duyulmuştur? C++'ta farklı parametrik yapılara ilişkin fonksiyonlar bir arada bulunabilmektedir. Bu nedenle C++ derleyicileri fonksiyon isimlerini parametre türleriyle kombine ederek amaç dosyaya yazarlar. Yine C++ derleyicileri global değişken isimlereini de onların içinde bulunduğu isim alanını da kombine ederek onları amaç dosyaya yazmaktadır. Yani C++ derleyicilerinin isimleri dekore etmesi (buna "name decoration" ya da "name mangling" de denilmektedir.) C derleyicilerinden farklıdır. İşte C++ derleyicilerinin C'deki fonksiyonlar ve global değişkenler için C'deki gibi bir isim dekarasyonu yapması gerekir. Bu da extern "C" bağlantı biçimiyle sağlanmaktadır. Bu bağlantı biçimleri küme parantezleri içerisine alınabilir. Böylece çok sayıda fonksiyonun ve global değişkeninin bildirimi daha zahmetsiz yapılabilmektedir. Örneğin: extern "C" { void foo(void); void bar(int a, int b); int g_x; } extern "C" bloklarının içerisinde prototip ve değişken bildirimlerinin dışındaki öğeler de bulundurulabilir. Ancak "extern C" bağlantı özelliğinin bunlar üzerinde bir etkisi yoktur. Böylece elimizde zaten var olan C'de yazılmış bir başlık dosyası varsa include işlemini extern "C" bloğu içerisinde yapabilirsiniz. Örneğin: extern "C" { #include "myproject.h" } C derleyicileri arasında da isim dekorasyonu ve çağırma biçimleri (calling conventions) bakımından bir anlaşma yoktur. Yani biz bir C derleyicisinde yazıp kütüphane haline getirdiğimiz fonksiyonları da başka bir C derleyicisinde bu kütüphaneye referans ederek kullanamayız. Dolayısıyla C'de yazılmış ve derlenmiş olan fonksiyonların ve global değişkenlerin C++'ta kullanılabilmesi için bunların C++ derleyicisi ile uyumlu bir C derleyicisinde derlenmiş olması gerekir. gcc ve g++ derleyicileri kendiş aralarında uyumludur. Benzer biçimde Microsoft'un C derleyicisi ile C++ derleyicisi de kendi aralarında uyumludur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 25) C++11 ile birlikte C++'a "aralık tabanlı for döngüsü (range based loop)" ismiyle yeni bir for döngüsü eklenmiştir. Aslında bu tarz for döngüleri zaten Java gibi C# gibi dillerde "foreach" döngüleri ismiyle bulunuyordu. Aralık tabanlı for döngüsünün genel biçimi şöyledir: for ( : ) C++20 ile birlikte aralık tabanlı for döngülerine normal for döngülerindeki gibi bir "init" bölümü de eklenmiştir ve genel biçim şu hale gelmiştir: for ([ifade ya da bildirim>;] : ) Aralık tabanlı for döngüleri şöyle çalışmaktadır: Bir dizi ya da dolaşılabilir (iterable) bir nesnenin her elemanı için döngü bir kez yinelenir. Her yinelemede dizinin ya da dolaşılabilir nesnenin sıradaki elemanı döngü değişkenine yerleştirilir ve döngü deyimi çalıştırılır. Örneğin: int a[] {10, 20, 30, 40, 50}; for (int x : a) cout << x << endl; Burada sırasıyla dizinin her elemanı döngü değişkeni olan x'e yerleştirilmiş ve döngü deyimi çalıştırılmıştır. Aralık tabanlı for döngüsünde döngü değişkeninin faaliyet alanı yine for döngüsü ile sınırlıdır. Örneğin: for (int x : a) { //... } Biz x döngü değişkenini yalnızca döngünün içerisinde kullanabiliriz, döngünün dışında kullanamayız. Aralık tabanlı for döngüleri "dizilerle ya da iterator yoluyla dolaşılabilir" nesnelerle kullanılabilmektedir. Biz bu nesnelere anlatımı kolaylaştırmak için "dizilim" diyeceğiz. (C++ standartlarında böyle bir terim yoktur. C++ standartlarında buna "range expression" denilmektedir.) Standartlarda aralık tabanlı for döngülerinin eşdeğeri iteratör kullanılarak verilmiştir. Ancak bi henüz iteratör kavramını bilmiyoruz. Hatalı olsa da şimdilik aralık tabanlı for döngüsünün dizisel eşdeğerini aşağıdaki varsayabilirsiniz. Aralık tabanlı for döngüsü bir dizi ile aşağıdaki gibi kullanılmış olsun: T a[] = {1, 2, 3, 4, 5}; for (T x : a) { //... } Bunun yaklaşık eşdeğer (tam eşdeğeri değil) şöyledir: auto __begin = a auto __end = a + 5; for (; __begin != __end; ++__begin) { T x = *begin; //... } Bu yaklaşık eşdeğerlilikte dikkat edilmesi gereken noktalardan biri "döngü değişkeninin sanki her yinelemede yeniden yaratılıyormuş gibi" olmasıdır. Diğer dikkat edilmesi gereken nokta da dizilimin sıradaki elemanının döngü değişkenine "ilkdeğer veriliyormuş gibi" yerleştirilmesidir. Aralık tabanlı for döngülerinde dizilimin her elemanı döngü değişkenine ilkdeğer veriliyormuş gibi atandığı için döngü değişkeni bir referans da olabilmektedir. Örneğin: int a[] {10, 20, 30, 40, 50}; for (int &r : a) r = 2 * r; Burada a dizisinin her bir elemanı sanki referansa ilkdeğer veriliyormuş gibi düşünülmelidir. Dolayısıyla döngü deyiminde r referansına değer atanması aslında dizinin ilgili elemanına değer atanması anlamına gelmektedir. O halde örneğin: T a[] = {1, 2, 3, 4, 5}; for (T &r : a) { //... } işleminin eşdeğeri şöyledir: auto __begin = a auto __end = a + 5; for (; __begin != __end; ++__begin) { T &r = *begin; //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a[]{10, 20, 30, 40, 50}; for (int x : a) cout << x << " "; cout << endl; for (int &r : a) r = 2 * r; for (int x : a) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aralık tabanlı for döngüsünde herhangi bir türden dizi kullanabiliriz. Tabii normal olarak döngü değişkeninin de o dizi türüne uygun olması gerekir. Örneğin: const char *names[] = {"ali", "veli", "selami", "ayse", "fatma"}; for (const char *name : names) { //... } Burada dizinin her elemanı const char * türündendir ve bir ismin adresini tutmaktadır. Döngü her yinelendiğinde name isimli gösterici sırasıyla dizideki isimleri gösterecektir. String'ler de const char türünden dizi belirttiğine göre biz bir string'teki karakterleri de aralık tabanlı for döngüleri ile dolaşabiliriz. Örneğin: for (char ch : "ankara") { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { const char *names[] = {"ali", "veli", "selami", "ayse", "fatma"}; for (const char *name : names) cout << name << " "; cout << endl; for (char ch : "ankara") cout << ch << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aralık tabanlı for döngülerinde ':' atomunun sağındaki dizilim belirten ifade (range expression) bir gösterici olamaz. Dizi isimlerinin ifadelere sokulduğunda dizinin ilk elemanını gösteren adreslere dönüştürüldüğünü biliyoruz. Ancak aralık tabanlı for döngülerinde böyle bir dönüştürme yapılmamaktadır. Dolayısıyla aralık tabanlı for döngülerinde ':' atomunun sağındaki ifadenin diziyi gösteren bir gösterici değil bizzat dizi nesnesinin kendisi olması gerekir. Örneğin: int a[] {1, 2, 3, 4, 5}; for (int x : a) { // geçerli //... } Ancak örneğin: int a[] {1, 2, 3, 4, 5}, *pi = a; for (int x : pi) { // geçersiz! //... } Dolayısıyla ':' atomunun sağına biz bir string yerleştirdiğimizde aslında oaray const char türünden bir dizi nesnesini yerleştirmiş gibi olmaktayız. Örneğin: for (char ch : "ankara") { //... } Bu döngünün eşdğerinin aşağıdaki gibi olduğu varsayılmalıdır: const char __temp[] = "ankara"; for (char ch : __temp) { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirtitğimiz gibi aralık tabanlı for döngüleri iteratör desteği olan dolaşılabilir (iterable) sınıf nesneleri ile de kullanılabilmektedir. Örneğin C++'ın standart kütüphanesindeki vector gibi list gibi sınıflar dolaşılabilir olduğu için aralık tabanlı for döngülerinde kullanılabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{ 10, 20, 30, 40, 50 }; for (int x : v) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz yukarıda auto belirleyicisi ile küme parantezi kullanılarak ildeğer vermelerde (C++17'deki küçük değişikliği anımsayınız) aslında initializer_list türünden bir sınıf nesnesinin yaratıldığını söylemiştik. İşte initializer_list sınıfı da iteratör yoluyla dolaşılabildiği için aralık tabanlı for döngülerinde kullanılabilmektedir. Örneğin: auto a = {1, 2, 3, 4, 5}; // a initializer_list türünden for (int x : a) { // geçerli //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { auto a = {1, 2, 3, 4, 5}; // a initializer_list türünden for (int x : a) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aralık tabanlı for döngülerinde döngü değişkeninin normal olarak ilgili dizilimin eleman türünden olması gerekir. Örneğin biz int bir diziyi aralık tabanlı for döngüsüyle dolaşacaksak döngü değişkeninin int türdne olmasını bekleriz. Pekiyi o zaman döngü değişkeninin türünü belirtmeye ne gerek vardır? İşte aslında mademki döngü değişkenine dizinin elemanları ilkdeğer veriliyormuş gibi yerleştirilmektedir o halde aslında bu türler farklı olabilir. Örneğin: double a[] {3.14, 2.718, 12.34, 5.2, 7.8}; for (int x : a) { //.... } Tabii burada double türünden int türüne dönüştürme sırasında bilgi kaybı söz konusu olacaktır. İşte bazı konular dikkate alındığında döngü değişkeninin farklı türden olabilmesi dah esnek bir kullanım oluşturmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { double a[]{3.14, 2.718, 12.34, 5.2, 7.8}; for (int x : a) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 12. Ders 25/09/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aralık tabanlı for döngülerinde döngü değişkeninin türü yerine "auto" tür belirleyicisi kullanılabilir . Bu durumda eğer dolaşılan nesne bir dizi ise dizinin türüne dayalı tür tespiti yapılır. Eğer dolaşılan nesne iteratör yoluyla dolaşılabilir bir nesne ise burada iteratörün türüne göre tür tespiti yapılmaktadır. (Bu tür durumlarda ilgili sınıfının * operatör fonksiyonu çağrıldığı için bu operatör fonksiyonunun geri dönüş değerine dayalı olarak tür tespiti yapılır.) Örneğin: int a[] = {1, 2, 3, 4, 5}; //... for (auto x : a) { // auto = int //... } Burada dizi int türden olduğu için dizi elemanları da int türdendir. Dolayısıyla auto belirleyicisi int türünü olarak belirlenecektir. Örneğin: const char *names[] = {"ali", "veli", "selami", "ayse", "fatma"}; //... for (auto x : names) { // auto = const char * //... } Burada names dizisi const char * türünden olduğu için auto da cont char * türü olarak belirlenecektir. Örneğin: vector v{1, 2, 3, 4, 5}; //... for (auto x : v) { auto = int //... } Her ne kadar vector sınıfını görmemiş olsak da burada bu vector nesnesi int türden değerler tuttuğu için auto da int olarak belirlenecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { double a[]{3.14, 2.718, 12.34, 5.2, 7.8}; vector v{1, 2, 3, 4, 5}; const char *names[] = {"ali", "veli", "selami", "ayse", "fatma"}; for (auto x : a) // auto = double cout << x << " "; cout << endl; for (auto &x : a) // auto = double cout << x << " "; cout << endl; for (auto x : v) // auto = int cout << x << " "; cout << endl; for (auto x : names) // auto = const char * cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aralık tabanlı for döngülerinde dizilim için doğrudan küme parantezleri içerisinde bir değer listesi kullanılabilir. Bu durumda bu küme parantezli liste derleyici tarafından otomatik olarak initializer_list türüne dönüştürülmektedir. Ancak bu dönüştürmenin yapılabilmesi için küme parantezleri içerisindeki değerlerin aynı türden olması gerekmektedir. initilizer_list türünün iteratör yoluyla dolaşılabilen bir tür olduğunu anımsayınız. initializer_list sınıfı kursumuzun ilerleyen zamanlarında ele alınacaktır. Örneğin: for (auto x : {1, 2, 3, 4, 5}) { // auto = int //... } Burada auto belirleyicisi int olarak tespit edilecektir. Tabii biz aslında auto yerine doğrudan int türünü de kullanabilirdik. Örneğin: for (int x : {1, 2, 3, 4, 5}) { // geçerli //... } Bu biçimdeki for döngülerinde küme parantezi içerisindeki elemanların aynı türden olması gerekmektedir. Örneğin: for (auto x : {1, 2, 3.5, 4, 5}) { // geçersiz! //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++20 ve sonrasında aralık tabanlı for döngülerinde "isteğe bağlı olarak (optional)" bir "init" kısmı bulundurulabilmektedir. Eğer "init" kısım bulundurulacaksa bunu ";" atomu izlemelidir. "init" kısmındaki ifade döngüye girişlte yalnızca bir kez çalıştırılmaktadır. Örneğin: int total; //... for (total = 0; auto x : {1, 2, 3, 4, 5}) total += x; cout << total << endl; Bu "init" ifadesi boş da bırakılabilir. Ancak bunun bir anlamı yoktur. Örneğin: for (; auto x : a) { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int total; for (total = 0; auto x : {1, 2, 3, 4, 5}) total += x; cout << total << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 26) C'de tek tırnak içerisindeki karakterler int türden kabul edilmektedir. Örneğin C'de 'a' karakter sabiti her ne kadar karakter sabiti ise de int türdendir. Ancak C++'ta içerisinde yalnızca tek bir karakter bulunan karakter sabitleri int türden değil char türdendir. Örneğin C++'ta 'a' sabiti char türdendir. Dolayısıyla örneğin sizeof('a') C'de int türünün byte uzunluğunu verirken C++'ta 1 verecektir. Tabii hem C'de hem de C++'ta aslında tek tırnak içerisine int türünün byte uzunluğu kadar karakter girilebilir. C++'ta tek tırnak içerisine birden fazla karakter girilirse böyle sabitler C'de olduğu gibi int türden kabul edilmektedir. Yani örneğin 'ab' sabiti hem C'de hem de C++'ta int türdendir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 27) C++'ta yapılar, birlikler, sınıflar ve enum türleri için tür bilgisi ifade edilirken "struct", "union", "class" ve "enum" anahtar sözcüklerinin kullanımı zorunlu değildir. Halbuki C'de yapı, birlik ve enum türleri yalnızca isimle belirtilmemektedir. Örneğin C'de date isimli bir yapı bildirmiş olalım. Biz bu yapı türünden nesne tanımlarken tür ismi olarak "struct date" kullanmak zorundayız. Halbuki C++'ta yalnızca "date" ismi yeterli olmaktadır. Örneğin: struct complex { double real; double imag; }; complex z; // C'de geçersiz, C++'ta geçerli C'de yukarıdaki yapı bildirimi ile oluşturulan türün ismi "complex" değildir, "struct complex" biçimindedir. Halbuki C++'ta bu yapıya ilişkin tür yalnızca "complex" ismiyle kullanılabilmektedir. Tabii C++'ta istersek tür isminin önüne struct, class, union, enum anahtar sözcüklerini getirebiliriz. Örneğin: struct complex z; // Hem C'de hem de C++'ta geçerli Tabii bu tür isimleri tür isminin kullanılabildiği yerde bu biçimde kullanılabilir. Örneğin: date *pd; // geçerli void *pv; //... pd = (date *)pv; // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; struct date { int day, month, year; }; int main() { date d = {10, 12, 2009}; // C'de geçersiz, C++'ta geçerli struct date k = {10, 12, 2009}; // Hem C'de hem de C++'ta geçerli cout << d.day << '/' << d.month << '/' << d.year << endl; cout << k.day << '/' << k.month << '/' << k.year << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 28) C'de enum türleri ile int türü arasında bir farklılık yoktur. Yani C'de bir enum türünden nesne sanki int türden bir nesneymiş gibi derleyiciler tarafından ele alınmaktadır. Benzer biçimde C'de enum sabitleri de (enumators) sanki int türden sabitlermiş gibi ele alınmaktadır. Dolayıısyla biz C'de enum türünden bir nesneye int türünden bir nesneyeatayabileceğimiz her şeyi atayabiliriz. Benzer biçimde C'de enum sabitleri ya da enum türünden nesneler sanki int türündenmiş gibi işlem öncesi tür dönüştürmesine sokulmaktadır. Örneğin aşağıdaki gibi bir C kodu olsun: enum Direction { Up, Right, Down, Left }; //... enum Direction d; int val; d = 2; // geçerli val = d + 1; // geçerli val = Down + 1; // geçerli Ancak C++'ta her enum türü bağımsız ve farklı bir türdür. Nümerik türlerden enum türlerine otomatik dönüştürme yoktur. Fakat bunun tersi olan enum türlerinden nümerik türlere otomatik dönüştürme vardır. Örneğin: enum Direction { Up, Right, Down, Left }; //... Direction d; int val; d = Right; val = d + 1; cout << val << endl; // 2 val = Right + Down; cout << val << endl; // 3 d = 2; // geçersiz! d = (Direction)2; // geçerli Görüldüğü gibi C'de biz nümerik türden bir değeri doğrudan enum türünden bir değişkene atayabilmekteyiz. Fakat C++'ta bu durum geçerli değildir. Ancak C++'ta biz enun türünden bir değeri nümerik türlerle işleme sokabiliriz. Bu durumda enum türü sanki bir tamsayı türüymüş gibi işleme sokulmaktadır. Yine C'de iki farklı enum türünü birbirlerine atayabiliriz. Çünkü C'de enum türleri tamamen int türü gibi ele alınmaktadır. Ancak C++'ta iki farklı enum türü arasında otomatik dönüştürme yoktur. Örneğin: enum Direction { Up, Right, Down, Left }; enum Color { Red, Green, Blue }; Color c; c = Right; // geçersiz! Direction türünden Color türüne otomatik dönüştürme yok! C'de enum sabitleri de (enumerators) int türden kabul edilmektedir. Halbuki C++'ta enum türleri farklı bir tür belirttiği için enum sabitleri de ilgili enum türündendir. Örneğin: enum Day { Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday }; Burada örneğin Sunday, Thursay enum sabitleri Day isimli enum türündendir. Örneğin: Day d; d = Thursday; // geçerli, d de Thursday de Day türünden Tabii yukarıdaki örnekte de görüldüğü gibi tür dönüştürme operatöryle nümerik türler enum türlerine, enum türleri başka enum türlerine dönüştürülebilir. Örneğin: enum Direction { Up, Right, Down, Left }; enum Color { Red, Green, Blue }; //... Direction d; d = Green; // geçersiz! d = (Direction)d; // geçerli fakat anlamlı olmayabilir C++'ta her enum türünün "ilişkin olduğu bir tamsayı türü (underlying integer type)" vardır. enum türünün ilişkin olduğu tamsayı türü aslında o enum türünün gerçekte derleyici tarafından ele alındığı tamsayı türüdür. enum türünün ilişkin olduğu tamsayı türünün bizim için iki önemi vardır: 1) İlgili enum türünden bir nesne tanımladığımızda o nesne o enum türünün ilişkin olduğu tamsayı türü kadar yer kaplamaktadır. 2) enum türünden bir değer artimetik işlemlere sokulduğunda ya da atama işlemine sokulduğunda derleyici sanki işleme giren değerin o enum türünün ilişkin olduğu tamsayı türüymüş gibi olduğunu kabul etmektedir. Özetle enum türü aslında C++ derleyicisi için arka planda bir tamsayı türü imiş gibi işleme sokulmaktadır. İşte buna enum türünün ilişkin olduğu tamsayı türü denilmektedir. Pekiyi enum türünün ilişkin olduğu tamsayı türü nasıl belirlenmektedir? İşte C++11 ile birlikte enum türünün ilişkin olduğu tamsayı türü açıkça ": " sentaksıyla belirtilebilmektedir. Örneğin: enum Direction : int { Up, Right, Down, Left }; Burada Direction isimli enum türünün ilişkin tamsayı türü açıkça "int" olarak belirtilmiştir. Tabii ": " sentaksındaki türün tamsayı türlerinden biri olması gerekir. (C++ standartlarında buna enum türünün ilişkin olduğu tamsayı türünün "fixed" yapılması de denilmektedir.) Tabii ": :: ifadesiyle erişilmektedir. Örneğin: Direction d; d = Direction::Up; // geçerli d = Up; // geçersiz! Burada enum sabitlerine erişmekte kullanılan :: operatörüne "çözünürlük operatörü (scopre resolution operator)" denilmektedir. Atomlar arasında istenildiği kadar boşluk karakteri bırakılabileceğine göre aşağıdaki yazım da geçerlidir: d = Direction :: Up Ancak programcılar genel olarak çözünürlük operatörünün iki tarafında boşluk kullanmazlar. Çözünürlük operatörü ile ileride başka onularda da karşılaşacağız. Örneğin: enum class Direction { Up, Right, Down, Left }; enum class Style { Up, Down, Hatch, Cross }; Bu iki faaliyet alanlı enum türünün birlikte bulunmasının bir sakıncası yoktur. Çünkü bunların enum sabitlerine artık enum ismi belirtilerek erişilmektedir. Faaliyet alanlı enum türünün normal enum türünden bir farklılığı da otomatik dönüştürmeye izin vermemesidir. Biz faaliyet alanlı bir enum türünü bir tamsayı türüne ya da nümerik türe atayamayız. Faaliyet alanlı bir enum türünü nümerik türlerle işleme sokamayız. Örneğin: enum class Direction { Up, Right, Down, Left }; //... Direction d = Direction::Down; int val; val = d; // Normal enum türleri için geçerli ama faaliyet alanlı enum türleri için geçersiz! val = d + 1; // Normal enum türleri için geçerli ama faaliyet alanlı enum türleri için geçersiz! d = 1; // Hem normal enum türleri için hem de faaliyet alanlı enum türleri için geçersiz! Görüldüğü gibi faaliyet alanlı enum türleri tür dönüştürmesi bakımından daha katıdır. Tabii faaliyet alanlı enum türlerine ilişkin değerler de tür dönüştürme operatörleriyle nümerik türlere dönüştürülebilir. Nümerik türler de yine tür dönüştürme operatörleriyle faaliyet alanlı enum türlerine dönüştürülebilir. Örneğin: enum class Direction { Up, Right, Down, Left }; //... Direction d = Direction::Down; int val; val = (int)d; // geçerli cout << val << endl; d = (Direction)(val + 1); // geçerli Faaliyet alanlı enum türlerinin de ilişkin olduğu tamsayı türü vardır. Bu tamsayı türü yine normal enum türlerinde olduğu gibi ": " sentaksıyla belirtilmektedir. Örneğin: enum class Color : unsigned char { Red, Green, Blue }; Ancak faaliyet alanlı enum türlerinde ilişkin olunan tamsayı türü belirtilmezse default durum "int" kabul edilmektedir. Örneğin: enum class Color { // Color enum türünün ilişkin olduğu tamsayı türü int Red, Green, Blue }; Örneğin: enum class Color { Red, Green, Blue = 3000000000 // dikkat! }; Burada Blue isimli enumn sabiti ilgili sistemdeki int türünün sınırlarını aşıyorsa bu durumda kod geçersiz hale gelecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; enum Direction { Up, Right, Down, Left }; void move(Direction d) { switch (d) { case Direction::Up: cout << "moving up...\n"; break; case Direction::Right: cout << "moving right...\n"; break; case Direction::Down: cout << "moving down...\n"; break; case Direction::Left: cout << "moving left...\n"; break; } } int main() { move(Direction::Up); move(Direction::Down); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 13. Ders 27/09/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 29) C++14 ile birlikte C++'a tamsayı türlerine ilişkin sabitlerin 2'lik sistemde ifade edilebilmesi özelliği de eklenmiştir. Anımsanacağı gibi C'de tamsayı türlerine ilişkin sabitler 10'luk, 16'lık ve 8'lik sistemlerde ifade edilebiliyordu. C++14 ile birlikte 2'lik sistemde de ifade olanağı dile eklenmiştir. Tamsayıların 2'lik sistemde ifadesi için 0b ya da 0B önekleri kullanılmaktadır. Örneğin: c = 0b10101100; Aynı özellik C'ye de C23 ile de eklenmek istenmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { unsigned char a = 0B1000011; printf("%02X\n", a); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 30) C++'a C++14 ile birlikte sabitlerdeki basamakların birbirinden görsel olarak ayırt edilebilmesi için basamak ayıracı özelliği eklenmiştir. Büyük sayıların ayıraçsız bir biçimde oluşturulması sayıların okunabilirliğini zorlaştırmaktadır. Örneğin: a = 1000000000; Burada a'ya atanan değerin kaç olduğunu anlayabilmek için özel bir dikkatin sarf edilmesi gerekir. C++14 ile birlikte artık ' (tek tırnak) basamakları görsel biçimde ayırmak için kullanılabilmektedir. Örneğin: a = 1'000'000'000; Tabii burada tek tırnak karakterlerinin üçlü basamakları ayırmak için kullanılması zorunlu değildir. Örneğin: a = 1'0'0'0'0'0'0'0'0'0; // geçerli Ancak tek tırnak sayının başında ya da sonunda olamaz. Örneğin: a = '10'000; // geçersiz! a = 10000'; // geçersiz! Sabit içerisinde yan yana birden fazla tek tırnak ayıracı da kullanılamamaktadır. Örneğin: a = 1''000'000'000; // geçersiz! Aslında basamak ayırçları pek çok dilde eskiden beri bulunuyordu, bazılarına ise onların çeşitli versiyonlarında eklendi. Örneğin Python, C# ve Java'da basamak ayıracı olarak _ (alt tire) kullanılmaktadır. Basamak ayıraçları yalnızca 10'luk sistemde belirtilen sabitlerde değil diğer sistemlerde belirtilen sabitlerde de kullanılabilmektedir. Örneğin: a = 0x12'34'56'78; b = 0'12'456; c = 0b1000'1010 Tek tırnak ayıracı 0x ya da 0X, 0b ya da 0B'den hemen sonra getirilemez. Ancak octal sistemde 0'dan hemen sonra getirilebilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 31) C++'ta C'de olmayan uzmanlaşmış tür dönüştürme operatörleri vardır. C'nin "(tür) operand" biçimindeki tür dönüştürme operatörü C++'ta aslında aynı biçimde kullanılabilmektedir. Ancak C'nin tür dönüştürme operatörü değişik dönüştürmeleri bir arada yapabildiği için hatalara zaemin hazırlayabilmektedir. İşte C++'ta farklı tarzda tür dönüştürmeleri için farklı operatörler bulundurulmuştur. C++'ta C tarzı tür dönüştürmesi yerine bu C++'a özel operatörlerin kullanılması iyi bir ekniktir. C++'ın özel tür dönüştürme operatörleri şunlardır: static_cast const_cast reinterpret_cast dynamic_cast Bu operatörler şablon senktası biçimde kullanılır. Yani dönüştürülecek tür açısal parantezler içerisinde belirtilir. Dönüştürülecek ifade de paranteze alınır. Kullanımın genel biçimi şöyledir: xxx_cast(ifade) Örneğin: val = static_cast(color); C'nin C++'ta da kullanılabilen tür dönüştürme operatörü adeta static_cast, const_cast, reinterpret_cast operatörlerinin birleşimi gibidir. Ancak bu konuda bazı ayrıntılar da vardır. dynamic_cast sınıflar konusuyla ilgilidir. Bu nedenle bu cast operatörü çok ileride ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- static_cast "standart dönüştürmeler (standard conversion)" için kullanılmaktadır. Standart dönüştürme demekle aritmetik türler arası dönüştürmeler, enum türleri ile nümerik türler arasındaki dönüştürmeleri, void * dönüştürmeleri, türemiş sınıftan taban sınıfa adres dönüştürmeleri kastedilmektedir. Örneğin double bir değeri int türüne dönüştürmek isteyelim: double d = 12.34; int i; i = d; // geçerli i = static_cast(d); // geçerli Görüldüğü gibi otomatik dönüştürmesinin yapılabildiği her yerde static_cast operatörü de kullanılabilmektedir. Örneğin: int a, b; double c; //... a = 10; b = 4; c = static_cast(a) / b; static_cast operatörü ile enum türleri nümerik türlere, nümerik türler de enum türlrine dönüştürülebilir. Örneğin: enum class Color { Red, Green, Blue }; //... Color c; int val; c = Color::Green; val = static_cast(c); void bir adresin türünün belirli bir türden adrese dönüştürülmesi, türü belirli bir adresin de void adrese dönüştürülmesi static_cast ile yapılabilmektedir. Örneğin: pi = static_cast(malloc(10 * sizeof(int))); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void *foo() { return NULL; } enum Color { Red, Green, Blue }; int main() { double a = 10.2; int val; int *pi; Color color; val = static_cast(a); // Zaten operatöre gerek yok pi = static_cast(foo()); // void *'dan diğer adres türlerine color = static_cast(2); // geçerli, static_cast bu dönüştürmeyi yapar return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- const_cast adres dönüştürmelerinde const'luğu atmak için (const away cast) kullanılmaktadır. Örneğin: int a = 10; const int *pci = &a; int *pi; pi = const_cast(pci); const_cast ile aynı türdeki adreslerde const'luk atılabilir. Örneğin: int a = 10; const int *pci = &a; char *pc; pc = const_cast(pci); // geçerli değil! yalnızca canst'luk atılmıyor const_cast adreslerdeki const'luğu atmaktadır. Dolayısıyla const_cast operatöründeki hedef türün bir adres türü olması gerekir. Örneğin: const int a = 10; int b; b = const_cast(a); // geçersiz! const_cast adresler için kullanılır const_cast const olmayan bir adresin const adrese dönüştürülmesinde de kullanılabilir. Ancak bu dönüştürme zaten otomatik yapıldığı için bu operatörün kullanılmasına gerek yoktur. Yukarıda da belirttiğimiz gibi otomatik dönüştürmelerin hepsi zaten static_cast operatörü ile de yapılabilmektedir. Yani biz örneğin int * türünü const int * türüne static_cast ile de dönüştürebiliriz. const_cast operatörünün asıl yaptığı şey adreslerdeki const'luğu atmaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a = 10; const int *pci = &a; int *pi; pi = const_cast(pci); // geçerli *pi = 20; cout << a << endl; pi = (int *)pci; // geçerli fakat kötü teknik! *pi = 30; cout << a << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- reinterpret_cast farklı türlerdeki adresler arasında ve adres türleriyle aritmetik türler arasında tür dönüştürmeleri için kullanılmaktadır. Örneğin: int a = 12345; unsigned char *pc; pc = reinterpret_cast(&a); Burada int * türü unsigned int * türüne dönüştürülmüştür. Örneğin: int a = 12345; unsigned long addr; addr = reinterpret_cast(&a); Burada bir adresin sayısal bileşeni bir tamsayı türüne reinterpret_cast operatörü ile dönüştürülerek atanmıştır. Örneğin: int *pi; pi = reinterpret_cast(0x123456); Burada 0x12345 int türden sabit bir adresin sayısal bileşeni olarak int türden göstericiye yerleştirilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a[] = { 1, 2, 3, 4, 5 }; unsigned char *pc; unsigned long addr; pc = a; // error! pc = (unsigned char *)a; // geçerli ama C++'ta kötü teknik pc = reinterpret_cast(a); // iyi teknik addr = (unsigned long)a; // geçerli ama C++'ta kötü teknik addr = reinterpret_cast(a); // geçerli ve iyi teknik return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Hem adresteki const'luğu atmak hem de adresi farklı türe dönüştürmek için iki operatör birlikte kullanılmalıdır. Örneğin: const int *pi; char *pc; //... pc = reinterpret_cast(const_cast(pi)); ya da şöyle de yapılabilirdik: pc = const_cast(reinterpret_cast(pi)); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a = 10; const int *pci; unsigned char *pc; pci = &a; pc = const_cast(reinterpret_cast(pci)); for (size_t i = 0; i < sizeof(a); ++i) printf("%02X ", pc[i]); printf("\n"); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- dynamic_cast operatörü sınıflarla ilgili işlem yapmaktadır. Bu operatör çok ileride ele elınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 32) C++'ta C'deki tür dönüştürme operatörünün sentaks olarak ters biçimini andıran "fonksiyonel tür dönüştürme operatörü" de bulunmaktadır. Fonksiyonel tür dönüştürme operatörü hem sınıflar konusunda uyum oluşturmak için hem de şablon mekanizmasını desteklemek için bulundurulmuştur. Bu operatör C++'ın ilk zamanlarından beri varlığını sürdürmektedir. Ancak C++11 ile "uniform initializer syntax" dile eklenince bunun küme parantezli biçimi de oluşturulmuştur. Fonksiyonel tür dönüştürme operatörü yukarıda da belirttiğimiz gibi adeta klasik tür dönüştürme operatörünün sentaks bakımından tersidir. Yani T bir tür belirtmek üzere C'nin klasik tür dönüştürme operatörü (T)ifade biçimindedir. Fonksiyonel tür dönüştürme operatörü ise T(ifade) biçimindedir. Burada dönüştürülecek türün değil ifadenin paranteze alındığına dikkat ediniz. T(ifade) bir fonksiyon çağrısına benzediği için buna "fonksiyonel tür dönüştürme operatörü" denilmiştir. Örneğin: int a = 10; int b = 4; double c; //... c = double(a) / b; Fonksiyonel tür dönüştürme operatörü işlev olarak C'nin tür dönüştürme operatörü gibidir. Yani yine static_cast, const_cast ve reinterpret_cast operatörlerinin bir birleşimi gibidir. Başka bir deyişle C tarzı tür dönüştürme operatörü ile yapılan hger şey fonksiyonel tür dönüştürmesi ile de yapılabilmektedir. Fonksiyonel tarzda tür dönüştürmesinin önemli bir sentaks kısıtı vardır. Bu dönüştürmede dönüştürülecek türün tek atomdan oluşması gerekmektedir. Örneğin: double a = 12.345; unsigned long b; b = unsigned long(a); // geçersiz! unsigned long iki atomdan oluşuyor Burada unsigned long türü iki atomdan oluştuğu için kullanım geçersizdir. Tabii unsigned long türünü typedef edip tek atomla ifade edersek bu durumda dönüştürme geçeli olur: double a = 12.345; unsigned long b; typedef unsigned long ulong; b = ulong(a); Buradaki tek atom koşuluna dikkat ediniz: int a = 10; char *pc; pc = char *(&a); // geçersiz! char * tek atomdam oluşmuyor Yine typedef işlemi ile bunu geçerli hale getirebiliriz: int a = 10; char *pc; typedef char *char_ptr; pc = char_ptr(&a); C++11 ile birlikte "uniform initializer syntax" dile eklenince fonksiyonel tarza tür dönüştümesinin küme parantezli versiyonu da oluşturuldu. T bir tür belirtmek üzere T{ifade} sentaksı da fonksiyonel tarzda tür dönüştürmesi gibi kullanılabilmektedir. Ancak burada "uniform initializer syntax" dolayısıyla daraltıcı dönüştürmelere izin verilmemektedir. Örneğin: int a = 10; int b = 4; double c; c = double{a} / b; // geçersiz! int -> double dönüştürmesi daraltıcı bir dönüştürme --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int a = 10; int b = 4; double c; c = double(a) / b; cout << c << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 14. Ders 02/10/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 33) C'de fonksiyonlar ve diziler bütünsel olarak birer nesne kabul edilmektedir. Ancak fonksiyon isimleri bir ifadede kullanıldığında derleyici tarafından otomatik olarak fonksiyonun başlangıç adresine dönüştürülmektedir. Aynı durum C'de diziler için de söz konusudur. Bir dizi ismini biz bir ifadede kullandığımızda derleyici bu dizi nesnesini otomatik olarak dizinin başlangıç adresine (yani ilk elemanın adresine) dönüştürmektedir. O halde C'de bir fonksiyonun ismi ifade içerisinde kullanıldığında onun başlangıç adresini belirtmektedir. Pekiyi fonksiyon çağırma operatörünün operandı ne olmalıdır? C standartlarına göre fonksiyon çağırma operatörünün operandı bir fonksiyon adresi olmalıdır. Dolayısıyla foo bir fonksiyon nesnesi olmak üzere foo(...) ifadesi aslında foo adresinden başlayan fonksiyonun çağrılması anlamına gelmektedir. Öte yandan foo fonksiyon nesnesi ifade kullanıldığında zaten fonksiyon adresine dönüştürüldüğü için *foo ifadesi de yeniden fonksiyon nesnenin kendisi anlamına gelir. O halde fonksiyon nesnesi ifadede kullanıldığında fonksiyon adresine dönüştürüleceğine göre (*foo)(...) geçerli olmaktadır. Aynı durum fonksiyon göstericilerinde de böyledir. pf bir fonksiyon göstericisi olmak üzere bu göstericinin gösterdiği yerdeki fonksiyon pf(...) ifadesiyle ya da (*pf)(...) ifadesiyle de çağrılabilmektedir. Ayrıca C'de gerekmiyor olsa da bir fonksiyon nesnesinin adresi & operatöryle alınabilmektedir. Örneğin pf bir fonksiyon göstericisi foo da onunla uyumlu bir fonksiyon olmak üzere aşağıdaki iki ifade eşdeğerdir: pf = foo; pf = &foo; C++ tasarım olarak C'yi kapsayacak biçimde oluşturulmuştur. Tabii bu kapsama mutlak değildir. Ancak C++ tasarlanırken özellikle ilk yıllarda C uyumunun korunmasına çalışılmıştır. Fakat C++'ta referanslar da olduğu için durum biraz daha farklılışamaktadır. C++'ta bir fonksiyon türünden referanslar söz konusu olabilir. Tabii böyle referanslar bir fonksiyon ile ilkdeğer verilerek tanımlanmalıdır. Örneğin: void foo() { cout << "foo" << endl; } //... void (&f)() = foo; f(); Bu referans ifadesinin eşdeğer gösterici karşılığı şöyle oluşturulabilir: void (*f)() = &foo; (*f)(); İşte C++'ta fonksiyon referansı ile fonksiyon göstericilerine ilkdeğer verirken aslında doğrudan fonksiyon isimleri kullanılabilmektedir. Ancak genel eğilim (üye fonksiyon göstericilerinde zorunlu olan durum) fonksiyon göstericilerinde fonksiyonun adresinin programcı tarafından alınmasıdır. Örneğin: void (*pf1)() = foo; // geçerli! void (*pf2)() = &foo; // C++'a göre daha uygun --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 34) C'de önek ++, önek --, =, +=, -=, *= gibi işlemli atama operatörleri, koşul operatörü sağ taraf değeri üretmektedir. Ancak C++'ta başından beri bu operatörler sol taraf değeri üretirler. Örneğin aşağıdaki ifade C'de geçersiz olduğu halde C++'ta geçerlidir: (a = 10) = 20; // C'de geçersiz C++'ta geçerli C'de a = 10 işleminden elde edilen ürün bir sağ rataf değeri olduğu için ona bir değer atanamaz. Ancak C++'ta atama operatöründen elde edilen ürün atanan nesnenin kendisidir. Dolayısıyla yukarıdaki örnek C++'ta geçerlidir. Bu durumda a'ya önce 10 atanacak sonra 20 atanacaktır. Benzer biçimde aşağıdaki ifade de C'de geçersiz olduğu halde C++'ta geçerlidir: ++a = 10; // C'de geçersiz C++'ta geçerli Ancak C++'ta da sonek ++ ve -- operatörleri sol taraf değeri üretmemektedir. Örneğin: a++ = 10; // C'de de C++'ta da geçersiz! Benzer biçimde aşağıda ifade de C'de geçersiz olduğu halde C++'ta geçerlidir: (foo() ? a : b) = 10; // C'de geçersiz C++'ta geçerli Normalde koşul operatörünün operand'ları farklı türlerdense işlem öncesi otomatik tür dönüştürmesi yoluyla aynı türe dönüştürülmektedir. Ancak C++'ta koşul operatörünün ürettiği değere atama yapılıyorsa ve operatörün ikinci ve üçüncü operand'ları nesne belirtiyorsa bunların aynı türden olması zounludur. Yani yukarıdaki örnekte a ve b aynı türden olmak zorundadır. Yukarıdaki işlemin eşdeğeri şöyledir: if (foo()) a = 10; else b = 10; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 35) C++'a C++11 ile birlikte decltype isimli bir "tür belirleyicisi (type specifier)" de eklenmiştir. decltype belirleyicisi parantez içerisinde bir ifade ile kullanılmaktadır. Genel biçimi şöyledir: decltype() C++ statik tür sistemine sahip bir programlama dili olduğu için bir ifadenin türü derleme aşamasında derleyici tarafından belirlenebilmektedir. İşte decltype belirleyicisi bizim ona verdiğimiz ifadenin türünü belirtir. Yani decltype belirleyicisine bir ifade verdiğimizde aslında biz o ifadenin türünü belirtmiş gibi oluruz. Örneğin: int a; const long b = 10; Burada decltype(a) int türünü, decltype(b) const long türünü, decltype(a + b) ise long türünü belirtmektedir. Biz tür bilgisi gereken her yerde decltype belirleyicisini kullanabiliriz. Örneğin: decltype(a) c; // burada c int türünden decltype(b) d = 10; // burada d const long türünden decltype(a + b) e; // burada e long türden Örneğin: int foo(int a) { //... } //... decltype(&foo) pf; // burada pf int (*)(int) türünden decltype(foo()) a; // burada a int türden decltype belirleyicisinde parantezler içerisine bir fonksiyon ismi yazıldığında oluşturulan tür fonksiyon adres türü olmamaktadır. O fonksiyonun türü olmaktadır. Dolayısıyla eğer parantezler içerisinde bir fonksiyon ismi bulundurulacaksa bu durumda gösterici ya da referans oluşturmak için dekleratörde * ya da & atomunun bulundurulması gerekmektedir. Örneğin: decltype(foo) *pf; // burada decltype foo int(int) türünden bir fonksiyon belirtmektedir. Aynı durum diziler için de söz konusudur. decltype belirleyicisinin parantezleri içerisinde bir dizi ismi verilirse decltype bu dizi türünü belirtir. Örneğin: int a[10]; decltype(a) b; // int b[10] ie eşdeğer, yani b int[10] türünden Tabii decltype belirleyicinin parantezi içerisinde yazdığımız ifade çalıştırılmamaktadır. Derleyici yalnızca o ifadenin türüyle ilgilenmektedir. Anımsanacağı gibi fonksiyon parametresi olarak dizi ya da fonksiyon dekleratörü kullanılırsa bu dekleratör gösterici belirtir. Örneğin: void foo(int a[]) { //... } Burada a int türden bir göstericidir. Örneğin: void bar(void f()) { //... } Burada f geri dönüş değeri void olan parametresi olmayan bir fonksiyon göstericisidir. Tabii prototiplerde biz değişken isimlerini yazmak zorunda değiliz: void foo(int[]); void bar(void()); Bu tür durumlarda decltype belirleyicisinden de faydalanılabilmektedir. Örneğin: void foo() { //... } void bar(decltype(foo) f) { //... } Buradaki bar fonksiyonunun tanımlanması aşağıdaki tanımlamayla eşdeğerdir: void bar(void f()) { //... } Tabii dekleratörde açıkça gösterici de kullanabilirdik: void bar(decltype(foo) *f) { //... } Buradaki bar tanımlaması da aşağıdaki ile eşdeğerdir: void bar(void (*f)()) { //... } Aynı tanımlamayı şöyle de yapabilirdik: void bar(decltype(&foo) f) { //... } decltype belirleyicisi fonksiyon adreslerine dönen fonksiyonların yazımını da kolaylaştırabilmektedir. Örneğin: void foo() { //... } void (*bar())() { //... return foo; } Burada bar geri dönüş değeri void parametresi olmayan bir fonksiyon adresine geri dönmektedir. Aynı tanımlamayı şöyle de yapabilirdik: decltype(foo) *bar() { //... return foo; } Ya da şöyle de yapabilirdik: decltype(&foo) bar() { //... return foo; } Tabii C++11 ile birlikte zaten fonksiyonların geri dönüş değerlerinde auto tür belirleyicisi kullanılabildiği için biz yukarıdaki fonksiyonu aşağıdaki gibi de tanımlayabiliriz: auto bar() { //... return foo; } decltype belirleyicisine operand olarak "sol taraf değeri oluşturan bir ifade" verilirse bu durumda tür sol taraf değeri referansı olmaktadır. Örneğin: int a[1]; int b = 10; decltype(a[0]) c = b; Burada c bir referanstır ve "int &" türündendir. Burada decltype belirleyicisinin parantezleri içerisinde operatör içerenbir ifade olduğuna dikkat ediniz. decltype belirleyicisine operand olarak bir değişken ismi verilmesiyle sol taraf değeri oluşturan bir ifade verilmesi arasındaki farka dikkat ediniz: int a[0]; int b; Burada decltype(a[0]) "int &" türünü, decltype(b) int türünü belirtmektedir. Bir ismi paranteze aldığımızda bir ifade oluşturduğuna göre burada decltype((a)) da "int &" türünü belirtecektir. C++14 ile birlikte decltype belirleyicisi auto belirleyici ile birlikte de kullanılabilir hale getirilmiştir. Örneğin: int a; decltype(auto) b = a; decltype bu biçimde auto ile yalnızca bildirimlerde kullanılabilir ve decltype(auto) ile bildirilen değişkenlere ilkdeğer verilmek zorundadır. Örneğin: decltype(auto) x; // geçersiz! ilkdeğer verilmemiş Örneğin: int a; decltype(auto) b = a; decltype(auto) ile auto arasında ince bir farklılık vardır. Normal olarak auto olarak bildirilmiş olan değişkene verilen ilkdeğer referans ise bildirilen değişken referans olmaz. Örneğin: int a = 10; int &b = a; auto c = b; // c int türden Ancak decltype(auto) ile bildirilen değişkene verilen ilkdeğer referans ise bildirilen değişken de referans olmaktadır. Örneğin: int a = 10; int &b = a; decltype(auto) c = b; // c int türden referans yani int & türünden decltype(auto) özellikle fonksiyonların geri dönüş değerlerinde anlamlı bir biçimde kullanılmaktadır. Örneğin: decltype(auto) foo() { //... } Burada foo fonksiyonun return ifadesinde bir referans varsa fonksiyonun geri dönüş değeri de return ifadesindeki tür türünden bir referans olacaktır. Eğer return ifadesinde bir referans yoksa bu durumda fonksiyonun geri dönüş değeri return ifadesindeki tür türünden olacaktır. Yukarıda da belirttiğimiz gibi C++14 ile eklenen decltype(auto) belirleyicisi daha çok fonksiyonların geri dönüş değerlerinde karşımıza çıkmaktadır. Ancak bu konudaki örnekler şablon içeren nispeten karmaşık örneklerdir. Bu nedenle biz burada bu duruma örnek vermeyeceğiz. İlerideki konularda bununla ilgili örnekler verilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 36) C++11 ile birlikta C++'a NULL adres sabitini temsil eden nullptr anahtar sözcüğü de eklenmiştir. Anımsanacağı gibi C'de NULL adres sabiti iki biçimde belirtiliyordu: 1) 0 değerini veren tamsayı türlerine ilişkin sabit ifadeleri 2) 0 değerini veren tamsayı türlerine ilişkin sabit ifadelerinin void * türüne dönüştürülmüş hali. Örneğin (void *) 0 gibi. C++'ta NULL adres sabiti başından beri 0 değerini veren tamsayı türlerine ilişkin sabit ifadesi biçiminde oluşturulmaktaydı. (C++'ta void * türü diğer türlere otomatik biçimde dönüştürülmediği için NULL adres sabiti olarak kullanılmamaktadır.) C++'ta NULL sembolik sabiti de tipik olarak şöyle define edilmiştir: #define NULL 0 Tabii standartlara bakılırsa NULL sembolik sabiti 0 değerini veren tamsayı türlerine ilişkin bir sabit ifadesi olarak da define edilebilir. NULL adres her ne kadar 0 ile temsil ediliyor olsa da NULL adresin sayısal bileşeni derleyicileri yazanlar tarafından belirlenmektedir. Yani NULL adres belleğin tepesindeki 0 adresi olmak zorunda değildir. NULL adres derleyici tarafından o sistemde kullanılmayan herhangi bir adres olabilir. Ancak yaygın sistemlerin hemen hepsinde derleyiciler NULL adres olarak gerçekten belleğin tepesindeki 0 adresini almaktadır. Örneğin: int *pi; pi = 0; // pi'ye int 0 değil NULL adres değeri atanıyor Burada pi göstericisine 0 adresi atanmamaktadır. O sistemdeki NULL adres neyse o atanmaktadır. Benzer biçimde örneğin: if (pi == 0) { // pi'nin içerisinde 0 adresi vr mı diye bakılmıyor, o sistemdeki NULL adres var mı diye baklılıyor //... } Burada da pi'nin içerisinde 0 adresi var mı diye kontrol yapılmamaktadır. O sistemdeki derleyicini belirlediği NULL adres var mı diye kontrol yapılmaktadır. Örneğin: if (pi) { //... } Bu ifade C'de ve C++'ta aşağıdaki ile eşdeğer kabul edilmektedir: if (pi != 0) { //... } NULL adresin tamsayı 0 ile temsil edilmesi C'nin ilk zamanlarından beri problem yaratan bir durum olmuştur. İşte C++11 ile artık NULL adres için nullptr isminde bir anahtar sözcük dile eklenmiştir. Örneğin: int *pi; pi = nullptr; // C++11 ve sonrasında izlenmesi gereken iyi teknik Burada pi göstericisine yine o sistemde derleyicini belirlediği NULL adres atanmaktadır. nullptr anahtar sözcüğünün C++ standartlarında nullptr_t isimli bir türden olduğu belirtilmektedir. Standartlar nullptr türünü şöyle açıklamıştır: typedef decltype(nullptr) nullptr_t; Burada biraz komik özyinelemeli bir anlatım uygulanmıştır. "nullptr nullptr_t türündendir. nullptr_t ise nullptr'nin türüdür". Özetle nullptr_t yalnızca nullptr değerine sahip olan özel bir türdür. C++11 ve sonrasında artık NULL adres sabiti için 0 yerine bu nullptr anahtar sözcüğünün kullanılması iyi bir teknik kabul edilmektedir. Bu anahtar sözcüğün eklenmesiyle birlikte "overload resolution" mekaznizması bu bakımdan güçlendirilmiştir. Null adresi temsil etmek için C23'e de aynı biçimde bir nullptr anahtar sözcüğünün eklenmesine karar verilmiştir. Belki de C'de en başından beri bu NULL adresin bir anahtar sözcük ile temsil edilmesi gerekiyordu. Pascal, Java, C# diller çok önceleri NULL adres için bir anahtar sözcük kullanmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 37) Anımsanacağı gibi typedef anahtar sözcüğü bir bildirime eklenebilir ve bildirimdeki değişkeni o değişkenin türüne ilişkin tür ismi haline getirir. C++11 ile birlikte bazı nedenlerden dolayı alternatif bir typedef mekanizması da dile eklenmiştir. Buna "type alias" da denilmektedir. Bu alternatif typedef mekanizmasının genel biçimi şöyledir: using = ; Örneğin: using I = int; Bu aşağıdaki typedef bildirimi eşdeğer etki oluşturmaktadır: typedef int I; Örneğin: using STRA = const char *[5]; Burada STRA türü 5 elemanlı her elemanı const char türünden bir gösterici olan diziyi temsil etmektedir. Yani: STRA names; bildirimi ile: const char *names[5]; bildirimi eşdeğerdir. Örneğin: using PF = void (*)(); Burada PF geri dönüş değeri void parametresi olmayan bir fonksiyon adres türünü temsil etmektedir. Yani: PF pf; bildiri ile aşağıdaki bildirim eşdeğerdir: void (*pf)(); C'de ve C++'ta void anahtar sözcüğü de teknik olarak bir tür belirtmektedir. Örneğin: using VOID = void VOID *pv; Bu bildirim aşağıdaki ile eşdeğerdir: void *pv; Pekiyi C++11 ile birlikte neden böyle alternatif bir typedef mekanizması eklenmiştir? Öncelikle bu yeni mekanizma daha doğal bir görünüme sahiptir. typedef bildirimleri kişiler tarafından zor öğrenilmektedir. Ancak bu alternatif typedef mekanizmasının dile asıl yerleştirilme gerekçesi şablon konusunda sağladığı kolaylıklardır. Bu yeni typedef sentaksı şablon mekanizması ile birlikte kullanılabilmektedir. Bu bağlamda çeşitli kolaylıklar sunmaktadır. Şablon mekanizması ileride ele alınacaktır. decltype belirleyicisi ise typedef ya da using tür bildirimi birlikte de kullanılabilmektedir. Örneğin: void foo() { //... } using FP = decltype(foo) *; FP bar() { //... return foo; } Ya da örneğin: typedef decltype(&foo) FP; FP bar() { //... return foo; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 15. Ders 04/10/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 38) C++'ta parametre değişkenleri default değer alabilmektedir. Fonksiyon çağrılırken default değer alan parametre değişkenleri için argüman girilmeyebilir. Bu durumda sanki argüman olarak o default değerlerin girilmiş olduğu kabul edilir. Eğer default değer alan parametre değişkenleri için argüman girilmişse bu durumda bu default değerler dikkate alınmaz. Örneğin: void foo(int a, int b = 10, int c = 20) { //... } Burada a parametre değişkeni için argüman girilmek zorundadır. Ancak b ve c parametre değişkenleri için argüman girilmeyebilir. Default argümanın verilme biçimine dikkat ediniz. Önce parametre değişkeninin ismi sonra '=' atomu ve sonra default değer belirtilmektedir. Default değer alan parametre değişkenlerine ilişkin fonksiyuonlar çağrıldığında aslında her zaman parametre değişkenleri için argüman ataması yapılmaktadır. Örneğin: foo(100, 200); Burada c parametre değişkeni için argüman belirtilmemiştir. Bu durumda c parametre değişkenine default olarak 20 değeri atanacaktır. Yani yukarıdaki çağrının eşdeğeri aşağıdaki gibidir: foo(100, 200, 20); Default argüman yalnızca çağrımı kolaylaştırmaktadır. Örneğin: foo(100, 200); Bu çağrının makine kodlarına bakıldığında aslında c parametre değişkeni için 20 değerinin akratıldığı görülecektir. Yani verilen default değerlerin aktarımı konusunda bir etkinlik farkı söz konusu değildir. Buradaki fonksiyon her zaman üç parametreye sahiptir ve çağrım sırasında her zaman bu üç parametre için üç argüman aktarılmaktadır. Yukarıdaki fonksiyonu aşağıdaki gibi çağırmış olalım: foo(100); Bu çağrının eşdeğeri şöyledir: foo(100, 10, 20); Bu örneğimizde a parametre değişkeni default değer almadığı için bu parametre değişkeni için argüman girilmesi zorunludur. Bir fonksiyonun bütün parametre değişkenleri default değer alabilir. Örneğin: void foo(int a = 10, int b = 20, int c = 30) { //... } Böyle bir fonksiyonu biz argümansız da çağırabilirdik: foo(); Bu çağrının eşdeğeri şöyledir: foo(10, 20, 30); Bir parametre değişkeni default değer almışsa onun sağındakilerin hepsinin default değer almış olması gerekmektedir. Örneğin: void foo(int a = 10, int b, int c = 20) // geçersiz! { //... } Burada yukarıda belirttiğimiz kurala uyulmamıştır. a parametre değişkeni default değer aldığı için onun sağındakilerin hepsinin default değer alması gerekirdi. Bu kural şöyle de ifade edilebilir:"" Default değer alan parametre değişkenleri parametre listesinin sağında birikmiş olmalıdır". Eğer yukarıdaki gibi bir durum geçerli olsaydı aşağıdaki gibi bir sentaksın da geçerli olması gerekirdi: foo(100, , 200); // böyle bir sentaks yok! Halbuki böyle bir sentaks yoktur. Parametre değişkenlerine verilen ilkdeğerler tipik olarak sabit ifadesi olsalar da bu zorunlu değildir. Global değişkenler ve hatta fonksiyonlar da bu ilkdeğer vermede kullanılabilmektedir. (Sınıflar konusuna geldiğimizde üye fonksiyonlar için sınıfın veri elemanları da ilkdeğer vermede kullanılabilmektedir.) Örneğin: int g_x = 10; void foo(int a = g_x + 1) // geçerli { //... } Ancak verilen ilkdeğerde önceki parametre değişkenleri kullanılamamaktadır. Örneğin: void foo(int a, int b = a + 1) // geçerli değil! { //... } Örneğin parametre değişkenine verilen default değer bir string olabilir: void putmsg(const char *msg = "Ok") { cout << msg << endl; } //... putmsg("invalid command"); putmsg(); Parametre değişkenine verilen default değer fonksiyon çağrısının geri dönüş değeri ile ilişklili de olabilir. Örneğin: int square(int a) { return a * a; } void bar(int a = square(10)) { //... } Tabii parametre değişkenlerine aslında fonksiyon çağrısı sırasında değer atanmaktadır. Yani parametre değişkeni örneğin global bir değişkene bağlı ise global değişkenin son değeri işleme sokulacaktır. Örneğin: int g_x = 10; void foo(int a = g_x + 1) { //... } //... foo(); Buradaki çağrının eşdeğeri şöyledir: foo(g_x + 1); Dolayısıyla a parametre değişkenine 11 değeri kopyalanacaktır. Örneğin: g_x = 20; foo(); Buradaki çağrının da eşdeğeri yukarıdaki gibi olduğuna göre o anca g_x global değişkeninin değeri kullanılacağından a parametre değişkenine 21 kopyalanacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int a = 100, int b = 200) { cout << "a = " << a << ", b = " << b << endl; } int main() { foo(); // foo(100, 200) foo(10); // foo(10, 100) foo(10, 20); // foo(10, 20) return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Default değer alan parametre değişkenlerine sahip fonksiyonlarda default değerler prototipte belirtilebilirler (genellikle böyle yapılır). Default değerler hem prototipte hem de tanımlamada belirtilemezler. Ya prototipte (tercih böyle olmalıdır) ya da tanımlamada belirtilmelidirler. Örneğin: void foo(int a = 10, int b = 20); void bar(double = 3.14); void foo(int a = 10, int b = 20) // error! { //... } void bar(double a) // geçerli { //... } Yukarıda da belirttiğimiz gibi eğer prototip bulunduruluyorsa default değerlerin prototipte belirtimesi tanımlama sırasında belirtilmemesi iyi tekniktir. Örneğin: void foo(int a = 10, int b = 20, int c = 30); //... void foo(int a, int b, int c) { //... } Tabii prototiplerde de bir parametre değişkeni default değer almışsa onun sağındakilerin hepsinin default değer alması gerekir. Örneğin: void foo(int a = 10, int b, int c = 20); // geçersiz prototip! Prototiplerde değişken ismi belirtilmeyebileceğine göre aşağıdaki prototipler biraz tuhaf gözükmekle birlikte geçerlidir: void foo(int = 10, int = 20, int = 30); // tuhat ama geçerli Standartlara göre bir paramere değişkenine yalnızca bir kez default değer verilebilir. Amcak bazı default değerler prototipte, bazıları ise tanımlama sırasında da belirtilebilmektedir. Örneğin aşağıdaki bildirimler geçerlidir: void foo(int a, int b = 10, c = 30); void foo(int a = 20, int b, int c) // geçerli, ama kötü teknik! { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void dispmsg(const char *msg = "Ok"); int main() { dispmsg(); dispmsg("error"); return 0; } void dispmsg(const char *msg) { cout << msg << endl; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Parametre değişkenlerine verilen default değerlerin yaygın kullanılan değerlerden olması gerekir. Aksi takdirde parametre değişkenin default değer almasının bir anlamı kalmaz. Parametre değişkenlerine herhangi bir değeri default değer olarak vermek kötü bir tekniktir. Örneğin: int add(int a = 10, int b = 20) { return a + b; } Burada verilen default değerlerin diğer değerlerden hiçbir farkı yoktur. Dolayısıyla böyle bir kullanım kötü bir tekniktir. Bu biçimde verilen ilkdeğerler bir anlam ifade etmediği gibi kodu inceleyenleri de yanlı yönlendirebilmektedir. Fakat örneğin: void disp_number(int a, int base = 10); Burada fonksiyonun ikinci parametresi birinci parametresindeki int değerin kaçlık sistemde ekrana yazdırılacağını belirtiyor olsun. Burada verilen default değer anlamlıdır. Parametre değişkenlerinin default değer alması kullanım kolaylığı da sağlamaktadır. Bazen fonksiyonların çok fazla parametresi olabilir. Fonksiyonu çağıran kişi bu detayları bilmek istemeyebilir. Böylece default değer almış parametreler programcı argüman girmez. Onlar için makul değerler fonksiyonu yazanlar tarafından zaten kullanılmış durumdadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bazen parametre değişkenlerine verilen default değerler aslında o parametre değişkenleri için argüman girilmediğini tespit etmekte kullanılmaktadır. Programcı asıl default değerleri başka bir biçimde elde ediyor olabilir. Örneğin: void disp_date(int day = 0, int month = 0, int year = 0); Burada aslında parametre değişkenlerine verilen 0 değeri çok kullanıldığı için verilmemiştir. Fonksiyonun default değerle çağrılıp çağrılmadığını anlamak için verilmiştir. Örneğin: void disp_date(int day = 0, int month = 0, int year = 0) { auto t = time(nullptr); tm *pt; pt = localtime(&t); if (!day) day = pt->tm_mday; if (!month) month = pt->tm_mon + 1; if (!year) year = pt->tm_year + 1900; cout << day << '/' << month << '/' << year << endl; } Burada eğer ilgili parametre için argüman girilmemişse o anda içinde bulunulan tarihe ilişkin değer kullanılmıtr. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; void disp_date(int day = 0, int month = 0, int year = 0) { auto t = time(nullptr); tm *pt; pt = localtime(&t); if (!day) day = pt->tm_mday; if (!month) month = pt->tm_mon + 1; if (!year) year = pt->tm_year + 1900; cout << day << '/' << month << '/' << year << endl; } int main() { disp_date(); disp_date(12); disp_date(12, 10); disp_date(12, 10, 2020); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 39) Anımsanacağı gibi C'de statik ömürlü (yani global ve statik yerel) nesnelere verilen ilkdeğerlerin sabit ifadesi olması zorunludur. Örneğin aşağıdaki gibi bir global değişken tanımalması C'de geçerli değildir: int square(int a) { return a * a; } int g_x = square(10); // C'de geçersiz! Bu kuralın gerekçesi statik ömürlü nesnelerin ilkdeğerleriyle birlikte amaç dosyaya ve çalıştırılabilir dosyaya yazılması zorunluluğudur. Dolayısıyla derleyicinin derleme aşamasında statik ömürlü nesnelere verilen ilkdeğeri belirlemiş olması gerekmektedir. Ancak C++'ta böyle bir zorunluluk yoktur. Yani C++'ta statik ömürlü nesneleri sabit ifadeleriyle ilkdeğer verilmesi zorunlu değildir. Yukarıdaki kod C'de geçersiz olduğu halde C++'ta geçerlidir. Pekiyi C++'ta statik ömürlü nesnelere nasıl ilkdeğer verilmektedir? Aslında bir C programında ve C++ programında akışın başlatıldığı yer main fonksiyonu değildir. Akış derleyicilerin yerleştirdiği ismine "start-up code" denilen bir koddan başlatılır. Bu kod main fonksiyonunu çağırmaktadır. main bitince akış yine start-up code döner. Zaten exit fonksiyonu da start-up code tarafından çağrılmaktadır. Startup-up code'un temsili şöyledir: ... ... ... status = main() exit(status) İşte C++'ta eğer global değişkenlere sabit ifadesi ile ilkdeğer verilmemişse bu ilkdeğer verme işlemi main fonksiyonundan önce start-up code tarafından yapılmaktadır. Bunun basit bir sınaması aşağıdaki programla yapılabilir: #include using namespace std; int square(int a) { cout << "square" << endl; return a * a; } int g_x = square(10); int main() { cout << "main" << endl; cout << g_x << endl; return 0; } Program çalıştırıldığında ekrana (stdout dosyasına) şunlar basılacaktır: square main 100 C++ standartlarına göre global değişkenlere verilen aynı kaynak dosya (translation unit) için yukarıdan aşağıya doğru ilkdeğerleri verilmektedir. Ancak proje birden fazla kaynak dosyadan oluşuyorsa bunların birbirlerine göre durumu hakkında bir sıra belirtilmemektedir. Statik yerel değişkenlere sabit ifadeleriyle ilkdeğer verilmemişse eğer fonksiyon çağrılmışsa ve yalnızca ilk çağrıldığında akış ilkdeğer verme noktasına geldiğinde bu ilkdeğer verme işlemi gerçekleştirilmektedir. Aşağıdaki program kuralın test edilmesinde kullanılabilir: #include using namespace std; int square(int a) { cout << "square" << endl; return a * a; } void foo() { cout << "foo" << endl; static int x = square(10); cout << x << endl; } int main() { foo(); foo(); return 0; } Eğer burada foo hiç çağrılmasaydı square fonksiyonu da çağrılmayacaktı. foo birden fazla kez çağrıldığında yalnızca ilk çağırmada toplamda bir kez square çağrılacaktır. Akış statik yerel değişkenin tanımlandığı noktaya gelmezse square zaten hiç çağrılmayacaktr. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 40) C'de fonksiyon tanımlarken parametre değişkenlerine isim verilmesi zorunludur. Ancak C++'ta böyle bir zorunluluk yoktur. Örneğin: void foo(int) // C'de geçersiz, C++'ta geçerli { //... } Tabii parametre değişkenine isim verilmemesi onun kullanılamayacağı anlamına da gelmektedir. C++'ta parametre değişkenine isim verilmemiş olsa da çağırma sırasında yine onun için argüman tedarik edilmek zorundadır. Örneğin: void foo(int) { //... } //... foo(); // geçersiz! foo(10) // geçerli Yine isimsiz parametreler için default argüman belirtilebilmektedir. Ancak bu durum genel olarak bir faydaya yol açmamaktadır. Örneğin: void foo(int = 10) // geçerli ama tuhaf { //... } İsim verilmemiş parametre değişkenlerinin parametre listesinin sonunda toplanması gibi bir zorunluluk yoktur. Örneğin: void foo(int a, int, int b) // geçerli { //... } Parametre değişkenlerine isim verilmemesi onların programcı tarafından kullanılamamasına yol açar. Pekiyi neden programcı böyle bir şeyi tercih etsin? İşte bazen programcının aynı parametrik yapıya sahip birden fazla fonksiyon tanımlaması gerekebilmektedir. Bu tür durumlarda kullanılmayan ama sözde (dummy) bir parametre değişkenine gereksinim duyulmaktadır. Sonraki paragrafta fonksiyonların overload edilmeleri konusu ele alınmaktadır. Tabii mademki isim verilmemiş parametre değişkenleri aslında fonksiyon tarafından kullanılamamaktadır. O halde optimizasyon sırasında derleyici o parametre değişkeni için bazı koşullar da sağlanıyorsa argüman parametre aktarımını hiç yapmayabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 16. Ders 09/10/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 41) C++'ta parametrik yapıları farklı olmak koşuluyla aynı faaliyet alanında aynı isimli birden fazla fonksiyon bulunabilir. Bu duruma İngilizce "function overloading" denilmektedir. Halbuki C'de hiçbir durumda aynı isimli birden fazla fonksiyon bulunamamaktadır. Parametrik yapıların farklı olması demek parametrelerin türce veya sayıca farklı olması demektir. Parametre değişkenlerinin isimlerinin ve fonksiyonların geri dönüş değerlerinin türlerinin bu bağlamda bir önemi yoktur. Önemli olan parametre değişkenlerinin ""türlerinin ya da sayılarının"" farklı olmasıdır. Örneğin aşağıdaki foo fonksiyonları C++'ta birlikte bulunabilir: void foo(int a) { //... } void foo(double a) { //... } void foo(int a, int b) { //... } void foo() { //... } Ancak aşağıdaki fonksiyonlar bir arada bulunamazlar: void bar(int a) { //... } void bar(int b) { //... } int bar(int c) { //... } Fonkisyonların geri dönüş değerlerinin farklı olması bu bağlamda dikkate alınmamaktadır. Yani parametrik yapısı aynı olan ancak geri dönüş değerleri farklı olan aynı isimli fonksiyonlar bir arada bulunamazlar. Örneğin: void foo(int a, int b) { //... } int foo(int a, int b) // geçersiz! overload edilemez { //... } Buradaki iki foo fonksiyonu overload edilemez. Çünkü bu iki fonksiyonun parametrik yapısı aynıdır. Bunların geri dönüş değerlerinin farklı olması bu bağlamda bir farklılık oluşturmamaktadır. Farklı parametrik yapılara ilişkin aynı isimli fonksiyonların bulunabilmesi özelliği yalnızca C++'ta değil diğer nesne yönelimli programlama dilelrini büyük bölümünde de vardır. Örneğin bu özelliğe Java ve C#'ta "method overloading" denilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyon ismi ve parametre türlerinin oluşturduğu dizilime "fonksiyonun imzası (function signature)" da denilmektedir. Örneğin: void foo(int *pi, int size) { //... } Burada fonksiyonun imzası "foo, int *, int" biçimindedir. İmzaya parametre değişkenlerinin isimlerinin ve fonksiyonun geri dönüş değerinin dahil edilmediğine dikkat ediniz. Örneğin: int bar(int a, long b) { //.. } Burada bar fonksiyonunun imzası "bar, int, long" biçimindedir. O halde overload edilme kuralını şu biçimde de ifade edebiliriz: "Aynı faaliyet alanı içerisinde aynı imzaya sahip olan birden fazla fonksiyon tanımlanamaz, ancak farklı imzalara sahip fonksiyonlar tanımlanabilir." Fonksiyon imzasının bir küme değil bir dizilim belirttiğine dikkat ediniz. Aşağıdaki iki fonksiyonun imzası farklıdır, dolayısıyla bu fonksiyonlar overload edilebilirler: void foo(int a, long b) // imza: foo, int, long { //... } void foo(long a, int b) // imza: foo, long, int { //... } Tabii türlerin typedef ya da using isimleri değil onların gerçek türleri imzayı oluşturmaktadır. Örneğin: void foo(size_t size) { //... } void foo(unsigned a) { //... } Burada eğer size_t türü ilgili sistemde unsigned int olarak typedef edilmişse bu iki fonksiyonun imzası aynı olur. Dolayısıyla overload edilemez. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- const ve volatile niteleyicileri semantik bakımdan tamamen farklı olsa da sentaks bakımından birbirlerine çok benzemektedir. Bu nedenle C++ standartlarında bu iki niteleyiciye "cv niteleyicileri (cv qualifiers)" da denilmektedir. Nesnenin kendisini niteleyen cv niteleyicileri "üst düzey (top level)" cv niteleyicileri olarak da isimlendirilmektedir. Örneğin: const int a = 10; Burada const niteleyicisi nesnenin kendisini const yaptığı için üst düzey niteleyicidir. Fakat örneğin: const int *pi; Burada const niteleyicisi nesnenin kendisini değil göstericinin gösterdiği yeri const yapmaktadır. Dolayısıyla üst düzey değildir. Türlerdeki "üst düzey const ve volatile niteleyicileri (top level cv qualifiers)" türleri farklılaştırmamaktadır. Yani fonksiyonun imzası oluşturulurken üst düzey (top level) const ve volatile niteleyicileri atılmaktadır. Örneğin: const int a = 10; // buradaki const üst düzey const volatile int b = 20; // buradaki const ve volatile üst düzey const int *pi; // buradaki const üst düzey değil const char * volatile str; // buradaki volatile üst düzey, const üst düzey değil const char * const *names; // buradaki her iki const da üst düzey değil Referanslardaki const ve volatile niteleyicileri referansların kendilerine ilişkin olmadığı için üst düzey değildir. Zaten referanslarda üst düzey const ve volatile oluşturulamamaktadır. Örneğin: int a = 10; const int &r = a; // buradaki const üst düzey değil Örneğin: void foo(int a) { //... } void foo(const int a) { //... } Burada iki foo fonksiyonunun imzaları aynıdır. Dolayısıyla aynı anda bulunamazlar. Ancak üst düzey olmayan const ve volatile niteleyicileri türleri farklılaştırmaktadır. Yani fonksiyonun imzasında üst düzey olmayan const ve volatile belirleyicileri korunmaktadır. Örneğin: void foo(int *pi, int size) // imza: foo, int *, int { //... } void foo(const int *pi, int size) // imza: const int *, int { //... } Burada ilk fonksiyonun imzası "foo, int *, int" ikinci fonksiyonun imzası "foo, const int *, int" biçimindedir. Aynı durum referanslar için de söz konusudur. Örneğin: void foo(int a) // imzası: foo, int { //... } void foo(int &r) // imzası: foo, int & { //... } void foo(const int &r) // imzası: foo, const int & { //... } Buradaki üç foo fonksiyonunun da parametrik yapıları dolayısıyla imzaları farklıdır. Overload işlemi geçerlidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyonlardaki default argümanlar imzayı etkilememektedir. Örneğin: void foo(int a, int b = 10) { //... } void foo(int a) // parametrik yapılar yani imzalar farklı, overload işlemi geçerli { //... } Bu iki fonksiyonun parametrik yapıları yani imzaları farklıdır. Birinci fonksiyonun inzası "foo, int, int" ikinci fonksiyonun imzası ise "foo, int" biçimindedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Amaç dosya formatlarında (object file formats) aynı isimli birden fazla global sembol bulunamamaktadır. Bu durumda C++ derleyicileri overload edilmiş fonksiyonların isimlerini mecburen onların parametre türleriyle kombine ederek amaç dosyaya yazar. Genel olarak bir global sembolün isminin değiştirilerek amaç dosyaya yazılmasına " isim dekorasyonu (name decoration)" ya da "name mangling" denilmektedir. Böylece programcı fonksiyonlara aynı isimleri vermiş olsa da aslında C++ derleyicileri onları farklı isimlerle amaç dosyaya yazmaktadır. C++ standartları "name decoration" konusunda herhangi bir belirlemede bulunmamıştır. Yani bu konu tamamen derleyicileri yazanların isteğine bırakılmıştır. Derleyiciler arasında "name decoration" bakımından önemli farklılıklar bulunabilmektedir. Genellikle C++ programcılarının bu konunun ayrıntılarını bilmesine gerek yoktur. Ancak sembolik makine dilinde C++ için fonksiyon yazanların mecburen bu durumun farkında olması gerekir. Aslında "name decoration" C'de de uygulanabilmektedir. Örneğin Microsoft C derleyicileri global sembollerin başın "_" öneki getirerek onları amaç koda yazar. Yine 32 bit sistemlerde Microsoft değişik "fonksiyon çağırma biçimlerine (calling convention)" göre değişik isim dekorasyonu kullanmaktadır. Aşağıdaki bağlantıda Microsoft C++ derleyicilerinin uyguladığı isim dekoroasyonu hakkında ayrıntılı bilgiler verilmektedir: https://en.wikiversity.org/wiki/Visual_C%2B%2B_name_mangling g++ ve clang++ derleyicilerinin isim dekorasyonları için de aşağıdaki bağlantıyı kullanabilirsiniz: https://itanium-cxx-abi.github.io/cxx-abi/abi.html#mangling Microsoft dekore edilmiş isimlerin aslında hangi parametrik yapıya sahip fonksiyonlara karşılık geldiğini anlayabilmek için "undname" isimi bir utility program da bulundurmuştur. Örneğin: F:\Dropbox\Kurslar\SysProg-1\Src>undname ?foo@@YAXHH@Z Microsoft (R) C++ Name Undecorator Copyright (C) Microsoft Corporation. All rights reserved. Undecoration of :- "?foo@@YAXHH@Z" is :- "void __cdecl foo(int,int)" --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aynı isimli bir fonksiyon çağrıldığında o fonksiyonlardan hangisinin çağrıldığının tespit edilmesi sürecine İngilizce "overload resolution" denilmektedir. Overload resolution işleminin bazı ayrıntılı kuralları vardır. Ancak overload resolution işlemi en basit haliyle şöyledir: "Çağrılma ifadesindeki argümanların türleriyle tam uyuşan bir fonksiyon varsa o fonksiyon çağrılır." Örneğin: void foo(int a) { cout << "int" << endl; } void foo(long a) { cout << "long" << endl; } void foo(double a) { cout << "double" << endl; } void foo(const char *str) { cout << "const char *" << endl; } Burada biz foo fonksiyonunu şöyle çağırmış olalım: foo(10); 10 int türden bir sabittir. Dolayısıyla argüman int türdendir. O halde int paranetreye sahip olan foo fonksiyonu çağrılacaktır. Örneğin: foo(3.14); 3.14 double türdendir. O halde double türden parametreye sahip olan foo fonksiyonu çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int a) { cout << "int" << endl; } void foo(long a) { cout << "long" << endl; } void foo(double a) { cout << "double" << endl; } void foo(int a, int b) { cout << "int, int" << endl; } void foo(const char *str) { cout << "const char *" << endl; } int main() { long x = 100; foo(10); // int foo(10.2); // double foo("ankara"); // const char * foo(x); // long foo(10, 20); // int, int return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi ya çağrılma ifadesindeki argümanların türleriyle tam bir uyuşma gösteren fonksiyon yoksa ne olacaktır? İşte bu durumda iki sonuç oluşabilir. Birincisi çağırma işlemi geçersiz olabilir. Yani çağırma error ile sonuçlanır. İkincisi aynı isimli fonksiyonlardan bir tanesi "kötünün iyisi" olarak seçilir. Bu noktada "overload resolution" sürecinin ayrıntıları devreye girmektedir. İzleyen paragraflarda ayrıntılı kurallar açıklanacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Derleyici tarafından yapılan dönüştürmelere "otomatik tür dönüştürmeleri (implicit type conversions)" denilmektedir. Overload resolution sürecinin ayrıntılarını anlayabilmek için otomatik (implicit) tür dönüştürmeleri arasındaki kalite farklılıklarının bilinmesi gerekir. T1, T2 ve T3 birer tür belirtmek üzere T1 -> T3 dönüştürmesi ile T2 -> T3 dönüştürmesi arasında bir iyiilik kıyaslaması yapıldığında üç durum söz konusu olabilir: 1) T1 -> T3 dönüştürmesi T2 -> T3 dönüştürmesinden daha iyi olabilir. 2) T2 -> T3 dönüştürmesi T1 -> T3 dönüştürmesinden daha iyi olabilir 3) T1 -> T3 dönüştürmesi ile T2 -> T3 dönüştürmelerinden hiçbiri diğerinden iyi ya da kötü olmayabilir. Örneğin: int -> long char -> long Burada T1 türü int türünü, T2 türü char türünü ve T3 türü de long türünü temsil etmektedir. C++ standartlarına göre bu iki dönüştürme arasında bir kalite farklılığı yoktur. Örneğin: char -> int long -> int C++ standartlarına göre char -> int dönüştürmesi long -> int dönüştürmesinden daha iyidir. Otomatik dönüştürmeler arasındaki kalite farklılıkları iyiden kötüye doğru şöyle sıralanmaktadır: 1) Tam Uyum (Exact Match): Üst düzey (top level) const ve volatile belirleyicileri atıldıktan sonra iki tür birbirinin aynısı ise tam uyum söz konusudur. Örneğin: int -> int const int -> int int -> volatile int double -> double 2) int Türüne Yükseltme Dönüştürmesi (Ineteger Promotion Conversion) ya da Double Türüne Yükseltme Dönüştürmesi (Floating Point Promotion): Bilindiği gibi int türünden küçük olan türlerin int türüne dönüştürülmesine standartlarda "integer promotion" ya da "integral promotion" denilmektedir. Benzer biçimde float türünden double türüne dönüştürmeye de "floating point promotion" denilmektedir. Örneğin: char -> int shor -> int float -> double 3) Nümerik Dönüştürmeler (Numeric Conversions): Yukarıdaki durumların dışında kalan temel nümerik türler arasındaki dönüştürmelerdir. Örneğin: int -> long char -> long long -> int char -> long double -> float 4) Kullanıcı Tanımlı Dönüştürmeler (User Defined Conversion): Bu dönüştürmeler sınıflar konusu ile ilgilidir. Burada üzerinde durmayacağız. O halde artık T1 -> T3 ve T2 -> T3 dönüştürmelerinin kıyaslamasını yapabiliriz. Örneğin: char -> int long -> int Burada char -> int dönüştürmesi daha iyidir. Örneğin: float -> double int -> double Burada float -> double dönüştürmesi daha iyidir. Örneğin: float -> long double int -> long double Burada iki dönüştürme de aynı kalitededir. Örneğin: int -> double long -> double Burada da iki dönüştürme aynı kalitededir. Örneğin: short -> long int -> long Burada da iki dönüştürme aynı kalitededir. Java ve C# ile C++ bu bakımdan farklılıklar göstermektedir. Örneğin bu dillerde int -> long dönüştürmesi short -> lng dönüştürmesinden daha iyi kabul edilmektedir. Bu dillerden geçenler bu durumlara dikkat etmemlidirler. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çağrılma ifadesindeki argümanların türleriyle tam uyuşan bir fonksiyon yoksa derleyici bazı kurallara göre mevcut fonksiyonların içerisinden birini seçebilmektedir ya da çağrıyı geçersiz kabul edebilmektedir. İşte bu noktada "overload resolution" işleminin bazı ayrıntıları devreye girmektedir. Şimdi bu ayrıntılar üzerinde duracağız. Overload resolution işlemi üç aşamada yürütülmektedir: 1) Önce aday fonksiyonlar (candidate functions) belirlenir. 2) Aday fonksiyonların içerisinden uygun olan (viable) fonksiyonlar seçilir. 3) Uygun olan fonksiyonlar arasından en uygun olan (the best viable) fonksiyon seçilmeye çalışılır. Eğer en uygun fonksiyon varsa ve bir tane ise overload resolution işlemi başarıyla sonuçlandırılır. En uygun fonksiyon yoksa ya da birden fazla ise overload resolution işlemi başarısızlıkla sonuçlanacaktır. Overload resolution işlemini iş yerine personel alımı için işletilen sürece benzetebiliriz. Önce kişiler CV'lerini gönderirler. Bunlar adaylardır. Onların arasından uygun olanlar seçilir ve mülakata çağrılır. Onların arasından da en uygun kişi seçilmeye çalışılır. 1) İlgili faaliyet alanında çağrılma ifadesi ile aynı isimli olan tüm fonksiyonlar aday fonksiyonlardır. Yani bir fonksiyonun aday olması için isminin aynı olması yeterlidir. 2) Çağrılma ifadesindeki argümanların sayısı ile aynı sayıda parametre değişkenine sahip olan ve her argümandan parametre değişkenine otomatik (implicit) tür dönüştürmesinin mümkün olduğu fonksiyonlar uygun (viable) fonksiyonlardır. (Yani uygun fonksiyon demek "eğer bu fonksiyon tek başına bulunsaydı çağrılabilirdi" demektir.) Bir aday fonksiyonunun uygun olabilmesi için argümanlarla aynı sayıda parametre değişkenine sahip olması ve argümanlardan parametre değişkenlerine otomatik dönüştürmenin mümkün olması gerekmektedir. Fonksiyonun n tane parametresi olduğunu ve fonksiyon k tane argümanla çağrıldığını varsayalım. Bu k tane argümanın hepsinden n tane parametrenin ilk k tanesi ile otomatik dönüştürme mümkün olsun. Eğer fonksiyonun n - k tane parametresi defult değer alıyorsa bu fonksiyon da uygun fonksiyondur. Benzer biçimde fonksiyonun k + 1'inci parametresi ya da default değer almış olan parametrelerin sonundaki parametresi "... (ellipsis)" biçimindeyse bu fonksiyon da uygun fonksiyondur. 3) En uygun fonksiyon "her argüman parametre deönüştürmesi diğer uygun fonksiyonlara göre ya daha iyi olan ya da daha kötü olmayan" fonksiyondur. Eğer böyle tek bir fonksiyon varsa o fonksiyon en uygun (best viable) fonksiyon olarak seçilir. Eğer bu biçimde birden fazla fonksiyon varsa ya da hiçbir fonksiyon yoksa çağırma işlemi geçersizdir ve error ile sonuçlanır. Örneğin aşağıdaki gibi overload edilmiş foo fonksiyonları olsun: void foo(int a, int b) // 1 { cout << "int, int" << endl; } void foo(int a, double b) // 2 { cout << "int, double" << endl; } void foo(double a, double b) // 3 { cout << "double, double" << endl; } void foo(const char *str, double a) // 4 { cout << "int, double" << endl; } void foo(int a) // 5 { cout << "int" << endl; } void bar(int a) // 6 { cout << "bar int" << endl; } Biz de bu fonksiyonu aşağıdaki gibi çağırmış olalım: foo(12.3f, 3.2f); Burada 1, 2, 3, 4 ve 5 numaralı fonksiyonlar aday fonksiyonlardır. Ancak yalnızca 1, 2 ve 3 numaralı fonklsiyonlar uygun fonksiyonlardır. Bu 1, 2 ve üç numaralı fonksiyonları çağrılma ifadesindeki her argüman parametre dönüştürmesi için birbirleriyle kıyazlayalım. Birinci argüman-parametre dönüştürmesi şöyledir: float ->int // 1 float -> int // 2 float -> double // 3 Burada 3 numaralı fonksiyonun birinci argüman parametre dönüştürmesi diğer uygun fonksiyonlardan daha iyidir. Şimdi ikinci argüman-parametre dönüştürmesine bakalım: float -> int // 1 float -> double // 2 float -> double // 3 Burada da 2 ve 3 numaralı fonksiyonlar eşit iyiliktedir ancak daha kötü değildir. O halde "tüm argüman-parametre dönüştürmesi diğerlerinden daha iyi olan ya da daha kötü olmayan" fonksiyon 3 numaralı fonksiyondur. Oberload resolution işleminden bu fonksiyon seçilecektir. Şimdi çağrısının şöyle yapıldığını varsayalım: foo('a', 2.3f); Bu durumda yine 1, 2 ve 3 numaralı fonksiyonlar uygun fonksiyonlardır. "Her argüman-parametre dönüştürmesi diğerlerinden daha iyi olan ya da dha kötü olmayan" bir fonksiyon vardır. O da 2 numaralı fonksiyondur. Bu durumda overload resolution işleminden 2 numaralı fonksiyon seçilecektir. Çağrının şöyle yapılmış olduğunu varsayalım: foo(3.14, 10); Burada 3 numaralı fonksiyonun birinci argüman-parametre dönüştürmesi diğerlerinden daha iyidir. Ancak ikinci argüman-parametre dönüştürmesi 1 numaralı fonksiyondan kötüdür. Benzer biçimde 1 numaralı fonksiyonun da nirinci argüman-parametre dönüştürmesi 3 numaralı fonksiyondan kötüdür. O halde burada "tüm argüman-parametre dönüştürmeleri diğerlerinden daha iyi olan ya da daha kötü olmayan" bir fonksiyon yoktur. Dolayısıyla overload resolution işlemi error ile sonuçlanacaktır. Bu tür error durumlarına İngilizce "ambiguity error" de denilmektedir. Aşağıdaki gibi iki bar fonksiyonu olsun: void bar(int a, long b) { //... } void bar(long a, int b) { //... } Biz de fonksiyonu şöyle çağırmış olalım: bar(10, 20); Burada da en uygun fonksiyon bulunamamaktadır. Şimdi aşağıdaki foo fonksiyonları bulunyor olsun: void foo(double a, double b) { //... } void foo(float a, float b) { //... } foo(10, 20); Burada "her argüman-parametre dönüştürmesi diğerlerinden daha iyi olan ya da daha kötü olmayan" iki fonksiyon vardır. Dolayısıyla en uygun fonksiyon iki tanedir. Budurum da error ile sonuçlanacaktır. Tabii biz çok sayıda uygun fonksiyonun olduğu durumda çağrı ifademizdeki argümanlarının türlerini bu uygun fonksiyonlardan birinin parametre türleriyle aynı yaparsak kesinlikle o fonksiyon seçilir. Çünkü tam uyumdan (exact match) daha iyi olabilecek bir fonksiyon yoktur. Tam uyumu sağlayan da zaten birden fazla fonksiyonun overload edilmesi mümkün değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int a, long b) // 1 { cout << "int, long" << endl; } void foo(long a, long b) // 2 { cout << "long, long" << endl; } void foo(double a, long b) // 3 { cout << "double" << endl; } void foo(int a, int b) // 4 { cout << "int, int" << endl; } void foo(double a, double b) // 5 { cout << "double, double" << endl; } void foo(char a, short b) // 6 { cout << "char, short" << endl; } void foo(int a, const char *s) // 7 { cout << "int , const char *" << endl; } void foo(int a) // 8 { cout << "int" << endl; } void bar(int a) // 9 { cout << "bar, int" << endl; } int main() { int a = 10; long b = 20; foo(a, b); // int, long foo('a', 'b'); // ambiguity error! foo(10L, 2.3); // ambiguity error! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 17. Ders 11/10/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örneği inceleyiniz. İlk iki çağrıda en uygun fonksiyon 1 numaralı fonksiyon olarak seçilecektir. Ancak üçüncü çağrıda en uygun fonksiyon bulunamayacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int a, int b) // 1 { cout << "int, int" << endl; } void foo(double a, int b) // 2 { cout << "double, int" << endl; } void foo(long a, long b) // 3 { cout << "long, long" << endl; } int main() { foo('a', 3.2); // int, int foo('a', 'b'); // int, int foo(3.2, 10L); // ambiguity error return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi overload resolution işleminde gösterici ve referanslardaki "üst düzey (top level) olmayan" const ve volatile niteleyicisi kaliteyi etkilemektedir. Her zaman kendi niteleyicine dönüştürme daha iyidir. Örneğin: void foo(const int *pi) // 1 { //... } void foo(int *pi) // 2 { //... } Aşağıdkai gibi bir çağrının yapıldığını varsayalım: const int a = 10; foo(&a); Burada 1 numaralı fonksiyon seçilecektir. Çünkü 2 numaralı fonksiyon zaten uygun bir fonksiyon değildir. Dolayısıyla üçüncü adıma zaten yalnızca birinci fonksiyon girmektedir. Çağrının şöyle yapıldığını varsayalım: int b = 10; foo(&b); Burada her iki fonksiyon da aday ve uygundur. Ancak int * -> int * dönüştürmesi ile int * -> const int * dönüştürmesi kıyaslandığında int * -> int * dönüştürmesi daha iyi kabul edilmektedir. Tabii aynı durum referanslar için de söz konusudur. Örneğin: void bar(int &r) // 1 { //... } void bar(const int &r) // 2 { //... } Çağrı şöyle yapılmış olsun: int a = 10; bar(a); Aynı gerekçeyle burada yine 1 numaralı fonksiyon daha iyidir. Çağrı şöyle yapılmış olsun: bar(10); Burada 1 numaralı fonksiyon yine uygun bir fonksiyon değildir. Dolayısıyla 2 numaralı fonksiyon seçilecektir. T1, T2 ve T3 birer tür belirtiyor olsun: T1 -> const T3 & T2 -> const T3 & Buradaki otomatik dönüştürmeler arasındaki kalite kıyaslaması referanslar dikkate alınmadan türlere bakılarak belirlenmektedir. Örneğin: int -> const int & int -> const double & Burada int -> const int & dönüştürmesi daha iyidir. Örneğin: char -> const int & char -> const double & Burada char -> const int & dönüştürmesi daha iyidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int &r) // 1 { cout << "int &" << endl; } void foo(const int &r) // 2 { cout << "const int &" << endl; } int main() { int a{10}; foo(a); // 2 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- T bir tür belirtmek üzere T ve T & parametrelerine sahip aynı isimli fonksiyonlar birlikte bulunabilir. Bunların imzaları farklıdır. Örneğin: void foo(int a) { //... } void foo(int &r) { //... } Ancak T bir belirtmek üzere T -> T dönüştürmesi ile T -> T & dönüştürme arasında bir kalite farklılığı yoktur. Her iki dönüştürme de "tam uyum (exact match)" kabul edilmektedir. Dolayısıyla aşağıdaki gibi bir çağrım geçerli değildir: int x = 10; foo(x); Burada her iki fonksiyon da "en uygun fonksiyon" durumundadır. T bir tür belirtmek üzere: T -> T T -> const T & dönüştürmeleri arasında da bir kalite farkı yoktur. Benzer biçimde: const T -> T const T -> const T 6 dönüştürmeleri arasında da bir kalite farklılığı yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int &r) // 1 { cout << "int &" << endl; } void foo(int a) // 2 { cout << "int" << endl; } int main() { int a = 10; foo(a); // ambiguity error! foo(10); // 2 seçilir çünkü 1 uygun fonksiyon değil return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Default argüman almış parametre değişkenleri "overload resolution" işleminin üçüncü adımında hiç dikkate alınmamaktadır. Örneğin: void foo(int a, int b = 10) { //... } void foo(int a) { //... } Bu iki fonksiyonun aynı anda bulunmasında bir sorun yoktur. Çünkü bunların parametrik yapıları farklıdır. Şimdi fonksiyonu şöyle çağırmış olalım: foo(10); Bu çağrım error ile sonuçlanacaktır. Çünkü birinci fonksiyonun default argüman alan parametreleri zaten overload resolution işleminin üçüncü adımında yokmuş gibi ele alınacaktır. Dolayısıyla burada her iki fonksiyon da en uygun fonksiyondur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İleride çokça karşılaşacağımız bir durum da sol taraf değeri referansı ve sağ taraf değeri referansı parametrelerine sahip aynı isimli fonksiyonlardır. Bunların birlikte bulunması bir soruna yol açmaz. Çünkü bunlar farklı türlerdendir ve overload edilebilirler. Örneğin: void foo(int &r) // 1 { //... } void foo(int &&r) // 2 { //... } Burada foo fonksiyonu bir sdol taraf değeri ile çağrılırsa 2 numaralı fonksiyon, bir sağ taraf değeri ilke çağrılırsa 1 numaralı fonksiyon uygun fonksiyon olmaz. Dolayısıyla bir "ambiguity" durumu yaşanmaz. Örneğin: int x = 10; foo(x); // 1 numaralı fonksiyon çağrılır foo(10); // 2 numaralı fonksiyon çağrılır Pekiyi sol taraf değeri referansı const olsaydı ve fonksiyon sağ taraf değeir ile çağrılsaydı ne olurdu? Örneğin: void foo(const int &r) // 1 { //... } void foo(int &&r) // 2 { //... } Burada fonksiyonun şöyle çağrıldığını varsayalım: foo(10); Her iki fonksiyon da uygun fonksiyonlardır. Ancak bu durumda sağ taraf değeri referansına sahip olan fonksiyonun daha iyi bir dönüştürme uyguladığı kabul edilmektedir. Dolayısıyla 2 numaralı fonksiyon seçilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aynı isimli sol taraf değeri referansı paranetresine ve sağ taraf deeri referansı parametresine sahip olan iki fonksiyonun bulunduğu bir durumda biz bu fonksiyonu sol taraf değeri ile çağrıdığımızda sol taraf değeir referans parametreli fonksiyonun çağrılacağını yukarıda söylemiştir. Örneğin: void foo(int &r) // 1 { //... } void foo(int &&r) // 2 { //... } Burada foo fonksiyonunu sol taraf değeri ile çağıralım: int x = 10; foo(x); Yukarıda da belirttiğimiz gibi zaten ikinci fonksiyon uygun fonksiyon olmadığı için birinci fonksiyon çağrılacaktır. Ancak ileride göreceğimiz bazı nedenlerden dolayı bazen programcılar bu sol taraf değeri ile sağ taraf değeri referans parametresine sahip fonksiyonun çağrılmasını isteyebilmektedir. Bu nasıl sağlanabilir? Sol taraf değerini paranteze almak bize bir fayda sağlamamaktadır. Çünkü C++'a göre bir sol taraf değerini paranteze alsak da o solf taraf değeri olmaya devam etmektedir. Örneğin: foo((x)); Burada yine birinci fonksiyon çağrılacaktır. int türüne dönüştümek geçici nesne oluşturacağı için fayda sağlayabilir. Örneğin: foo((int)x); // foo(static_cast(x)); Ancak bu genel bir çözüm değildir. Çünkü T bir tür ve t de o türden bir sol taraf değeri belirtmek üzere biz her tür için (T)t işlemini yapamayız. Sol taraf değerini sol taraf değeri referansına dönüştürürsek hiçbir fayda sağlayamayız. Örneğin: foo((int &)x); // foo(static_cast(x)); Burada yine bir numaralı fonksiyon çağrılacaktır. İşte bu işlemin en taşınabilir ve normalk yolu sol taraf değerini sağ taraf değeri referansına dönüştürmektir. Örneğin: foo((int &&)x); // static_cast(x)); Artık ikinci fonksiyon çağrılacaktır. Yukarıda da belirriğimiz gibi böyle bir gereksinim başka konularda karşımıza çıkacaktır. Bunu daha resmi olarak yapabilmek için C++'ın standart kütüphanesine C++11 ile birlikte move isimli bir fonksiyon eklenmiştir. move fonksiyonun bazı detrayları vardır. Ama yaptığı işlem bir sol taraf değerini sanki sağ taraf değeri gibi işleme sokmaktır. Örneğin: foo(move(x)); Burada ikinci fonksiyon çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 18. Ders 16/10/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Overload işlemi aynı faaliyet alanında yapılan bir işlemdir. Dolayısıyla "overload resolution" aynı faaliyet alanındaki aynı isimli fonksiyonlar arasında yürütülmektedir. Başka bir deyişle "verload resolution" işlemi için "aday fonksiyonlar (candidate functions)" isim aramsı sırasında ismin bulunduğu faaliyet alanındaki fonksiyonlardan oluşturulmaktadır. Özellikle sonraki maddede açıklayacak olduğumuz isim alanları ve daha sonra görecek olduğumuz sınıflar konusunda bunun önemi ortaya çıkacaktır. Aşağıdaki örnekte main fonksiyonu içerisinde foo fonksiyonu çağrılmıştır. foo fonksiyonu main fonksiyonunun yerel bloğunda bulunduğu için artık o yerel blokta bildirilen fonksiyonlar overload resolution işleminde aday fonksiyon olarak seçilecektir. Dolayısıyla double parametreye sahip olan foo aday fonksiyon olmadığı için overload resolution işlemine girmeyecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(double a) { cout << "double" << endl; } void foo(long a) { cout << "long" << endl; } void foo(int a) { cout << "int" << endl; } int main() { void foo(int a); foo(3.14); // int parametreli foo çağrılacak return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi farklı parametrik yapılara sahip aynı isimli fonksiyon tanımlamanın bize ne faydası vardır? Yani bu özellik C++'a neden eklenmiştir? NYPT birtakım anahtar kavramların bileşimi olarak ele alınabilir. Bu anahtar kavramların hepsi aslında uzun kodların daha kolay algılanmasına yönelik kavramlardır. İşte NYPT'te benzer işlemleri yapan ama aralarında küçük farklılıklar bulunan fonksiyonlara özellikle aynı isimler verilmektedir. Böylece "sanki farklı çok sayıda fonksiyon var" algısından "tek bir fonksiyon var" algısına geçiş yapılır. Zaten NYPT insanın doğayı algılayış biçiminden modellenmiştir. Örneğin çok farklı sandalyeler olabilir. Bunların renkleri yapıldığı malzemeler farklı olabilir. Ancak neticede bizim için hepsi oturulabilecek sandelyedir. Zaten doğada biribirinin aynısı olan şeyler genellikle bulunmamaktadır. Dikkatle incelendiğinde aynı marka ve renkteki sandalyeler arasında da farklılıklar olduğunu görürüz. Ancak bizim için o sandalyeler biribirinin aynısı gibidir. Görüldüğü gibi birtakım nesneleri farklılaştırmak algısal karışıklık yaratmaktadır. Benzer olanları aynıymış gibi algılamak ise algısal açıklık sağlamaktadır. Eğer bizim bilişsel sistemimiz farklı olan ancak biribirine benzeyenleri aynıymış gibi ele alamasydı dünya beynimizin işleyebileceğinden çok daha karmaşık bir hale gelirdi. İşte NYPT'de benzer işlemleri yapan ama ayrıntıda farklılar içeren fonksiyonlara aynı isimleri vermek iyi bir tekniktir. Örneğin: int abs(int a); long abs(long a); double abs(double a); ... Burada abs fonksiyonlarının hepsi benzer işlemleri yapmaktadır. Yalnızca işlem yaptıkları tür farklıdır. O halde bunlara aynı isimleri vermek iyi bir tekniktir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 42) C++'ta global "isim kirliliğini (name pollution)" engellemek için "isim alanları (namespaces)" denilen C'de olmayan bir özellik bulunmaktadır. Farklı firma ya da kurumların kütüphanelerinin bir arada kullanıldığı projelerde "isim çakışmaları (name collisions)" oluşabilmektedir. Örneğin A firmasının kütüphanesi ile B firmasının kütüphanesini birlikte kullanırken bu iki firma tesadüfen bir yapı ya da sınıfa aynı isimleri vermiş olabilirler. Bu önemli bir problemdir. Biz her iki firmanın kütüphanelerini kullanmak için onların başlık dosyalarını include ettiğimizde isim çakışması nedeniyle error'ler oluşacaktır. İşte isim alanları bu problemleri büyük ölçüde (tamamen değil) ortadan kaldırmak için düşünülmüştür. Örneğin A firmasının kütüphanesi ile B firmasının kütüphanesini birlikte kullanmak isteyelim. Bunun için iki firmanın oluşturduğu başlık dosyalarını include edelim: #include "a.hpp" #include "b.hpp" İki firma da tesadüfen bir yapıya aynı ismi vermiş olabilir. Bu durumda derleme sırasında error oluşacaktır. Bu error'leri pratik bir biçimde düzeltmek de mümkün değildir. Çünkü kütüphane derlenmiştir ve oradaki isimler object modüllere çoktan yazılmıştır. Yani çakışan isimleri başlık dosyasında değiştirmek bir fayda sağlamayacaktır. Bir isim alanı bildiriminin genel biçimi şöyledir: namespace { // namespace içindeki eleman bildirimleri } İsim alanları global bölgede bildirilirler. Yerel isim alanları oluşturmak mümkün değildir. Global alanda yapılabilen tüm bildirimler ve tanımlamalar isim alanları içerisinde de yapılabilmektedir. İsim alanları içerisindeki fonksiyonlar yine global fonksiyonlardır. İsim alanları içerisinde tanımlanmış olan değişkenler yine global değişkenlerdir. İsim alanları yalnızca isimleri farklılaştırma işlevini görmektedir. Her isim alanı farklı bir faaliyet alanı belirtmektedir. Aynı isim alanı içerisinde aynı isimli birden fazla değişken tanımlanamaz. Ancak farklı isim alanlarında aynı isimli değişkenler tanımlanabilmektedir. (Farklı isim alanlarındaki aynı isimli fonksiyonlar için "overload" terimi kullanılmaz. Çünkü "overload" aynı faaliyet alanındaki fonksiyonlar için kullanılmaktadır.) Yerel bir bloğun içerisinde isim alanı oluşturulamamaktadır. İsim alanları global bölgede oluşturulmak zorundadır. Bir isim alanı içerisindeki bir isme isim alanı ismi ve :: operatörü ile erişilir. :: operatörü iki operandlı araek bir operatördür. Bu operatöre "çözünürlük operatörü (scope resolution operator)" denilmektedir. Örneğin: CSD::foo(); gibi bir ifadede foo fonksiyonunun CSD isim alanı içerisinde olduğunu belirtmekteyiz. Çözünürlik operatörü aslında başka bağlamlarda da kullanılmaktadır. Burada çözünürlük operatörünün solundaki opendın isim alanı ismi, sağındaki operandın o isim alanı içerisindeki bir isim olduğuna dikkat ediniz. Çözünürlük operatörü ile belirtilen isimlere "niteliklendirilmiş (qualified)" isimler denilmektedir. Her kurum global isimlerini kendine özgü bir isim alanı içerisinde oluşturursa global isim kirliliği büyük ölçüde (ama tamamen değil) engellenmiş olmakradır. İsim alanına benzer benzer kavramlar pek çok yeni dilde de bulunmaktadır. Örneğin bu kavram Java'da "paket (package)", Python'da "modül (module)" ismiyle karşımıza çıkmaktadır. C# her ne kadar büyük ölçüde Java'dan kopya çekilmişse de kendini C++'a daha fazla yaklaştırmıştır. C# C++'ın isim alanlarını basitleştirerek bünyesine katmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace CSD { int a = 10; void foo() { cout << "CSD::foo" << endl; } } namespace Other { int a = 20; void foo() { cout << "Other::foo" << endl; } } int main() { cout << CSD::a << endl; cout << Other::a << endl; CSD::foo(); Other::foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İç içe (nested) isim alanları söz konusu olabilir. Bu durumda içteki isim alanınının elemanlarına dışarıdan birden fazla çözünürlük operatörü kullanılarak erilişir. Örneğin: namespace CSD { namespace Util { void foo() { cout << "CSD::Util::foo" << endl; } } void foo() { cout << "CSD::foo" << endl; } } int main() { CSD::foo(); CSD::Util::foo(); return 0; } Birbirlerini kapsayan isim alanlarında da aynı isimli değişkenler bulunabilirler. Bunlar birbirleriyle karışmaz. Bu örnekteki foo fonksiyonlarından biri doğrudan CSD isim alanı içerisinde diğeri ise CSD isim alanı içerisindeki Util isim alanı ieçrisindedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace CSD { namespace Util { void foo() { cout << "CSD::Util::foo" << endl; } } void foo() { cout << "CSD::foo" << endl; } } int main() { CSD::foo(); CSD::Util::foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İç bir isim alanı C++17 ile birlikte artık hamlede de bildirilebilmektedir. Örneğin: namespace A::B::C { void foo() { //... } } Burada foo fonksiyonu A isim alanının içerisindeki B isim alanının içerisindeki C isim alanının içerisine yazılmıştır. Yani aşağıdaki bildirim yukarıdaki ile eşdeğerdir: namespace A { namespace B { namespace C { void foo() { //... } } } } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace A::B::C { void foo() { cout << "A::B::C::foo" << endl; } } int main() { A::B::C::foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir isim alanı içerisindeki ismi dışarıdan çözünürlük operatörü ile isim alanı ismini belirterek kullanabiliyorduk. Ancak aynı isim alanı içerisindeki bir isim doğrudan kullanılabilmektedir. Örneğin: namespace CSD { int g_x; void foo() { cout << g_x << endl; // geçerli } } Burada foo fonksiyonu g_x değişkenini hiç niteliklendirmeden doğrudan kullanabilmektedir. Çünkü foo fonksiyonunun kendisi de aynı isim alanı içeisindedir. Niteliksiz isim arama (unqualified name lookup) sırasında isim alanlarına içten dışa doğru bakılmaktadır. Dolayısıyla iç bir isim alanı onun dışındaki isimleri de niteliklendirmeden kullanabilir. İsim aramasının (name lookup) ayrıntıları ileride başka bir bölümde ele alınacaktır. Örneğin: namespace CSD { int g_x; namespace Util { void foo() { cout << g_x << endl; // geçerli } } } Burada g_x değişkeni Util isim alanı içerisinde değildir, util isim alanını kapsayan isim alanı ieçrisindedir. Ancak g_x aranırken isim alanlarına içten dışa doğru bakıldığı için g_x bulunacaktır. İsim alanları ayrı faaliyet alanları belirtmektedir. Faaliyet alanı kuralına niteliksiz aramalarda göre dar faaliyet alanındaki isme erişilir. Örneğin: namespace CSD { int g_x; namespace Util { int g_x; void foo() { cout << g_x << endl; // Buradaki g_x kendi isim alanındaki g_x. CSD::g_x görünür (visible) değil } } } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aynı isim alanını birden fazla kez bildirmek tamamen geçerli bir durumdur. Bu durum isim alanına ekleme yapıldığı anlamına gelir. Örneğin: namespace CSD { void foo() { //... } } //... namespace CSD { void bar() { //... } } Burada foo ve bar fonksiyonlarının her ikisi de CSD isim alanı içerisindedir. Yani isim alanları tek parça olarak yazılmak zorunda değildir. Tabii aynı isim alanı içerisinde aynı isimli tek bir değişken tanımlamabilir. Örneğin: namespace CSD { int g_x; } //... namespace CSD { int g_x; // geçersiz! aynı isim alanında aynı isimli tek bir değişken tanımlanabilir } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace A { void foo() { cout << "A::foo" << endl; } } namespace A // geçerli { void bar() { cout << "A::bar" << endl; } } int main() { A::foo(); A::bar(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tabii farklı isim alanlarının içerisindeki aynı isim alanları aslında farklı isim alanlarıdır. Burada ekleme işlemi söz konusu değildir. Örneğin: namespace A { namespace B { //... } } namespace B { //... } Buradaki B ismindeki iki isim alanı tamamen farklı isim alanıdır. Dolayısıyla birleştirilmezler. Aynı isim alanlarının içindeki aynı isimli isim alanları birleştirilmektedir. Örneğin: namespace A::B { void foo() { cout << "A::B::foo" << endl; } } namespace A { namespace B { void bar() { cout << "A::B::bar" << endl; } } } Burada B isim alanları birleştirilir. Çünkü her iki B de A isim alanı içerisindedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace CSD { namespace A { void foo() { cout << "A::foo" << endl; } } } namespace A { void bar() { cout << "A::bar" << endl; } } int main() { CSD::A::foo(); A::bar(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Hiçbir isim alanının içerisinde olmayan global bölge de bir isim alanı belirtmektedir. Bu bölgeye "global isim alanı (global namespace)" denilmektedir. Örneğin: namespace CSD { void foo() { //... } } void bar() { //... } int main() { //... } Burada bar fonksiyonu global isim alanı içerisindedir. CSD isim alanı da global isim alanı içerisindedir. Her isim alanınının doğrudan ya da dolaylı olarak global isim alanı içerisinde olduğuna dikkat ediniz. main fonksiyonu da global isim alanı içerisindedir. İsim alanlarını bir dizin (directory) ağacına benzetirsek global isim alanını kök dizine benzetebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace CSD { void foo() // global isim alanı içerisindeki CSD isim alanının içerisinde { cout << "A::foo" << endl; } } void foo() // global isim alanının içersinde { cout << "foo" << endl; } int main() { CSD::foo(); // CSD'nin içerisindeki foo çağrılıyor foo(); // global isim alanı içerisindeki foo çağrılıyor return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyonun prototipi bir isim alanı içerisinde bildirilip tanımlaması onu kapsayan herhangi bir isim alanında niteliklendirilerek yapılabilir. Örneğin: namespace CSD { void foo(); //... } void CSD::foo() // geçerli { //... } Burada foo fonksiyonu aslında CSD isim alanı içerisindedir. Ancak tanımlaması global isim alanında yapılmıştır. Tanımlama sırasında foo isminin nitelikli bir biçimde belirtildiğine dikkat ediniz. Örneğin: namespace CSD { namespace Util { void foo(); //... } void Util::foo() // geçerli { //... } } Burada prototip bildirimi CSD::Util içerisinde, tanımlama ise onu kapsayan CSD isim alanı alnı içerisinde yapılmıştır. Tabii tanımlamanın hemen bir yukarıdaki isim alanı içerisinde yapılması zorunlu değildir. Kapsayan herhangi bir isim alanı içerisinde niteliklendirmelerle de yapılabilir. Örneğin: namespace CSD { namespace Util { void foo(); //... } //... } void CSD::Util::foo() // geçerli { //... } Global isim alanının CSD isim alanını kapsadığına dikkat ediniz. Ancak prototip de tanımalamada kapsayan isim alanında yapılamaz. En azından prototipin kendi isim alanınde bildirilmesi gerekmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace A { namespace B { void foo(); void bar(); void tar(); //... void foo() { cout << "A::B::foo" << endl; } } void B::bar() { cout << "A::B::bar" << endl; } } void A::B::tar() { cout << "A::B::tar" << endl; } int main() { A::B::foo(); A::B::bar(); A::B::tar(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çözünürlük operatörünün tek operandlı (unary) biçimi de vardır. Tek operand'lı kullanım şöyledir: :: Çözünürlük operatörünün tek operand'lı biçimi isim aramasını global isim alanında yapmaktadır. Örneğin: ::foo(); Burada global isim alanındaki foo çağrılmıştır. Eğer isim global isim alanında bulunamazsa başka bir isim alanına bakılmaz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo() { cout << "foo" << endl; } namespace CSD { void foo() { cout << "CSD::foo" << endl; } void bar() { foo(); // CSD::foo ::foo(); // global isim alanındaki foo } } int main() { CSD::bar(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyon çağrıldığında önce isim araması yapılır. İsim bulunursa ismin bulunduğu faaliyet alanındaki aynı isimli fonksiyonlar aday fonksiyon olarak overload resolution işlemine sokulur. Daha üst isim alanlarındaki fonksiyonlar aday fonksiyon olarak seçilmezler. Yani overload işlemi aynı isim alanındaki aynı isimli fonksiyonların bir arada bulunması anlamına gelmektedir. Örneğin: void foo(double a) { cout << "::foo, int" << endl; } void foo(long a) { cout << "::foo, long" << endl; } namespace CSD { void foo(int a) { cout << "CSD::foo, int" << endl; } void bar() { foo(3.2); // CSD::foo, int çağrılır } } Burada CSD::bar fonksiyonunda foo fonksiyonu double bir argümanla çağrılmıştır. Niteliksiz isim aramada CSD içerisindeki foo fonksiyonu bulunacaktır. İşte artık aday fonksiyonlar CSD isim alanı içerisindeki foo fonksiyonları olacaktır. Bu örnekte global isim alanı içerisinde "tam uyum (exact match)" sağlayan bir foo fonksiyonu olsa da o fonksiyonun çağrılmayacağına dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace A { void foo(const char *str) { cout << "A::foo const char *" << endl; } namespace B { void foo(double a) { cout << "A::B::foo double" << endl; } namespace C { void foo(int a) { cout << "A::B::C::foo int" << endl; } void bar() { foo(12.3); // A::B::C::foo çağrılıyor foo("ankara"); // geçersiz! } } } } int main() { A::B::C::bar(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 19. Ders 18/10/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın standart kütüphanesi içerisindeki ve C++'ta C'nin standart kütüphanesi içerisindeki tüm öğeler "std" isimli bir isim alanı içerisine yerleştirilmiştir. Bu nedenle bu isimler kullanılırken isimlerin "std" ismi ile niteliklendirilmesi gerekir. Aksi takdirde isimler global isim alanında aranır ve o isim alanında bulunamayacaklardır. Örneğin: std::cout << a << std::endl; Biz şimdiye kadar böyle bir niteliklendirme yapmadık. Bunun nedeni programın yukarısına using namespace denilen direktifi yerleştirmiş olmamızdır. using namespace std; Bu direktif sayesinde std isim alanındaki isimleri kullanırken niteliklendirmeyi elimine etmekteyiz. Ancak bazı programcılar bu direktifi kullanmazlar ve std isim alanındaki isimlere nitelikli bir biçimde erişirler. Böyle kodlçar görürseniz yadırgamamalısınız. Biz kursumunda gebel olarak bu direktiften faydalanacağız. Bu direktifin işlevi izleyen paragraflarda ele alınmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include int main() { std::cout << "this is a test" << std::endl; std::printf("this is a test\n"); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Farklı isim alanları içerisindeki aynı değişkenlerin birbirleriyle karışmaması nasıl sağlanmaktadır. Aslında amaç dosya (object file) formatlarında isim alanı biçiminde bir kavram yoktur. Yani isim alanları C++'ın bir kabulüdür. Aslında C Programala Dili C++ Programlama Dilinden daha doğaldır. Burada "doğallık" makinede olanları daha doğrudan ifade etme ve ona yakınlık anlamında kullanılmaktadır. C++ C'den yüksek seviyeli bir programlama dilidir. Örneğin bir şey "C'de böyle ancak C++'ta şöyle yapılıyorsa" gerçekte C'deki gibi yapılmaktadır. İşlemciler C'deki gibi çalışmaktadır. C++'taki pek çok kavram NYPT'yi uygulamak dile eklenmiş yapay kavramlardır. Örneğin amaç kod düzeyinde isim alanı diye bir kavram yoktur. Sınıf diye de bir kavram yoktur. Derleyiciler isim alanları içerisindeki isimleri amaç dosyaya (object file) isim alanı isimleriyle kombine ederek yazmaktadır. Bu nedenle farklı isim alanlarının içerisindeki aynı isimler aslında amaç dosyada farklı isimler gibi bulurlar. Tabii daha önceden de belirttiğimiz gibi isim dekorasyonu (name decoration) derleyiciden derleyiciye değişebilmektedir. Bu nedenle bir derleyicide oluşturulmnuş bir amaç dosya diğer derleyicide kullanılamamaktadır. Örneğin;: namespace CSD { namespace Util { int x; } } Buradaki x ismini Microsoft derleyicileri amaç dosyaya şöyle yazmaktadır: ?x@Util@CSD@@3HA Aynı ismi g++ derleyicisi amaç dosyaya şöyle yazmıştır: ZN3CSD4Util1xE --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyonun başka bir modülde (yani başka bir C++ kaynak dosyasında) tanımlanmış olduğunu ya da kütüphane içerisinde bulunduğunu varsayalım. Bi bu fonksiyonu başka bir modülden (C++ kaynak dosyasından) kullanmak istiyorsak onun prototipini bulundurmamız gerekir. Fonksiyonun kendisi link aşamasında linker tarafından bulunmaktadır. Tabii bu prototip bildiriminin aynı isim alanı içerisinde yapılması gerekmktedir. Örneğin: namespace CSD { void foo(); //... } Buraki fonksiyonlar muhtemelen başka bir kaynak dosya içerisinde CSD alanında tanımlanmıştır. Bizim bu isimleri başka bir kaynak dosyadan kullanabilmemiz için prototip bildirimini de aynı isim alanı içerisinde yapmalıyız. Bunun yukarıdakinden daha bir prtaik bir yolu yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İsim alanı bildirimi yapılırken namespace anahtar sözcüğünden sonra isim alanı ismi belirtilmeden doğrudan blok açılırsa böyle isim alanlarına "isimsiz isim alanları (unnamed namespace)" denilmektedir. Örneğin: namespace { int g_x; void foo() { //... } } Burada g_x ve foo isimsiz isim alanı içerisindedir. İsimsiz isim alanlarındaki isimler o isimsiz isim alanı hangi isim alanı içerisinde bildirilmişse sanki o isim alanındaki isimler gibi kullanılabilmektedir. Yukarıdaki örneğimizde isimsiz isim alanı global isim alanı içerisinde bildirilmiştir. (Genellikle hep böyle yapılır.) o halde biz g_x ismini ve foo ismini sanki global isim alanındaymış gibi kullanabiliriz. Örneğin: #include using namespace std; namespace { int g_x; void foo() { cout << "foo" << endl; } } int main() { g_x = 10; // geçerli foo(); // geçerli cout << g_x << endl; // geçerli return 0; } Pekiyi isimsiz isim alanları neden kullanılmaktadır? İsimsiz isim alanlarını gören derleyici aslında bu isim alanlarına kendisi "tek (unique)" olan bir isim uydurmaktadır. Yani örneğin: namespace { //... } gibi bir isimsiz isim lanaı bildirimi aşağıdaki ile eşdeğerdir: namespace compiler_generated_unique_named { //... } using namespace compiler_generated_unique_named; Yani aslında isimsiz isim alanı içerisindeki isimler amaç dosyaya derleyici tarafından üretilmiş olan bir isim alanı ismi eşliğinde yazılmaktadır. Dolayısıyla farklı C++ kaynak dosyalarındaki isimsiz isim alanları içerisindeki aynı isimler karışmamaktadır. Çünkü derleyici her kaynak dosyadaki isimsiz isim alanını diğerlerinden farklı olacak bir isim ile kombine ederek amaç dosyaya yazmaktadır. İşte isimsiz isim alanları "modüle özgü global değişken oluşturabilmek" için kullanılmaktadır. Farklı modüllerdeki isimsiz isim alanları içerisinde aynı isimli değişkenler bulunabilir. Anımsanacağı gibi modüle özgü global değişkenler C'de "static" anahtar sözcüğü ile "static global" değişkenler olarak oluşturulmaktadır. C++'ta her ne kadar static global değişken bildirimi yine varsa da bu yöntem "modüle özgü global değişken oluşturmak için kötü bir teknik"" olarak kabul edilmektedir. static global değişkenler yerine C++'ta "isimsiz isim alanları (unnamed namespaces)" kullanılmalıdır. Tabii derleyiciler isimsiz isim alanlarının içerisindeki değişkenleri aslında amaç dosyaya PUBLIC olarak yazmazlar. Yani tıpkı static global değişkenlerde olduğu gibi bu değişkenler gerçekten ismi biliniyor olsa bile başka bir modülden extern bildirimi ile kullanılamamaktadır. Başka bir deyişle aslında isimsiz isim alanınındaki isimlerin static belirleyicisi ile oluşturulmuş isimlerden link işlemi bakımından bir farkı yoktur. İsimsiz isim alanları başka isim alanlarının içerisinde de olabilmektedir. Örneğin: namespace CSD { namespace { int g_x; //... } } Buradaki g_x ismi sanki CSD isim alanındaymış gibi kullanılabilir. Örneğin biz bu ismi CSD::g_x biçiminde kullanabiliriz. Tabii bu isimlerin de başka bir modülden (C++ kaynak dosyasından) kullanılma olanağı yoktur. Yukarıdaki bildirimin eşdeğerini şöyle düşnebilirsiniz: namespace CSD { namespace compiler_generated_unique_name { int g_x; //... } using namespace compiler_generated_unique_name; } using namespace direktifi izleyen paragraflarda ele alınmaktadır. Tabii aynı isim alanı içerisindeki isimsiz isim alanları yine tek bir isimsiz isim alanı biçiminde birleştirilmektedir. Başka bir deyişle biz isimsiz isim alanlarını da tek parça olarak yazmak zorunda değiliz. İsimsiz isim alanı içerisindeki değişkenlerle aynı isimli üst isim alanında değişkenler bulunabilir. Bu durum bir soruna yol açmaz. Örneğin: namespace { int g_x = 10; //... } int g_x; Ancak bu örnekte her iki g_x değişkeni de isim aramsı sırasında sanki global isim alanındaymış gibi bir etki oluşacağına göre bu ismin niteliksiz kullanımı soruna yol açacaktır. Örneğin: cout << g_x << endl; // geçersiz! hangi g_x olduğu belli değil Tabii global olana erişmek için tek operand'lı çözünürlük operatöründen faydalanabiliriz. Örneğin: cout << ::g_x << endl; // geçerli, global isim alanındaki g_c kullanılıyor. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte inline isim alanları biçiminde bir isim alanı türü de dile eklenmiştir. inline isim alanları herhangi bir isim alanının içerisinde bildirilebilir. Bildirim sırasında inline anahtar sözcüğü de kullanılmaktadır. Örneğin: inline namespace CSD { //... } inline anahtar sözcüğünün fonksiyonlarla kullanımını görmüştük. Ancak buradaki inline anahtar sözcüğünün inline fonksiyonlardaki inline anahtar sözcüğü ile semantik bir benzerliği yoktur. inline isim alanı içerisindeki isimler inline alanı hangi isim alanının içerisinde bildirilmişse sanki o isim alanındaki isimlermiş gibi kullanılabilmektedir. (Anımsanacağı gibi isimsiz isim alanlarındaki isimler de bu biçimde kullanılabiliyordu) Örneğin: inline namespace CSD { int g_x; void foo() { cout << "CSD" << endl; } } Biz burada g_xx ve foo isimlerini inline CSD isim alanı içerisinde bildirdik. CSD isim alanı global isim alanı içerisinde olduğu için sanki bu isimler global isim alanı içerisindeymiş gibi de kullanılabilmektedir. Örneğin: g_x = 10; // geçerli, niteliklebdirmeye gerek yok cout << g_x << endl; // geçerli, niteliklendirmeye gerek yok foo(); // geçerli, niteliklendirmeye gerek yok Tabii istersek yine bu isim alanındaki isimleri nitelikli de kullanbiliriz: CSD::g_x = 10; // geçerli, açıkça niteliklendirme yapılmış cout << CSD::g_x << endl; // geçerli, açıkça niteliklendirme yapılmış CSD::foo(); // geçerli, açıkça niteliklendirme yapılmış Pekiyi bunun anlamı nedir? Aslında inline isim alanları o kadar sık kullanılabilecek bir özellik değildir. Özellikle kütüphanelerin farklı versiyonlarının kolay bir biçimde default hale getirilmesi için düşünülmüştür. Örneğin: namespace CSD { namespace utilV1 { void foo() { //... } void bar() { //... } } inline namespace utilV2 { void foo() { //... } void bar() { //... } } //... } Burada aslında CSD isim alanı içerisinde foo ve bar fonksiyonları bulunmaktaymış. Ancak zamanla bu fonksiyonların ileri versiyonları oluşturulmuş. Programcı da eski versiyonları muhafaze ederek yeni versyionları default hale getirmek istemiş olabilir. Burada UtilV2 içerisindeki isimler sanki CSD içerisindeymiş gibi bir etki oluşmaktadır. Daha ileride bu kütüphanein üçüncü versiyonu çıkartıldığnda programcı muhtemelen şöyle yapacaktır: namespace CSD { namespace utilV1 { void foo() { //... } void bar() { //... } } namespace utilV2 { void foo() { //... } void bar() { //... } } inline namespace utilV3 { void foo() { //... } void bar() { //... } } //... } Eski versiyonlara yine erişilebildiğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 20. Ders 23/10/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 43) Gereksiz niteliklendirmeyi elimine edebilmek için "using namespace direktifi" denilen bir direktiften faydalanılmaktadır. using namespace direktifinin genel biçimi şöyledir: using namespace ; Burada isim alanı ismi tek bir isimden olaşabileceği gibi :: operatörü kullanılarak oluşturulan niteliklendirilmiş bir isim alanı ismi de olabilir. Örneğin: using namespace CSD; using namespace CSD::Util::Test using namespace std; using namespace direktiflerinin ayrı ayrı oluşturulması gerekmektedir. Aşağıdaki gibi bir arada oluşturulamamaktadır: using namespace CSD, CSD::Util, std; // geçersiz! böyle bir sentaks yok Bu direktiflerin aşağıdaki gibi ayrı ayrı oluşturulması gerekir: using namespace CSD; using namespace CSD::Util; using namespace std; using namespace direktifi bir isim alanına ya da yerel bir bloğa yerleştirilebilir (bir sınıf içerisine yerleştirilemez). Tabii using namespace direktifinde belirtilen isim alanının daha önce derleyici tarafından görülmüş olması gerekmektedir. Yani önce isim alanı bildirimi yapılmalı daha sonra o isim alanı için using namespace direktifi kullanılmalıdır. Olmayan bir isim alanına using namespace direktifi uygulanamaz. using namespace direktifini ele almadan önce "isim araması (namelookup)" denilen bir kavramdan kısaca bahsedeceğiz. İsim araması kavramı C standratlarında olmayan C++'ın karmaşıklığı nedeniyle C++ standartlarında bulunan bir kavramdır. Derleyici bir isim ile karşılaştığında o isme ilişkin bir bildirimi bulmaya çalışır. Çünkü her ismin bir bildirimi olmak zorundadır. İşte derleyici isimleri sırasıyla bazı faaliyet alanlarında aramaktadır. İsim bir faaliyet alanında bulunursa başka bir faaliyet alanına bakılmamaktadır. Yani arama isim bulunamadığı sürece devam ettirilmektedir. Örneğin C'de (her ne kadar bu kavram C'de yoksa da) yerel bir blokta bir isim kullanıldığında önce o ismin bildirimi içten dışa doğru yerel bloklarda aranmaktadır. Nihayet bu yerel bloklarda bulunamazsa global alana da bakılmaktadır. C++'ta isim araması iki grubua ayrılmaktadır: 1) Niteliksiz isim araması (unqualified name lookup) 2) Nitelikli isim arama (qualified name lookup) Çözünürlük operatörü olmadan yazılmış isimlerin aranması ve çözünürlük operatörlerinin en solundaki isimlerin aranması niteliksiz isim arama kurallarına göre yapılmaktadır. Örneğin: a = 10; CSD::Util::foo(); Burada a ve CSD isimleri niteliksiz isim arama kurallarına göre aranmaktadır. Çözünürlük operatörünün sağındaki isimler nitelikli isim arama kurallarına göre aranmaktadır. Örneğin: std::cout << "test" << std::endl; Burada std niteliksiz isim arama kurallarına göre cout ve endl nitelikli isim arama kurallarına göre aranmaktadır. İsim araması biraz karmaşık bir konudur. Çünkü ileride görülecek başka konular da devreye girdiğinde konu biraz da ayrıntılı hale gelmektedir. Ancak biz bu aşamada en basit bir biçimde diğer konuları hiç dikkate almadan isim araması konusunda bazı şeyler söylmek istiyoruz. Niteliksiz isim araması kabaca şu aşamalarla yapılmaktadır (else-if biçiminde): 1) İsim kullanıldığı bloğun içerisinde kullanım yerine kadar olan yerel bölgede aranır. 2) İsim yerel bloğu kapsayan yerel bloklarda içten dışa doğru değişkenin kullanıldığı bölgede sırasyla aranır. 3) İsim yerel bloğun ilişkin olduğu fonksiyonun içinde bulunduğu isim alanında kullanım yerine kadarki bölgede aranır. 4) İsim kapsayan isim alanlarında içten dışa doğru kullanım yerine kadarki bölgede aranır. 5) İsim nihayet global isim alanında kullanım yerine kadarki bölgede aranır. Niteliksiz isim aramasını şuna benzetebiliriz: "Biz arkadaşımıza Ahmet'i gördün mü?" diye bir soru soralım. Arkadaşımız Ahmet'i önce arkadaş çevresinde, bulamazsa okulda, bulamazsa şehirde, bulamazsa ülkede, bulamazsa dünyada bulamazsa evrende arayacaktır. Nitelikli isim aramada isim kabaca (ayrıntıları vardır) ilgili isim alanında aranmaktadır. Eğer isim o isim alanında bulunamazsa kapsayan isim alanlarına bakılmamaktadır. Örneğin: CSD::a = 10; Burada a değişkeni CSD isim alanında aranır. Eğer orada bulunmazsa CSD'yi kapsayan isim alanlarında arama yapılmamaktadır. Nitelikli isim aramayı da şuna benzetebiliriz: Biz arkdaşımıza "Okuldaki Ahmet'i gördün mü?" diye sorsaydık. Arkadaşımız Ahmet'i yalnızca okulda arardı. Okulda bulmazsa şehre bakmazdı. Çünkü biz zaten onun okulda olması gerektiğini söylemiş olmaktayız. using namepspace direktifinde iki isim alanı söz konusudur. Birincisi direktifin yerleştirildiği isim alanı, ikincisi direktifte belirtilen isim alanı. Eğer direktif bir yerel bloğa yerleştirilmişse direktifin yerleştirildiği isim alanı o yerel bloğu kapsayan isim alanıdır. Derleyici using namespace direktifini gördüğünde önce direktifin yerleştirildiği isim alanı ile direktifte belirtilen isim alanını kapsayan en dar isim alanını tespit eder. Örneğin: namespace A { namespace B { //... } using namespace B; //... } Burada direktifin yerlşetirildiği isim alanı A'dır. Direktifte belirtilen isim alanı ise B'dir. İkisini kapsayan en dar isim alanı A'dır. Örneğin: using namespace std; Burada direktifin yerleştirildiği isim alanı global isim alanıdır. Direktifte belirtilen isim alanı ise std isim alanıdır. İkisini de kapsayan en dar isim alanı global isim alanıdır. Örneğin: namespace A { namespace B { namespace C { //... } } using namespace B::C; //... } Burada direktifin yerleştirildiği isim alanı A, direktifte belirtilen isim alanı A::B::C'dir. İki isim alanını kapsayan en dar isim alanı A'dır. using direktifi şöyle etki göstermektedir: Niteliksiz isim araması (unqualified name lokkup) sırasında sanki direktifte belirtilen isim alanının içerisindekiler direktifin yerleştirildiği ve direktifte belirtilen isim alanını kapsayan en dar isim alanına enjekte edilmiş gibi olmaktadır. Ancak bu enjekte edilme niteliksiz isim aramaları (unqualified name lookup) sırasında (yani doğrudan yazılan isimlerin aranması sırasında) ve yalnızca direktifin yerleştirildiği faaliyet alanında etkili olmaktadır. Örneğin: #include using namespace std; int main() { cout << "this is a test" << endl; return 0; } Burada cout ve endl isimleri aranırken sanki std isim alanı içerisindeki her şey global isim alanındaymış gibi bir etki oluşmaktadır. Böylece cout ve endl isimleri aslında std isim alanında olduğu halde derleyici tarafından global isim alanında bulunacaktır. Tabii niteliksiz isim araması sırasında aranan isim using direktifinin yerleştirildiği isim alanı ile direktifte belirtilen isim alanını kapsayan en dar isim alanına kadar bulunursa zaten direktifin bir etkisi kalmamaktadır. Örneğin: namespace CSD { int a; } using namespace CSD; int main() { int a; a = 10; // yerel a, CSD isim alanındaki a değil return 0; } Burada main fonksiyonunun içerisindeki a ismi (a = 10'daki a ismi) zaten yerel blokta bulunacaktır. Dolayısıyla CSD içerisndeki a sanki global isim alanı içerisindeymiş gibi oluşan etki burada bir fayda sağlamayacaktır. using namespace direktifinin etki göstermesi için niteliksiz isimlerin aranmasında direktifin yerleştirildiği isim alanına baklıyor olması gerekmektedir. Aksi takdirde bu direktif ilgili ismin aranmasında bir etki göstermemektedir. Örneğin: namespace A { namespace B { namespace C { using namespace std; void foo() { cout << "A::B::C::foo" << endl; // geçerli } } } } int main() { cout << "this is a test" << endl; // error! return 0; } Burada A::B::C içerisine yerleştirilmiş olan using namespace direktifi main fonksiyonundaki isimlerin aranmasında etkili olmaz. Çünkü main fonksiyonundaki isimlerin aranması sırasında using namespace direktifi görülmemektedir. Örneğin: using namespace std; namespace A { namespace B { namespace C { void foo() { cout << "A::B::C::foo" << endl; // geçerli } } } } int main() { cout << "this is a test" << endl; // geçerli return 0; } Artık hem A::B::C::foo içerisindeki hem de main içerisindeki isimlerin aranmasında direktif etkili olacaktır. using namespace direktifi bir yerel bloğa yerleştirilirse yalnızca o yerel bloktaki niteliksiz aramalarda direktif görüleceği için etki yalnızca o yerel blokta oluşacaktır. Örneğin: int main() { using namespace std; cout << "this is a test" << endl; // geçerli return 0; } void foo() { cout << "this is a test" << endl; // geçersiz' } main fonksiyonu içerisindeki direktifin foo fonksiyonunda bir etkisi yoktur. İsim hem direktifte belirtilen isim alanında hem de direktifin yerleştirildiği isim alanı ile direktifte belirtilen isim alanını kapsayan en dar isim alanında bulunursa bu durum geçersizdir. Örneğin: namespace CSD { int a = 10; } int a = 20; int main() { using namespace CSD; cout << a << endl; // ambigous? Hangi a? return 0; } Tabii bu örnekte eğer isim global isim alanına kadar isim araması sırasında bulunsaydı herhangi bir sorun oluşmayacaktı. Örneğin: #include using namespace std; namespace CSD { int a = 10; } int a = 20; int main() { int a = 100; using namespace CSD; cout << a << endl; // main fonksiyonundaki a return 0; } Burada CSD içerisindeki a sanki global isim alanındaymış gibi etki göstermektedir. Oysa global isim alanında da bir a değişkeni bulunmaktadır. Bu durumda isim araması başarısz olacaktır. Benzer biçimde isim birden fazla using namespace direktifi ile belirtilen isim alanında bulunyorsa bu durumda da isim araması başarısız olur. Örneğin: namespace A { int x; int y; } namespace B { int x; int z; } using namespace A; using namespace B; int main() { x = 10; // geçersiz! hangi x? y = 20; // sorun yok, A::y nalaşılır z = 30; // sorun yok B::z anlaşılır return 0; } Tabii burada using namespace direktiflerinin yerleştirilmesinde bir sorun yoktur. Sorun a isminin niteliksiz biçimde kullanılması sırasında oluşmaktadır. Bu tür durumlarda çakışma olmadığı sürece isimleri niteliksiz bir biçimde, çakışma durumda da nitelikli bir biçimde kullanabiliriz. Örneğimizde x değişkenini niteliksiz kullanamadığımıza göre A::x ve B::x biçiminde nitelikli kullanma yoluna gidebiliriz. Niteliksiz isim aramsı sırasında using namespace direkifi ile bleirtilen isim alanında da using namespace direktifi varsa etki geçişli olarak devam eder. Örneğin: namespace A { int x; //... } namespace B { using namespace A; int y; //... } using namespace B; int main() { x = 10; // geçerli return 0; } Burada etki şöyle düşünülmelidir. "using namespace B" direktifi ileB isim alanı içerisindeki isimler global isim alanına enjekte edilmiş gibi olmaktadır. Böylece isim alanındaki "using namespace A" direktifi de sanki global isim alanındaymış gibi etkiye sahip olacaktır. Bu etki de A isim alanı içerisindekilerin global isim alanına enjekte edilmesine yol açacaktır. Oveload resolution işlemi sırasında aday fonksiyonların seçilmesinde using namespace direktifi etkili olmaktadır. Aşağıdaki örneği inceleyiniz: using namespace std; namespace A { void foo(int a) { cout << "A::foo(int)" << endl; } } namespace B { using namespace A; void foo(long a) { cout << "B::foo(long)" << endl; } } void foo(double a) { cout << "::foo(double)" << endl; } using namespace B; int main() { foo(10); // A::foo çağrılır foo(10L); // B::foo çağrılır foo(3.14); // ::foo çağrılacak return 0; } Burada tüm foo fonksiyonları aslında global isim alanındaymış gibi düşünülmelidir. Dolayısıyla bu fonksiyonların hepsi aday fonksiyon olarak seçilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Nitelikli isim araması sırasında yalnızca belirtilen isim alanına bakıldığına dikkat ediniz. Örneğin: int a = 10; namespace CSD { int b = 20; //... } int main() { CSD::a = 10; // geçersiz! return 0; } Burada CSD::a ifadesindeki a ismi nilikli bir biçimde yalnızca CSD isim alanında aranacaktır. Eğer orada bulunmazsa kapsayan isim alanlarına bakılmayacaktır. Nitelikli isim araması sırasında "isim ilgili isim alanında bulunamazsa" o isim alanındaki using namespace direktiflerinde belirtilen isim alanlarında da nitelikli arama yapılmaktadır. (Burada en dar isim alanı tanımı kullanılmamaktadır.) Örneğin: namespace A { int x = 10; //... } namespace B { using namespace A; int y = 20; //... } int main() { cout << B::x << endl; // geçerli A::x anlaşılır return 0; } Burada B::x ifadesindeki x ismi B isim alanında bulunamadığı için A isim alanına da bakılacak ve orada bulunacaktır. Eğer isim ilgili isim alanında bulunsaydı o isim alanındaki using namespace direktifleri etkili olmayacaktı. Örneğin: namespace A { int x = 10; //... } namespace B { using namespace A; int y = 20; int x = 30; //... } int main() { cout << B::x << endl; // geçerli, B::x anlaşılır return 0; } Burada B::x ifadesindeki x ismi B'de bulunduğu için A'da aranmayacaktır. Aşağıdaki örneği inceleyiniz: namespace A { int x = 10; //... } namespace B { using namespace A; int x = 20; //... } namespace C { using namespace B; //... } int main() { cout << C::x << endl; // geçerli, B::x anlaşılır return 0; } Buradaki C::x ifadesinde x'in aranması sırasında bir sorun oluşmayacaktır. Çünkü standartlara göre isim C isim alanında bulunamadığında sanki B::x gibi aranacaktır. Dolayısıyla isim B isim alanında bulunduğu için sorun çıkmayacaktır. Aşağıdaki örnekte ise C::x ifadesindeki x birden fazla isim alanında buulunacağı için isim araması başarısız olacaktır. Bu tür durumlarda using namespace direktiflerinin sırasının hiçbir önemi yoktur: namespace A { int x = 10; //... } namespace B { int x = 20; //... } namespace C { using namespace A; using namespace B; //... } int main() { cout << C::x << endl; // geçersiz! ambiguity, A::x ve B::x arasında seçim yapılamaz return 0; } Bu tür durumlarda isimlerin farklı isim alanlarında bulunuyor olmaması gerekir. Bulunma derinliğinin bir önemi yoktur. Örneğin: namespace CSD { int x; //... } namespace A { using namespace CSD; //... } namespace B { int x = 20; //... } namespace C { using namespace A; using namespace B; //... } int main() { cout << C::x << endl; // geçersiz! ambiguity, B::x ve CSD::x arasında seçim yapılamaz return 0; } Burada C::x isminin aranması yine başarısızlıkla sonuçlanacaktır. using namespace direktifindeki isim alanlarına ilişkin isimler de isim aramsına sokulmaktadır. Örneğin: using namespace CSD::Util Burada derleyici CSD ismini "niteliksiz olarak", Util ismini de CSD içerisinde nitelikli olarak arayacaktır. Direktifin geçerli olması için burada belirtilen isim alanı isimlerinin bulunuyor olması gerekir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 21. Ders 25/10/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 44) using namespace direktifinin dışında C++'ta ayrıca bir de "using bildirimi" denilen bir bildirim de vardır. using bildirimi bir isim alanındaki ismi bir faaliyet alanına sokmak için kullanılmaktadır. Bu nedenle bu bir direktif değil bildirimdir. Çünkü bildirimlerde yeni bir isim bir faaliyet alanına katılmaktadır. using bildiriminin genel biçimi şöyledir: using ::, [::], [...]; Buradaki isim_alanı_ismi iç bir isim alanı (nested namespace) belirtebilir. Örneğin: using std::cout, std::endl; using CSD::Util::test //... cout << "this is a test" << endl; test(); Tabii using bildirimi bir faaliyet alanına yenibir ismi soktuğuna göre o faaliyet alanında o ismin tek olması gerekmektedir. Örneğin: namespace CSD { int a; //... } void foo() { using CSD::a; int a; // geçersiz! aynı faaliyet alanında aynı isim birden fazla kez bildirilemez //... } using bildirimi bir fonksiyona uygulandığında o isim alanındaki aynı isimli tüm fonksiyonlar ilgili faaliyet alanına sokulmuş olur. Yalnızca aynı isimli tek bir fonksiyonunun using bildirimi ile faaliyet alanına sokulması mümkün değildir. Örneğin: namespace CSD { void foo() { //... } void foo(int a) { //... } //... } int main() { using CSD::foo; foo(); // geçerli foo(10); // geçerli return 0; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; namespace CSD { int x, y, z; void foo() { cout << "CSD::foo()" << endl; } void foo(int a) { cout << "CSD::foo(int)" << endl; } void bar() { cout << "CSD::bar" << endl; } } void foo(double a) { cout << "CSD::foo(double)" << endl; } int main() { using CSD::foo, CSD::x; foo(); // CSD::foo() foo(10); // CSD::foo(int) foo(3.14); // CSD::foo(int) x = 10; // geçerli CSD::y = 20; // geçerli CSD::z = 30; // geçerli return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 45) İsim alanlarına ilişkin diğer bir bildirim de "namespace alias" denilen bildirimdir. Bu bildirim bir isim alanı ismini daha kolay kullanmak için düşünülmüştür. Örneğin isim alanı ismi uzun olabilir. Biz bunu kısa bir biçimde kullanmak isteyebiliriz. Ya da örneğin iç bir isim alanınını tek bir isim iel kullanmak isteyebiliriz. bildirimin genel biçimi şöyledir: namespace = ; Burada '=' atomunun sağında bir isim alanı bulunmak zorundadır. Bir tür ismi bulunamaz. Tabii buradaki isim alanı iç bir isim alanı olabilir. Örneğin: namespace U = CSD::Util; Artık burada U demekle CSD::Util demek tamamen aynı anlama gelmektedir. Bu işlemin bir typedef işlemi olmadığında dikkat ediniz. typedef işleminde ya da onun modern biçimi olan using işleminde bir türe alternatif isim verilmektedir. İsim alanları tür belirtmemektedir. Bu nedenle bu bildirime "namespace alias" bildirimi denilmektedir. namespace alias bildirimi herhangi bir isim alanına ya da herhangi bir bloğa yerleştirilebilir. Tabii ismin faaliyet alanı yerleştirildiği yere bağlı olarak değişmektedir. namespace alias bildirimi tek tek yapılma zorundadır. Örneğin: namespace U = CSD::Util namespace T = CSD::Test Aşağıdaki gibi yapılamaz: namespace U = CSD::Util, T = CSD::Test --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 46) C++11 ile birlikte diğer bazı dillerde çeşitli biçimlerde bulunan "öznitelikler (attributes)" konusu dile eklenmiştir. Öznitelikler derleyici için kullanılan direktiflerdir. Özniteliklerin temel amacı derleyicinin daha iyi kod üretmesini sağlamak, uyarı mekanizması üzerinde etkili olmak ve derleyicinin bazı davranışlarını değiştirmektir. C'de derleyicinin kimi davranışları komut satırı argümanlarıyla ya da #pragma direktifi yardımıyla değiştirilebilmektedir. #pragma direktifi C'de standart bir direktif olmasına karşın direktifin yanındaki komutlar derleyiciden derleyiciye değişebilmektedir. Örneğin: #pragma pack(1) struct SAMPLE { char a; short b; int c; }; #pragma pack komutu yapı elemanlarının hizalanması üzerinde etkili olmaktadır. Bu pragma komutu pek çok derleyici tarafından desteklenmektedir. Ancak pek çok pragma direktifi ilgili derleyiciye hatta ilgili platforma özgü olabilmektedir. C'de pragma direktifleri önişlemci aşamasında parse edilmektedir. Dolayısıyla zayıf bir kullanım alanına sahiptir. İşte öznitelikler bu pragram direktiflerinin standart ve genel bir biçimi gibi düşünülebilir. C++11 ile dile eklenen öznitelikler zaman içerisinde neredeyse tüm sentaktik öğelerde kullanılabilir duruma getirilmiştir. İzleten paragraflarda tipik olarak bu özniteliklerin hangi sentaktik öğelerde kullanılabileceğinin bir listesini (ayrıntılı olmayan listesini) vereceğiz. Bir öznitelik oluşturmanın genel biçimleri şöyledir: [[öznitelik_ismi]] [[öznitelik_ismi()]] [[öznitelik_ismi(argüman_listesi)]] [[öznitelik_isim_alanı::öznitelik_ismi]] [[öznitelik_isim_alanı::öznitelik_ismi()]] [[öznitelik_isim_alanı::öznitelik_ismi(argüman_listesi)]] [[using öznitelik_isim_alanı: öznitelik_ismi, öznitelik_ismi, ...]] Yukarıdaki iki köşeli parantezler içerisindeki öznitelik isimleri birden fazla olabilir. Bu durumda öznitelikler ',' atomu ile ayrılmalıdır. Aşağıda bazı geçerli öznitelik oluşturma örnekleri vermek istiyoruz: [[xxx]] [[xxx, yyy]] [[xxx(aaa, bbb)]] [[xxx(), yyy(aaa, bbb)]] [[nnn::xxx]] [[nnn::xxx(aaa, bbb)]] [[nnn::xxx(aaa, bbb), kkk::yyy(cccc)]] [[using N: xxx, yyy]] Burada xxx, yyy, aaa, bbb, ccc gibi isimler herhangi bir isim olarak kullanılmıştır. Öznitelik isim alanı iç içe olamamaktadır. Örnepşn: [[nnn::kkk::xxx]] Böyle bir öznitelik isim alanı geçerli değildir. Yani öznitelik bildiriminde en fazla bir tane :: atomu kullanılmalıdır. Bir sentaktik öğeye tek bir [[...]] yerleştirilmek zorunluluğu da yoktur. Birden fazla [[...]] aralarına başka bir atom bulundurulmadan yerleştirilebilir. Yukarıda da belrttiğimiz gibi öznitelikler (attributes) pek çok sentaktik öğede kullanılabilmektedir. Özniteliğin sentakstaki yerine göre kimin için yazıldığı belirlenebilmektedir. Örneğin: [[xxx::yyy]] void foo() { //... } Burada öznitelik fonksiyonun kendisi bulundurulmuştur. Örneğin: [[xxx::yyy]] int a, b, c; Burada öznitelik bildirimin tamamı için bulundurulmuştur. Örneğin: int a [[xxx::yyy]], b, c; Burada öznitelik a değişkeni için bulundurulmuştur. Örneğin: namespace [[xxx::yyy]] CSD { //... } Burada öznitelik CSD isim alanı için bulundurulmuştur. Özetle özniteliğin bulundurulduğu yer o özniteliğin sentaksın hangi parçasını nitelediğini belirtmektedir. Öznitelikler kabaca şu sentaktik öğelerde kullanılabilmektedir: - İsim alanlarında isim alanı isimlerinden önce. Örneğin: namespace [[xxx::yyy]] CSD { //... } - Bildirimlerde tür belirleyicilerinden ve niteleyicilerinden önce (yani bildirimlerin başında). Örneğin: [[xxx::yyy]] int a, b, c; - Bildirimlerde dekleratördeki isimlerden sonra. Örneğin: int a [[xxx::yyy]], b; - Dizi dekleratörlerinde diziyi belirten köşeli parantezlerden sonra. Örneğin: int a[10] [[xxx::yyy]]; - Gösterici ve referanslarda *, & ve && atomlarından sonra. Örneğin: int * [[xxx::yyy]]pi; int & [[xxx::yyy]] r = x; - Fonksiyonlarda bildirimin başında. Örneğin: [[xxx::yyy]] void foo() { //... } - Fonksiyonlarda fonksiyonun parantezlerinden sonra. Örneğin: void foo() [[xxx::yyy]] { //... } - Fonksiyonlarda fonksiyon isimlerinden sonra. Örneğin: void foo [[xxx::yyy]]() { //... } - Parametre değişkenlerinde tür belirleyicisinden önce. Örneğin: void foo([[xxx::yyy] int a, int b) { //... } - Parametre değişkenlerinde değişken isminden sonra. Örneğin: void foo(int a [[xxx::yyy], int b) { //... } - Deyimlerin başlarında. Örneğin: [[xxx::yyy]] if (ifade) { //... } [[xxx::yyy]] { ifade1; ifade2; ifade3; } [[xxx::yyy]] for (int i = 0; i < 10; ++i) { //... } - Boş deyimlerde. Örneğin: [[xxx::yyy]]; /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 22. Ders 30/10/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Belli bir sentaktik öğeye yerleştirilen özniteliklerin ne anlam ifade ettiği (yani semantiiği) derleyicileri yazanların isteğine bırakılmıştır. Yani derleyicilerin farklı öznitelikleri olabilmektedir. Her öznitelik her sentaktik öğede geçerli olmayabilir. Örneğin bazı öznitelikler yalnızca fonksiyon bidiriminde ya da tanımlamasında kullanılabilir. Bazı öznitelikler değişken tanımlamasında kullanılabilir. C++ standartlarında her derleyicinin desteklemesi gereken az sayıda öznitelik baştan belirlenmiştir. (C++'ın çeşitli sürümlerinde bu listeye eklemeler yapılmıştır.) Bunlara "standart öznitelikler" diyebiliriz. Standandartlara göre isim alanı içermeyen tüm öznitelikler ve std isim alanı içeren öznitelikler "reserved" bırakılmıştır. Yani bunların programcılar tarafından ve derleyiciler tarafından kullanılması yasaklanmıştır. (Genel olarak standartlarda "reserved" özelliklerin kullanılması "tanımsız davranış" olarak ele alınmaktadır.) Örneğin [[xxx]] biçiminde isim alanı içermeyen bir öznitelik programcılar tarafından da derleyicileri yazanlar tarafından da kullanılmamalıdır. Benzer biçimde [[std:xxx]] biçimindeki bir öznitelik de "reserved" durumdadır. O halde derleyicileri yazanlar kendileri öznitelik isim alanı uydurup kendi özniteliklerini bu öznitelik isim alanı ile oluşturmalıdırlar. Örneğin [[gnu::xxx]] gibi, [[msvc::xxx] gibi. Ayrıca standartlar "derleyici tarafından tanınmayan" bütün özniteliklerin derleyici tarafından "görmezden gelinmesi (ignore) gerektiğini" belirtmektedir. Bu durumda biz derleyicilerde olmayan bir öznitelik ismi uydursak programda herhangi bir hata ortaya çıkmayacaktır. (Tabii derleyiciler tanıyamadıkları öznitelikler için uyarı mesajları verebilirler.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi özniteliklere neden gereksinimn duyulmaktadır? Temel nedenleri şöyle ifade edebiliriz: - Derleyicilerde bazı davranışların değiştirilmesini sağlamak için - Derleyiciye ipucu vererek daha etkin kod üretimini sağlamak için - Derleyicilerin uyarı mekanizmalarında etkili olabilmek için - Derleyicilerin bazı "implementation defined" durumlarına yönelik açıklama yapmak için - Kodun okunabilirliğini artırmak için Konunun başında de belirttiğimiz gibi yukarıdaki amaçların bazıları komut satırı argümanlarıyla ve #pragma direktifleriyle kısmen sağlanabilmektedir. Ancak öznitelikler "daha genel ve çok daha spesifik" bir yöntem sunmaktadır. Öznitelikler neredeyse her sentaktik öğeye getirilebildiği için çok daha ince belirlemelerin yapılmasına olanak sağlamaktadır. Standart özniteliklerin yanı sıra çalıştığınız derleyiciye özgü özniteliklere göz gezdirebilirsiniz. Biz bu bölümde bazı standart öznitelikleri gözden geçireceğiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart [[noreturn]] özniteliği yalnızca fonksiyonlarda kullanılabilmektedir. Bir fonksiyonu bu biçimde özniteliklendirirsek derleyiciye bu fonksiyonun geri dönmeyeceğini söylemiş oluruz. Böyle fonksiyonların geri dönmesi "tanımsız davranış" oluşturmaktadır. noreturn özniteliği herhangi bir argüman almamaktadır. Bu öznitelik C++11'den beri bulunmaktadır. Bir fonksiyonun prototipinde ya da tenımlamasında [[noreturn]] özniteliği kullanılmışsa bütün prototiplerinde ve tanımlamasında bu özniteliğin kullanılması gerekmektedir. Pekiyi bir fonksiyonun geri dönmemesi nasıl mümkün olabilir? İşte aşağıda bazı senaryolaır görüyorsunuz: [[noreturn]] void foo() { //... exit(EXIT_SUCCESS); } [[noreturn]] void bar() { //... for (;;) { // sonsuz döngü //... } } [[noreturn]] void tar() { //... throw exception(); } Öte yandan standart kütüphanedeki bazı fonksiyonlar da artık [[noreturn]] ile bildirilmiştir. Örneğin exit fonksiyonu böyledir: [[noreturn]] void exit(int exit_code); Pekiyi bir fonksiyonun geri dönmeyeceğini derleyiciye söylemekle kim ne kazanmış olmaktadır? Derleyiciler fonksiyonları geri döndürebilmek için bazı makine komutlarını üretilen koda yerleştirmek zorundadır. (Örneğin pek çok işlemcide geri dönüşü "ret" isimli makine komutu sağlamaktadır. Ancak tek başına bu "ret") makine komutu yeterli de olmayabilir. Derleyici bazı yazmaçları geri dönmeden önce girişteki değerlerle yeniden yüklemek zorunda kalabilmektedir. Dolayısıyla fonksiyonun geri dönmeyeceğini anlayan derleyici bu kodları fonksiyona eklemeyebilir. Bu da daha etkin bir kod üretimi anlamına gelmektedir. Öte yandan fonksiyon çağrısında da kodun geri dönmeyeceğini anlayan derleyici orada da bazı optimizasyonları yapabilmektedir. [[noreturn]] programcılar için okunabilirliği de artırmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart [[deprecated]] özniteliği pek çok sentaktik öğeye getirilebilmektedir. Bu öznitelik ilgili özelliğin "deprecated" yapıldığını belirtir. Bu öznitelik C++14 ile eklenmiştir. Örneğin: [[deprecated]] void foo(); Burada foo fonksiyonunun ilgili kütüphanede artık "deprecated" yapıldığı belirtilmiştir. Deprecated sözcüğü "hala desteklenen ancak ileri verisyonlarda artık kaldırılabilecek olan" öğeleri belirtmektedir. Yukarıdaki örnekte foo fonksiyonunu biz hala kullanabiliriz. Ancak ileride bu fonksiyon kaldırılabileceğine göre bunu kullanmamaız daha uygun olacaktır. Genellikle "deprecated" öğeler için "daha iyi" alternatifler bulundurulmaktadır. Programcının bu daha iyi olan alternatifleri kullanması uygun olacaktır. Tipik olarak derleyiciler "deprecated" özellikleri gördüklerinde bir uyarı mesajıyla durumu programcıya bildirmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart [[likely]] ve [[unlikely]] öznitelikleri deyimlerde ve etiketlerde (labels) kullanılabilmektadır. Bu öznitelik C++20 ile eklenmiştir. Örneğin: if (koşul) [[likely]] { //... } else { //... } Burada if deyimim doğruysa ksımından sapması çok daha muhtemel bir durum olarak belirtilmiştir. derleyiciler bu tür durumlarda daha iyi makine komutları üretebilmektedir. Bu konu "instruction scheduling", "instruction reordering" ve "jump prediction" denilen optimizasyon temalarıyla ilgilidir. İşlemciler bir makine komutunu yaparken aynı zamanda sonraki komutlar üzerinde de birtakım hazırlık işlemlerini yapabilmektedir. Bu nedenle if deyimlerinde mümkün olan durumun makine komutları olarak öne yerleştirilmesi önemlidir. Örneğin: if (foo() == -1) [[unlikely]] { //... } Burada foo başarısız olduğunda -1 değerine geri dönüyor olsun. Programcı derleyiciye "bu fonksiyonun başarsız olma olasılığı çok düşük" demek istemektedir. Bu bilgiyi elde eden derleyici daha etkin makine komutları üretebilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standat [[assume(ifade)]] özniteliği belli bir noktada belli bir koşulun kesinlikle sağlanması gerektiğini belirtmktedir. Bu öznitelik boş deyime uygulanabilir. (Yani bu öznitelikten sonra ';' atomunun gelmesi gerekir.) Eğer söz konusu koşul sağlanmazsa "tanımsız davranış" oluşmaktadır. Örneğin: void foo(int a) { [[assume(a > 0)]]; //... } Burada fonksiyonun pozitif bir argümanla çağrılacağı derleyiciye bildirilmiştir. Derleyici bu varsayımı kullanarak daha etkin kod üretebilir. Eğer assume özniteliğine geçirilen argüman virgül operatörü içeriyorsa bu virgül operatörü paranteze alınmalıdır. Bu durumda virgül operatörünün sol tarafı öncül işlemi sağ tarafı koşulu belirtir. Örneğin: [[assume((foo(), x > 0))]] Burada foo çağrıldıktan sonra x değişkeninin değerinin pozitif olacağı belirtilmiştir. Tabii assume özniteliğindeki ifade işletilmez. Yai bu örnekte foo çağrılmayacaktır. Derleyici ileride foo çağrıldığında bu çağrıdan sonra x'in 0'dan büyük olacağını anlayacaktır. Örneğin (cppreference.com sitesinden alınmıştır): x = 3; int z = x; [[assume((h(), x == z))]]; h(); g(x); // Derleyici bu işlemi g(3) ile eşdeğer olarak ele alabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart [[fallthrough]] özniteliği swith deyimlerinde case etiketleri için kullanılmaktadır. Öznitelik boş deyimlere uygulanabilmektedir. Fallthrough işleminin kasten yapıldığını belirtmektedir. Dolayısıyla derleyiciler bu tür durumlarda "yanlışlıkla yapılan fallthrough işlemlerinde" verdikleri uyarı mesajlarını vermezler. Bu öznitelik C++17 ile birlikte standartlara eklenmiştir. Örneğin: void f(int n) { void g(), h(); switch (n) { case 1: case 2: g(); [[fallthrough]]; case 3: h(); } } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- standart [[maybe_unused]] özniteliği bildirilen bir değişkenin kullanılmadığında oluşabilecek uyarıyı ortadan kaldırmak için kullanılmaktadır. Bu öznitelik değişken bildirimlerinde, sınıf bildirimlerinde enum bildirimlerinde, enum sabit bildiriminde, typedef isimlerinin bildiriminde sınıfın veri elemanlarının, global fonksiyonların ve üye fonksiyonların (ileride görülecek) bildirimlerinde kullanılabilir. Örneğin: [[maybe_unused]] int a; void foo(int a, int [[maybe_unused]] b) { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 47) C++'ın çeşitli versyonlarında deyimlerde de bazı eklemeler yapılmıştır. C++17 ile birlikte if deyimine isteğe bağlı bir "init kısmı" eklenmiştir. Bu init kısmı bir ifade içerir. İfadeden sonra ';' atomunun bulunması gerekir. Yukarıda da belirttiğimiz gibi bu "init" kısmı deyidme bulundurulmak zorunda değildir. Dolayısıyla deyim eski biçimiyle uyumludur. Örneğin: if (ifade; koşul) { //... } else { //... } Bu deyim aşağıdaki ile eşdeğerdir: { ifade; if (koşul) { //... } else { //... } } if deyim,ne eklenen bu "init" kısmının adeta for döngüsünün birinci kısmı gibi deyime girişte yapıldığına dikkat ediniz. if deyiminin init ksımında bir bildirim de bulunabilmektedir. Bu durumda bildirilen değişken if deyimi içerisinde kullanılabilir. Örneğin: if (char buf[32]; fgets(buf, 32, stdin) != NULL) { //... } Burada if deyiminin "init" kısmında buf dizisi bildirilmiş ve if içerisinde kullanılmıştır. Buradaki buf dizisinin faaliyet alanı if ile sınırlıdır. "init" kısmı boş da bırakılabilir. Tabii böyle bir kullanımın anlamı yoktur. Örneğin: if (; koşul) { // geçerli ama anlamsız //... } Benzer biçimde switch deyimine de "init" kısmı eklenmiştir. Genel kullanım if deyiminde belirttiğimiz gibidir. Örneğin: switch (int x = foo(); x) { //... } C++20 ile birlikte Aralık tabanlı for döngülerine de "init" kısmının eklendiğini aralık tabanlı for döngülerini anlattığımız kısımda belirtmiştik. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 48) C++17 ile birlikte "constexpr if" C++23 ile de "consteval if" deyimi biçiminde yeni iki if deyimi varyasyonu eklenmiştir. constexpr if deyiminin genel biçimi şöyledir: if constexpr ([init;] koşul) [ else ] constexpr if deyiminde koşul ifadesinin sabit ifadesi olması gerekmektedir. Aslında constexpr if deyimi #if önişlemci komutunu çağrıştırmaktadır. Anısanacağı gibi #if komutunda #if anahtar sözcüğünün yanınadaki sabit ifadesi sıfırdan farklı ise #else anahtar sözcüğüne kadarki kısım derleme modülüne veriliyordu, eğer #if anahtar sözcüğünün yanındaki ifade 0 ise #else ile #endif arasındaki kod bölümü derleme modülüne veriliyordu. Başka bir deyişle #if deyiminin koşula göre bir kısmı koddan atılmaktaydı. İşte constexpr if deyimi de bunun bir benzerini yapmaktadır. Ancak bu işlem önişlemci aşamasında değil derleme aşamasında yapılmaktadır. Örneğin: constexpr int x = 10; //... if constexpr (x > 0) foo(); else bar(); Burada if deyiminin else kısmı tamamen koddan kaldırılacaktır. consexpr if deyimi #if önişlemci komutunu çağrıştırıyor olsa da aralarında önemli farklılıklar vardır. constexpr if deyiminin derleme aşamasında derleme modülü tarafından yapılıyor olması çeşitli durumlarda esneklikler sağlamaktadır. constexpr if deyimi özellikle şablonlarla birlikte kullanılmaktadır. constexpr if deyiminde deyimin bir kısmı kodda atılıyor olsa bile atılan kısmın yine de sentaks ve semantik kısıtları sağlıyor olması gerekmektedir. Örneğin: constexpr int x = 10; int *pi; //... if constexpr (x > 0) { foo(); } else { pi = 10; // geçersiz! } Burada constexpr if deyiminin yanlışsa kısmı koddan atılacak olsa da buradaki deyimler üzerinde sentaks ve semantik kontroller yine yapılacaktır. C++23 ile birlikte consteval if deyimi de dile eklenmiştir. consteval if deyiminin işlevi ve gerekliliği biraz daha karmaşıktır. Deyimin genel biçimi şöyledir: 1) if consteval { } [ else { }] 2) if consteval { } [ else { }] Bu özellik C++'a çok yeni eklendiği için derleyicileriniz bu özelliği desteklemiyor olabilir. Bu if deyimi şu anlama gelmektedir: "Eğer söz konusu kod parçası sabit ifadesi gereken bir bağlamda kullanılmışsa buradaki if deyimi doğruysa biçimde sapacak, eğer söz konusu kod parçası sabit ifadesi gereken bir bağlamda kullanılmadıysa buradaki if deyimi yanlışsa biçiminde sapacaktır." Örneğin: constexpr int cube(int a) { if consteval { return a * a * a; } else { return pow(a, 3); } } Burada cube fonksiyonu parametresi ile aldığı sayının kübüyle geri dönmektedir. Ancak bu constexpr fonksiyon sabit ifadesi gereken bir bağlamda da çağrılmış olabilir, sabit ifadesi gerekmeyen bir bağlamda da çağrılmış olabilir. Eğer bu fonksiyon sabit ifadesi gereken bir bağlamda çağrılmışsa bu durumda consteval if deyimim doğruysa kısmı derleme aşamasında devreye girecek ve a * a * a işlemi ile derleme aşamsında değer hesaplanacaktır. Eğer bu fonksiyon sabit ifadesi gerekmeyen bir bağlamda çağrılmışsa bu durumda consteval if deyiminin yanlışsa kısmı devreye girecek ve küp alma işlemi derleme zanında değil fonksiyon çağrılarak yapılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 49) C++'ta ilk standartlardan beri "anonim birlik (anonymous union)" oluşturma özelliği bulunmaktadır. Anonim birlik oluşturabilmek için birliğe bir isim verilmemesi ve birlik bildiriminde aynı zamanda nesne tanımlamanın yapılmamış olması gerekmektedir. Örneğin: union { int a; long b; double c; }; Burada biz a, b, ve c değişkenlerini doğrudan kullanabiliriz. Bu değişkenler çakışık yerleştirilmektedir. Yani burada yapılmak istenen şey aşağıdakiyle eşdeğerdir: union SomeName { int a; long b; double c; }; SomeName sn; Biz burada sn.a, sn.b ve sn.c ile çakışık yerleştirilen birlik elemanlarını kullanabiliriz. Ancak anonim birliklerde buna hiç gerek yoktur. Yukarıda da belirttiğimiz gibi anonim birlik oluşturabilmek için birliğe isim verilmemiş olamsı gerekmektedir. Örneğin: union U { int a; long b; double c; }; Burada artık biz a, b, ve c değişkenlerini doğrudan kullanamayız. Benzer biçimde: union { int a; long b; double c; } x; Burada da biz a, b ve c değişkenlerini doğrudan kullanamayız. Birlikler C++'ta aslında sınıflar gibi özelliklere sahiptir. Dolayısıyla anonim birlikler sınıflara ilişkin özleliklere sahip olamazlar. (Örneğin ileride birliklerin içerisinde üye fonksiyonların bulunabileceğini göreceğiz. Ancak anonim birliklerde üye fonksiyonlar bulunamamaktadır.) Anonim birliklerin yazım kolaylığı sağladığına dikkat ediniz. Örneğin: union { int a; double; }; ile aşağıdaki tanımlamalara dikkat ediniz: int a; double b; Bu ikisi tanımlama arasındaki tek fark anonim birlik tanımlamasında a ve b nesnelerinin çakışık yerleştirilmesidir. Anonim birlikler bir isim alanının içerisinde (yani global düzeyde) tanımlanacaksa union anahtar sözcüğünün önüne static anahtar sözcüğünün getirilmesi gerekmektedir. Örneğin: static union { int a; double b; }; Ancak anonim birlik sisimsiz isim alanı içerisine yerleştirilirse bu static belirleyicisine gerek kalmamaktadır. Anom birliklerin elemanlarının birliğin yerleştirildiği faaliyet alanına enjekte edildiğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 50) C++11 ile birlikte dile static_assert isimli bir bildirim (decalartion) eklenmiştir. static_assert bildirimi derleme zamanında assert kontrolü yapmaktadır. Bilindiği gibi standart assert fonksiyonu programın çalışma zamanı sırasında işleme sokulmkatadır. Halbuki static_assert bildirimi derleme aşamasında işleme sokulur. Derleme aşamasında #if önişlemci komutuyla da bazı kontroller yapılıp #error önişlemci komutuyla derleme işlemi sonlandırılabilmektedir. Ancak önişlemci aşamasında yapılacek kontroller oldukça sınırlıdır. static_assert bildiriminin genel biçimi şöyedidr: static_assert(sabit_ifadesi_koşulu, "mesaj"); Derleme aşamasında eğer buradaki koşul sağlanmazsa derleyici derleme işlemini keser ve ikinci argümanla girilen hata mesajını yazdırır. Örneğin. static_assert(sizeof(int) >= 4, "int type too small"); Burada programcı int türünün 4 byte'tan küçük olmamasını istemektedir. sizeof bir derleme zamanı operatörü olduğu için kontrolün bizzat derleme aşamasında yapılması gerekmektedir. C++17 ile birlikte buradaki ikinci parametre artık bulundurulmak zorunda değildir. Örneğin: static_assert(sizeof(int) >= 4); Tabii static_assert bildiriminde koşulun sabit ifadesi biçiminde oluşturulması gerekmektedir. Her ne kadar semantik olarak static_assert bir bildirime benzemiyorsa da static_assert C++ standartlarında bir "bildirim (declaration)" olarak gramere eklenmiştir. static_assert özellikle şablonlarla birlikte sıkça kullanılmaktadır. static_assert bildiriminin derleme aşmasında ele alındığına dikkat ediniz. Bu bildirim bir fonksiyonun içerisine yerleştirilirse etki göstermesi için o fonksiyonun çağrılmasına gerek yoktur. Çünkü fonksiyonun çağrılması çalışma zamanına ilişkin bir etkinliği belirtir. İleride göreceğimiz şablon işlemlerinde "şablon açımları (template instantiation)" derleme aşamasında yapılmaktadır. Eğer static_assert fonksiyon şablonlarında kullanılırsa bunların devreye girmesi "açım (instantiation)" yapılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 51) C99 ile birlikte C'ye "designated initializer" denilen ilkdeğer verme biçimi eklenmişti. Bu sayede artık C99 ve sonrasında yapıların ve dizilerin elemanlarına sırasıyla ilkdeğer verme zorunluğu kaldırılmış oldu. C'de sentaks aşağıdaki gibidir: struct SAMPLE { int a, b, c, d; }; struct SAMPLE s = {.c = 10, .a = 20}; int a[10] = {[5] = 20, [3] = 30, 40}; İşte bu "designated initiaizer" sentaksı daraltılarak C++20 ile C++'a da sokulmuştur. Ancak C99'da olmayan aşağıdaki kısıtlar oluşturulmuştur: - C++'ta dizilerde designated initializer kullanımı yoktur. - Bazı koşulları sağlayan C ile uyumlu yapılarda ve sınıflarda (C++'ta yapılar da birer sınıftır) designated initializer kullanılabilmektedir. Ancak yapıların ve sınıfların sıralı elemanlarına bu biçimde değer verilebilmektedir. (Yani ilkdeğer verilen elemanların sırası yapı bildirimindeki sıraya uygun olmalıdır.) struct Sample { int a, b, c, d; }; Sample s = {.b = 10, .d = 20}; // geçerli Sample k = {.d = 20, .b = 10}; // geçersiz! Burada yapı (ya da sınıf) elemanlarına önceki eleman önde olacak biçimde ilkdeğer verilmek zorundadır. Halbuki C99'da böyle bir zorunluluk yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında C ve C++ standartlarında "operatörlerin öncelik tablosu" diye bir tablo bulunmamaktadır. Operatörler arasındaki öncelik ilişkisi standartlarda zaten BNF gramerinin doğal sonucu olarak oluşmaktadır. Operatörlerin öncelik tablosu aslında gerçek durumu belli bir kusurla basitleştirmek için düşünülmüştür. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 24. Ders 06/11/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın Nesne Yönelimli Programlama Tekniği İle Doğrudan İlgili Olan Farklılıkları ve fazlalıkları --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar "C++'ın C'den Nesne Yönelimli Programalama Tekniği İle Doğrudan İlgili Olamayan Farklılıkları ve Fazlalıkları" üzerinde durduk. Artık C++'ı "nesne yönelimli (object oriented)" bir dil yapan sınıflar ve onlarla ilgili konuları ele alacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ı nesne yönelimli bir dil yapan en önemli özellik "sınıflar (classes)" konusudur. Sınıflar C'deki yapılara benzer olmakla birlikte yalnızca data değil aynı zamanda fonksiyon da içeren veri yapılarıdır. Yani C'deki yapılar yalnızca data içerirken C++'taki sınıflar aynı zamanda fonksiyon da içermektedir. Zaten C'deki yapılar ve birlikler de artık C++'ta sınıf anlamına gelmektedir. C++'ta "sınıf türü (class type)" denildiğinde yalnızca sınıflar değil, yapılar ve birlikler de anlaşılmalıdır. C++'ta bir sınıf hem data hem fonksiyon içermek zorunda değildir. Yalnızca data ya da yalnızca fonksiyon da içerebilir. Bu durumda sınıf kavramı aslında C'deki yapılar üzerinde bir fazlalık gibi değerlendirilebilir. Sınıflar nesne yönelimli programlama tekniğinin yapı taşlarıdır. Tüm nesne yönelimli dillerde sınıf isminde ya da bu işlevde bir veri yapısı bulunmaktadır. Tabii diller arasında bu veri yapıları arasında temel özellikleri aynı olmak üzere farklılıklar bulunabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf bildiriminin genel biçimi şöyledir: class { [data ve fonskiyon bildirimleri ve tür tanımlamaları] }; struct { [data ve fonksiyon bildirimleri ve tür tanımlamaları] }; Örneğin: class Sample { //... }; struct Mample { //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta bir sınıfın içerisindeki fonksiyonlara "üye fonksion (member function)" denilmektedir. Halbuki Java, C#, Python gibi dillerde buna "metot (method)" denilmektedir. C++'ta bir sınıf "public", "protected" ve "private" olmak üzere üç bölümden oluşmaktadır. Bir bölüm, bölüm belirten anahtar sözcük ve ':' atomu ile başlatılır, başka bir bölüm belirten anahtar sözcüğe kadar devam eder. Sınıf bildirimi içerisinde bu bölüm belirten anahtar sözcükler birden fazla kez kullanılabilirler. Örneğin: class Sample { public: //... protected: //... private: //... public: //... private: //... }; Sınıf isimleri pek çok programcı tarafından "Pascal tarzı (Pascal casting)" ile harflendirilmektedir. Pascal tarzında her sözcüğün ilk harfi büyük harf ile yazılır. Biz de kursumuzda bu isimlendirmeyi tercih edeceğiz. Ancak C++'ın standart kütüphanesindeki sınıf isimleri "klasik C tarzı (snake casting)" ile isimlendirilmiştir. Bir sınıf bildirimine hiçbir bölüm belirten anahtar sözcük ile başlanmazsa bu durumda sınıf bildirimi "class" anahtar sözcü ile yapılmışsa default bölüm "private", "struct" anahtar sözcüğü ya da "union" ile yapılmışsa default bölüm "public" biçimdedir. Bunun dışında sınıf bildiriminin class ya da struct anahtar sözcüğüyle yapılmasının bir farkı yoktur. struct ve union bildirimlerinde C ile uyumu korumak amacıyla default bölüm public yapılmıştır. Örneğin: class Sample { // buradaki elemanlar private bölümde public: // buradaki elemanlar public bölümde protected: // buradaki elemanlar protected bölümde }; Örneğin: struct Test { // buradaki elemanlar public bölümde private: // buradaki elemanlar private bölümde protected: // buradaki elemanlar protected bölümde }; Sınıf içerisinde bölümlerin oluşturulma sırasının hiçbir önemi yoktur. Ancak ağırlıkı tercih edilen durum önce public, sonra protected, sonra da private bölümdür. Örneğin: class Sample { public: //... protected: //... private: //... }; Tabii sınıf bildiriminde bu bölümlerin hepsi olmak zorunda değildir. Herhangi biri ya da birden fazlası olabilir. C'deki struct ve union türleri C++'ta birer sınıf belirtmekle birlikte C uyumunun korunması için onların default bölümü public yapılmıştır. Örneğin: struct Sample { int a, b, c; // bu elemanlar public bölümde }; Biz bu bölümlerin ne anlam ifade ettiğini görene kadar sınıf elemanlarını hep public bölüme yerleştireceğiz. Diğer bazı nesne yönelimli dillerde public, protected ve private bölümlerin dışında başka bölümler de olabilmektedir. (MÖrneğin C#'ta "internal" ve "protected internal" bölümler vardır.) Python gibi bazı dilelrde sınıflarda bölümler yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta sınıflar global ya da yerel biçimde bildirilebilirler. Ancak C'deki yapılarda olduğu gibi hemen her zaman sınıflar yerel değil global olarak bir isim alanının içerisinde bildirilmektedir. (Hiçbir isim alanının içinde olmayan global bölgenin de "global isim alanı" denilen bir isim alanı belirttiğini anımsayınız.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın üye fonksiyonlarının yalnızca prototipleri sınıf bildirimi içerisinde belirtilebilir. Tanımlamaları o sınıfın içinde bulunduğu isim alanında ya da o isim alanını kapsayan isim alanlarından birinde yapılabilir. Üye fonksiyon tanımlaması sınıf dışında yapılırken fonksiyon isminin sınıf ismi ile :: operatörü kullanılarak niteliklendirilmesi gerekir. Üye fonksiyonu dışarıda tanımlamanın genel biçimi şöyledir: <[isim alanı isimleri::]sınıf ismi>::([prametre bildirimi]) { //... } Örneğin: class Sample { public: void foo(); void bar(); //... }; void Sample::foo() { //... } void Sample::bar() { //... } Standartlara göre üye fonksiyon tanımalamaları sınıf bildirimini kapsayan daha dış bir isim alanında isim alanını ismi de belirtilerek yapılabilir. Örneğin: namespace CSD { class Sample { public: void foo(); void bar(); //... }; void Sample::foo() // geçerli { //... } } void CSD::Sample::bar() // geçerli { //... } int main() { //... return 0; } Tabii en normal durum sınıf bildirimi hangi isim alanı içerisinde yapılmışsa üye fonksiyon tanımalamarının da o isim alanının içerisinde yapılmasıdır. Tabii using namespace direktifi ile yine sınıf isminin görülmesi sağlanabilir. Örneğin: namespace CSD { class Sample { public: void foo(); void bar(); //... }; void Sample::foo() // geçerli { //... } } using namespace CSD; void Sample::bar() // geçerli { //... } Burada Sample ismi CSD ile niteliklendirilmemiştir. Çünkü zaten derleyici tarafından Sample ismi görülmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: int a; int b; void foo(); void bar(int a); }; void Sample::foo() { cout << "Sample::foo" << endl; } void Sample::bar(int a) { cout << "Sample::bar" << endl; } int main() { //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir üye fonksiyonun tanımlaması sınıf içerisinde yapılabilir. Ancak bu durumda üye fonksiyon inline kabul edilmektedir. Örneğin: class Sample { public: int a; int b; void foo() // foo inline { //... } void bar() // bar inline { //... } }; Bu nedenle programcılar inline olarak açılmasını istedikleri küçük küçük üye fonksiyonların tanımlamalarını doğrudan sınıf bildirimi içerisinde yapabilmektedir. Tabii üye fonksiyonun prototipinin önüne inline anahtar sözcüğü getirilip fonksiyon yine dışarıda tanımlanırsa da inline olur. Ancak bu biçim pek tercih edilmemektedir. Örneğin: class Sample { public: int a; int b; inline void foo(); // foo inlile inline void bar(); // bar inline }; void Sample::foo() { //... } void Sample::bar() { //... } Java ve C# gibi dillerde prototip olmadığı için mecburen sınıfın metotları sınıf içerisinde tanımlanmaktadır. Bu dillerde inline diye bir kavram da yoktur. Bu dillerde metotlar sınıfın içerisinde yazılmak zorunda olduğu için bölüm C++'taki gibi bölüm kavramının okunabilirliği azaltacağı düşünülmüştür. Bu nedenle bu dillerde bölüm belirten anahtar sözcükler ayrıca her alan'da ve metotta belirtilmektedir. Örneğin: // Dikkat C#/Java örneği class Sample { private int a; private int b; public void foo() { //.. } public void bar() { //... } void tar() // C#'ta private, java'da internal { //... } } Bu dillerde alan ya da metotlarda erişim belirleyici anahtar sözcükler yazılmazsa default durum C#'ta private, Java'da ise internal kabul edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: int a; int b; void foo() { cout << "Sample::foo" << endl; } void bar(int a) { cout << "Sample::bar" << endl; } }; int main() { //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi C++'ta bir fonksiyon ya sınıfların dışında yani global bölgede bulunabilir ya da sınıfların içerisinde bulnabilir. Sınıfların içerisinde bulunan fonksiyonlara "üye fonksiyon (member function)" dendiğini söylemiştik. Biz kurusuuzda sınıfların içerisinde olmayan C'de ki gibi fonksiyonlara "global fonksiyonlar (global functions)" da diyeceğiz. Tabii global fonksiyon dendiğinde global isim alanındaki fonksiyonlar anlaşılmamlıdır. Global fonksiyonlar sınıfların içerisinde bulunmayan fonksiyonlardır. Yani herhangi bir isim alanın içerisinde bulunan ancak bir sınıfın içerisinde bulunmayan fonksiyonlara "global fonksiyon" diyeceğiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta sınıflar (yapıların ve birliklerin de bir sınıf olduğunu anımsayınız) bir "faaliyet alanı (scaope)" da belirtmektedir. Dolayısıyla C++'ta farklı sınıflarda aynı isimli ve aynı parametrik yapıya sahip fonksiyonlar bulunabilir. Bunlar farklı faaliyet alanlarında bulunduklarından sorun oluşturmazlar. Tabii aynı zamanda aynı isimli ve aynı parametrik yapıya sahip bir global fonksiyon da bulunabilmektedir. Örneğin: class Sample { public: void foo(); //... }; class Test { public: void foo(); //... }; void Sample::foo() { //... } void Test::foo() { //... } void foo() { //... } Örnekteki bu fonksiyonların hepsinin birlikte bulunmasında hiçbir sakınca yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıflar hem data hem de fonksiyonlara sahip olabiliyordu. C++'ta sınıflar içerisindeki fonksiyonların "üye fonksiyon (member function)" biçiminde isimlendirildiğini belirtmiştik. İşte C++'ta sınıftaki data elemanlarına da "veri elemanları (data member)" denilmektedir. Hem üye fonksiyonlar hem de veri elemanları sınıfın elemanlarıdır. Örneğin: class Sample { public: int a; int b; void foo(); void bar(); }; Burada a ve b Sample sınıfının veri elemanlarıdır. foo ve bar ise üye fonksiyonlarıdır. Sınıflardaki veri elemanlarının isimlendirilmesi dilden dile değişebilmektedir. Örneğin C# ve Java'da bunlara "alan (field)" denilmektedir. Python'da ise "öznitelik (attribute)" denilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta her sınıf bir tür de belirtmektedir. Sınıflar türünden nesneler tanımlanabilir. Örneğin: class Sample { //... }; Sample s; // s nesnesi Sample türünden class Sample k; // tür isminde class ve struct sözcükleri de kullanılabilir, ancak gereksizdir. Sınıfların tür isimleri yalnızca sınıf isimleriyle belirtilebilmektedir. Ancak tür isminde class anahtar sözcüğü de kullanılabilir. Örneğin: class Sample { //... }; int Sample; // geçerli Sample s; // geçersiz! class Sample k; // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesi içerisinde sınıfın üye fonksiyonları (member functions) yer kaplamaz. Üye fonksiyonlar mantıksal bakımdan sınıfla ilişkilendirilmiş durumdadır. Üye fonksiyonlar tıpkı global fonksiyonlar gibi programın ".text" denilen bölümünde yer kaplarlar. Sınıf nesnesi içerisinde yalnızca sınıfın "static olmayan veri elemanları (nonstatic data member)" yer kaplamaktadır. C++'ta veri elemnanları ve üye fonksiyonlar static olabilir ya da static olmayabilir. Static veri elemanları ve static üye fonksiyonlar ileri ele alınacaktır. Örneğin: class Sample { public: int a; // nesne içerisinde yer kaplar int b; // nesne içerisinde yer kaplar static int c; // nesne içerisinde yer kaplamaz! static veri elemanları ileride ele alınacak void foo(); // nesne içerisinde yer kaplamaz void bar(); // nesne içerisinde yer kaplamaz static void tar(); // nesne içerisinde yer kaplamaz! static üye fonksiyonlar ileride ele alınacaktır. }; Pekiyi bir sınıf nesnesi içerisindeki eleman organizasyonu nasıldır? İşte sınıf türünden nesneler statik olmayan veri elemanlarından oluşan bileşik nesnelerdir. C++ standartlarına göre "iki bölüm belirten anahtar sözcük arasındaki veri elemanları ilk yazılan eleman düşük adreste olacak biçimde ardışıl olarak" yerleştirilir. Ancak farklı bölümlerdeki elemanların birbirlerine göre durumu standartlarda derleyicileri yazanların isteğine bırakılmıştır. Fakat yaygın tüm derleyiciler bölüm farkı gözetmeksizin ilk yazılan eleman düşük adreste olacak biçimde ardışıl bir yerleşim uygulmaktadır. Tabii yine derleyiciler C'de olduğu gibi elemanlar arasında hizalama amaçlı kontrollü boşluklar (padding) bırakabilmektedir. Örneğin: class Sample { public: int a; int b; private: int c; int d; }; Burada a ile b ve c ile d ilk yazılan eleman düşük adreste olacak biçimde ardışıl yerleştirilmek zorundadır. Ancak hangi bölümün nesnenin düşük adresinde bulunacağı standartlarda derleyicileri yazanların isteğine bırakılmıştır. Yaygın derleyicilerin hepsi bölümleri dikkate almaksızın tıpkı yapılarda olduğu gibi elemanları yukarıdan aşağıya doğru ardışıl dizmektedir. Standartlardaki anlatıma göre biz bir sınıf nesneninin adresini aldığımızda bu adres ile sınıfın ilk veri elemanının adresi aynı olmak zorunda olmak zorunda değildir. (Halbuki C'deki yapılarda bir yapı nesnesinin adresi onun ilk elemanının adresi ile aynı olmak zorundadır. C'de yapı nesnelerinin başında derleyiciler padding uygulayamazlar.) Biz kurusmuzda gösterimlerde sınıfın veri elemanlarını ilk yazılan eleman düşük adreste olacak biçimde peşi sıra geliyormuş diziliyormuş gibi göstereceğiz. Böylece bazı açıklamaları daha anlaşılabilir bir hale getirebileceğiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ include using namespace std; class Sample { public: int a; int b; void foo(); void bar(int a); }; void Sample::foo() { cout << "Sample::foo" << endl; } void Sample::bar(int a) { cout << "Sample::bar" << endl; } int main() { Sample s; // s'in içerisinde yalnızca a ve b veri elemanları yer kaplıyor. return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 25. Ders 08/11/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Üye fonksiyonlar global fonksiyonlar gibi doğrudan isim belirtilerek çağrılamazlar. Üye fonksiyonlar ilgili sınıf türünden bir nesne ile "." operatörü kullanılarak çağrılırlar. Örneğin: class Sample { public: int a; int b; void foo(); void bar(); }; void Sample::foo() { //... } void Sample::bar() { //... } Sample s; // s Sample sınıfı türünden bir nesne s.foo(); // üye fonksiyonlar ilgili sınıf türünden bir nesne ile çağrılabilir s.bar(); // üye fonksiyonlar ilgili sınıf türünden bir nesne ile çağrılabilir Görüldüğü gibi bir üye fonksiyonu çağırabilmek için önce ilgili sınıf türünden bir nesnenin tanımlanmış olması gerekmektedir. Burada üye fonksiyonların Sample::foo() ve Sample::bar() biçiminde çağrılmadığına, ilgili sınıf türünden bir nesne kullanılarak çağrıldığına dikkat ediniz. İleri üye fonksiyonların static de olabileceğini göreceğiz. static üye fonksiyonlar nesne olmadan sınıf ismiyle niteliklendirilerek (yani Sample::tar() gibi) çağrılabilmektedir. Ancak biz bu konuyu anlatana kadar zaten üye fonksiyonların static olmayan (nonstatic) üye fonksiyonlar olduğunu varsayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: int a; int b; void foo(); void bar(); }; void Sample::foo() { cout << "Sample::foo" << endl; } void Sample::bar() { cout << "Sample::bar" << endl; } void foo() { cout << "global foo" << endl; } void bar() { cout << "global bar" << endl; } int main() { foo(); // global foo bar(); // global bar Sample s; // s nesnesi Sample türünden s.foo(); // Sample sınıfının üye fonksiyonu olan foo s.bar(); // Sample sınıfının üye fonksiyonu olan bar return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfların veri elemanlarına da yine ilgili sınıf türünden bir nesne ile "." operatörü kullanılarak erişilmektedir. Bu erişim biçimi zaten C'deki yapılarda da böyledir. Örneğin: class Sample { public: int a; int b; void foo(); void bar(); }; //... Sample s; s.a = 10; // geçerli s.b = 20; // geçerli s.foo(); // geçerli s.bar(); // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: int a; int b; void foo(); void bar(); }; void Sample::foo() { cout << "Sample::foo" << endl; } void Sample::bar() { cout << "Sample::bar" << endl; } int main() { Sample s; s.a = 10; s.b = 20; cout << s.a << endl; cout << s.b << endl; s.foo(); s.bar(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta "sınıf faaliyet alanı (class scope)" isminde yeni bir faaliyet alanı daha vardır. Sınıf faaliyet alanı sınıf bildiriminin içerisinde ve sınıfın tüm üye fonksiyonların içerisinde bir ismin doğrudan kullanılabilirliğini anlatmaktadır. Bu durumda C++'ta faaliyet alanları genişten dara doğru şöyledir: - Dosya faaliyet alanı (file scope) - Sınıf faaliyet alanı (class scope) - Fonksiyon faaliyet alanı (function scope) - Block faaliyet alanı (block scope) Sınıfın tüm elemanaları yani sınıf bildirimi içerisinde bildirilen tüm isimler sınıf faaliyet alanı kuralına uyarlar. Biz bu isimleri doğrudan üye fonksiyonlar içerisinden kullanabiliriz. Örneğin: class Sample { public: int a; int b; void foo(); void bar(); }; void Sample::foo() { a = 10; // geçerli, a sınıf faaliyet alanına ilişkin b = 20; // geçerli b sınıf faaliyet alanına ilişkin } void Sample::bar() { cout << a << endl; // geçerli, a sınıf faaliyet alanına ilişkin cout << b << endl; // geçerli b sınıf faaliyet alanına ilişkin foo(); // geçerli foo sınıf faaliyet alanına ilişkin } Burada sınıf bildiriminin içerisinde bildirilmiş olan a, b, foo ve bar değişkenleri "sınıf faaliyet alanı (class scope)" kuralına uymaktadır. Bu nedenle bu isimler Sample sınıfının üye fonksiyonları içerisinde doğrudan (yani niteliklendirilmeden) kullanılabilirler. Sınıf faaliyet alanının "bir grup fonksiyon içerisinde" tanınma aralığı anlamına geldiğine ve bu nedenle fonksiyon faaliyet alanından daha geniş ancak dosya faaliyet alanından daha dar bir faaliyet alanı belirttiğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: int a; int b; void foo(); void bar(); }; void Sample::foo() { a = 10; // geçerli, a sınıf faaliyet alanına ilişkin b = 20; // geçerli b sınıf faaliyet alanına ilişkin } void Sample::bar() { cout << a << endl; // geçerli, a sınıf faaliyet alanına ilişkin cout << b << endl; // geçerli b sınıf faaliyet alanına ilişkin foo(); // geçerli foo sınıf faaliyet alanına ilişkin } int main() { //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir üye fonksiyonun içerisinde kullanılan sınıfın veri elemanları aslında o üye fonksiyon hangi nesne ile çağrılmışsa o nesnenin veri elemanlarıdır. Zaten üye fonksiyonların nesne ile çağrılmasının nedeni budur. Örneğin: class Sample { public: int a; int b; void set(int x, int y); void disp(); }; void Sample::set(int x, int y) { a = x; b = y; } void Sample::disp() { cout << a << ", " << b << endl; } Buradaki set ve disp iye fonksiyonlarının içerisinde a ve b değişkenleri aslında bu üye fonksiyonlar hangi nesneyle çağrılırsa onun a ve b veri elemanlarıdır. Örneğin: Sample s, k; s.set(10, 20); k.set(30, 40); Burada s.set(10, 20) çağırısında 10 değeri s'in a elemanına, 20 değeri ise s'in b elemanına atanmaktadır. Benzer biçimde k.set(30, 40) çağrısında da 30 ve 40 k nesnesinin a ve b elemanlarına atanmaktadır. Örneğin: s.disp(); k.disp(); Burada s.disp() çağrısındaki a ve b s nesnesinin a ve b elemanlarıdır. Bunların içeisinde 10 ve 20 bulunduğuna göre ekrana 10 ve 20 basılacaktır. k.disp() çağrısındaki a ve b ise k nesnesinin a ve b elemanlarıdır. k nesnesinin a ve b elemanlarında 30 ve 40 değerleri vardır. Bu durumda 30 ve 40 değerleri ekrana basılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: int a; int b; void set(int x, int y); void disp(); }; void Sample::set(int x, int y) { a = x; b = y; } void Sample::disp() { cout << a << ", " << b << endl; } int main() { Sample s; Sample k; s.set(10, 20); k.set(30, 40); s.disp(); k.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bu durumda sınıfın veri elemanları ve üye fonksiyonları ne anlama gelmektedir? İşte sınıfın veri elemanları aslında üye fonksiyonlar tarafından ortak bir biçimde kullanılan data'ları temsil etmektedir. Sınıfın üye fonksiyonları aynı nesnenin veri elemanlarını ortak olarak kullanmaktadır. Bir üye fonksiyon o elemanlara değer yerleştirdiğinde diğer bir üye fonksiyon o değerleri kullanabilir. Sınıflar belli bir konuda faydalı işlemler yapan üye fonksiyonlardan oluşmaktadır. Üye fonksiyonlar da bu faydalı işlemleri ortak veri elemanlarını kullanarak gerçekleştirmektedir. NYPT'de sınıflar belli bir konu (kavram) üzerinde işlemler yapan üye fonksiyonlar gibi değerlendirilebilir. Örneğin Date isimli bir sınıf tarih konusunda yararlı birtakım işlemleri yapan üye fonksiyonlara sahip olabilir. String isimli bir sınıf bir yazı üzerinde işelemler yapan üye fonksiyonlara sahip olabilir. Benzer biçimde SerialPort isimli bir sınıf seri port işlemlerini yapan üye fonksiyonlara sahip olabilir. NYPT'de artık fonksiyonlar temelinde konuşmak yerine sınıflar temelinde konuşulur. Böylece "birbirinden farklı çok sayıda fonksiyon var" algısı yerine "belli işlemleri yapan belli sınıflar var" algısı oluşturulmaktadır. Bu da tasarım ve yazım işlemlerini algısal bakımdan kolaylaştırmaktadır. O halde biz NYPT'de bir sınıf ile karşılaştığımızda "bu sınıfın belli bir konu üzerinde faydalı işlemler yapan üye fonksiyonlara sahip olduğunu, bu üye fonksiyonların da sınıfın veri elemanlarını (yani nesnesinin parçalarını) ortak biçimde kullanıdığını" anlamalıyız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın bir üye fonksiyonu başka bir üye fonksiyonunu doğrudan (niteliklendirmeden) çağırabilir. Çünkü üye fonksiyonlar da sınıf faaliyet alanındadır. Bu durumda çağıran üye fonksiyon hangi nesneyle çağrılmışsa çağrılan üye fonksiyonun da aynı nesneyle çağrılmış olduğu kabul edilir. Örneğin: class Sample { public: int a; int b; void set(,nt x, int y); void disp(); }; void Sample::set(int x, int y) { a = x; b = y; disp(); // set hangi nesneyle çağrılmışsa disp de o nesneye çağrılmış gibi işlem görür } void Sample::disp() { cout << a << ", " << b << endl; } Burada set fonksiyonu şöyle çağrılmış olsun: Sample s; s.set(10, 20); Bu durumda set içerisindeki a ve b elemanları s nesnesinin a ve b elemanlarıdır. set üye fonksiyonu disp üye fonksiyonunu çağırmıştır. İşte burada sanki disp üye fonksiyonu da s ile çağrılmış gibi bir etki oluşacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ using namespace std; class Sample { public: int a; int b; void set(int x, int y); void disp(); }; void Sample::set(int x, int y) { a = x; b = y; disp(); } void Sample::disp() { cout << a << ", " << b << endl; } int main() { Sample s; s.set(10, 20); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 26. Ders 13/11/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın üye fonksiyonları da overload edilebilir. Yani bir sınıf içerisinde parametrik yapıları farklı olan aynı isimli üye fonksiyonlar bulunabilir. Sınıfların da birer faaliyet alanı belirttiğini anımsayınız. Dolayısıyla farklı faaliyet alanlarında aynı isimli ve parametrik yapıya ilişkin (aynı imzaya ilişkin) fonksiyonlar bulunabildiğine göre farklı sınıflarda aynı isimli ve aynı parametrik yapıya sahip üye fnksiyonlar buunabilir. Örneğin: class Sample { public: void foo(int a); void foo(double a); void foo(const char *str); //... }; Bu durumda "overload resolution" işlemi daha önce açıkladığımız kurallarla yürütülmektedir. Yani önce aday fonksiyonlar seçilmekte, sonra onların arasından uygun fonksiyonlar ve nihayet en uygun fonksiyon seçilmeye çalışılmaktadır. Biz üye fonksiyonu aynı türden bir sınıf nesnesi ile çağırdığımızda sınıftaki aynı isimli tüm üye fonksiyonlar aday fonksiyonlar olarak seçilmektedir. Örneğin: class Sample { public: void foo(int a); void foo(double a); void foo(const char *str); //... }; Sample s; s.foo(10); // foo(int) s.foo(12.3); // foo(double) (Daha ileride sınıflarda türetme işlemlerini göreceğiz. Overload resolution sürecinde aday fonksiyonlar aşağıdan yukarı ismin ilk bulunduğu sınıftan seçilmektedir. Taban sınıflardaki aynı isimli fonksiyonlar aday fonksiyon olarak alınmamaktadır. C++'taki Java ve C# dillerindne bu bakımdan farklıdır.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: void foo(int a); void foo(double a); void foo(const char *str); }; void Sample::foo(int a) { cout << "foo(int)" << endl; } void Sample::foo(double a) { cout << "foo(double)" << endl; } void Sample::foo(const char *tr) { cout << "foo(const char *)" << endl; } int main() { Sample s; s.foo('a'); // Sample::foo(int) çağrılır s.foo(3.14); // Sample::foo(double) çağrılır s.foo("ankara"); // Sample::foo(const char *) çağrılır return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesi yaratıldığında derleyici tarafından otomatik olarak çağrılan sınıfın özel üye fonksiyonlarına "yapıcı fonksiyonlar (constructors)" denilmektedir. Yapıcı fonksiyonlar tipik olarak birtakım ilk işlemleri yapmak ve sınıfın veri elemanlarına birtakım ilkdeğerleri vermek için kullanılmaktadır. Yapıcı fonksiyonlar sınıf ismiyle aynı isimli olan üye fonksiyonladır. Örneğin class Sample { public: Sample(); // yapısı fonksiyon void foo(); // normal bir üye fonksiyon //... }; Sample::Sample() { //... } void Sample::foo() { //... } Yapıcı fonksiyonların geri dönüş değerleri diye kavramları yoktur. Bu nedenle yapıcı fonksiyonlarda geri dönüş değerlerinin türü yerine hiçbir şey yazılmaz. Geri dönüş değerinin türü yerine void yazmak da geçerli bir durum değildir. Çünkü "void" geri dönüş değerinin olmadığı anlamına gelmektedir. Halbuki yapıcı fonksiyonların böyle bir kavramları yoktur. Yapıcı fonksiyonlar içerisinde return deyimini kullanabiliriz. Ancak onun yanına bir ifade yazamayız. Yapıcı fonksiyonlar ilgili sınıf türündne bir nesne yaratıldığında derleyici tarafından otomatik olarak çağrılmaktadır. Örneğin: Sample s; // yapıcı fonksiyon çağrılacaktır Böylece bir nesne yaratıldığında o nesnenin ilişkin olduğu sınıfın hedeflediği amaçlar için birtakım ilk işlemler arka planda yapılmış olur. Örneğin: class SerialPort { //... }; //... SerailPort sp; Burada yaratılan SerialPort nesnesi için yapıcı fonksiyon çağrılacaktır. Bu fonksiyon da seri port işlemleri için gerekli birtakım ilk işlemleri yapabilecektir. Yapıcı fonksiyon içerisinde kullanılan sınıfın veri elemanarı o anda yaratılmış olan nesnenin veri elemanlarıdır. Örneğin: class Sample { public: Sample(); void disp(); int a; int b; }; Sample::Sample() { a = 10; b = 20; } void Sample::disp() { cout << a << ", " << b << endl; } //... Sample s; s.disp() Burada s nesnesi yaratıldığında yapıcı fonksiyon çağrılacaktır. Onun içerisindeki a ve b elemanları s nesnesinin veri elemanıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(); void disp(); int a; int b; }; Sample::Sample() { a = 10; b = 20; } void Sample::disp() { cout << a << ", " << b << endl; } int main() { Sample s; s.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın yapıcı fonksiyonları overload edilebilir. Yani sınıfta farklı parametrik yapılara ilişkin birden fazla yapıcı fonksiyon bulunabilir. Örneğin: class Sample { public: Sample(); Sample(int a); Sample(double a); Sample(const char *str); //... }; Parametresi olmayan yapıcı fonksiyona "default yapıcı fonksiyon (default constructor)" denilmektedir. Örneğin: class Sample { public: Sample(); // default constructor Sample(int a); Sample(double a); Sample(const char *str); //... }; Eğer yapıcı fonksiyonun bütün parametreleri default değer almışsa bu yapıcı fonksiyon da "default yapıcı fonksiyon" olarak kullanılabilmektedir. Örneğin: class Sample { public: Sample(int a = 0); // bu yapıcı fonksiyon parametre aldığı halde defauult yapıcı fonksiyon olarka da kullanılabilir Sample(double a); Sample(const char *str); //... }; Bir sınıf nesnesi yaratılırken eğer isimden sonra hiç parantez açılmamışsa bu durumda derleyici nesne için sınıfın default yapıcı fonksiyonunu (default constructor) çağırır. Örneğin: Sample s; // default yapıcı fonksiyonu çağrılır Eğer değişken isminden sonra parantezler açılırsa parantezlerin içerisine argümanlar yerleştirilir. Bu durumda çağrılacak yapıcı fonksiyon overload resolution kuralına göre tespit edilir. Örneğin: class Sample { public: Sample(); // default constructor Sample(int a, int b); Sample(double a, double b); //... }; //... Sample s(10, 20); // int, int parametreye sahip yapıcı fonksiyon çağrılacaktır Bu durumda sınıfın bütün yapıcı fonksiyonları aday fonksiyonlardır. Bunlar arasından uygun olanlar seçilir ve uygun olanlar arasından da en uygun yapıcı fonksiyon seçilmeye çalışılır. C++11 ile birlikte "uniform initializer" sentaks dile eklendikten sonra nesne yaratırken normal parantezler yerine artık küme parantezleri de kullanılabilmektedir. Örneğin: Sample a{10, 20}; // C++11 ve sonrasında geçerli Ancak uniform initializer sentaksında "daraltıcı dönüştürmelere (narrowing conversion)" izin verilmediğini anımsayınız. Tabii daha önceden de belirttiğimiz gibi önce overload resolution işlemi yapılıp en uygun fonksiyon tespit edilir. Sonra bu fonksiyonun daraltıcı dönüştürmeye yol açıp açmadığına bakılır. Örneğin: class Sample { public: Sample(); Sample(int a); //... }; //... Sample s{3.14}; Burada overload resolution işlemi sonucunda int parametreli fonksiyon seçilecektir. Ancak bu fonksiyon daraltıcı dönüştürme uyguladağı için tanımlama error ile sonuçlanacaktır. Verilen hata mesajının bu biçimde olduğuna dikkat ediniz. Default yapıcı fonksiyon çağrılacak biçimde nesne yaratma işlemi aşağıdaki gibi yapılamamaktadır: Sample s(); Çünkü bu bir nesne tanımlaması değil bir prototip bildirimidir. Tabii C++11 ile birlikte aşağıdaki tanımlama default yapıcı fonksiyonun çağrılması için geçerlidir: Sample s{}; Çünkü böyle bir prototip bildirim sentaksı yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: int a; int b; Sample(); Sample(int x, int y); }; Sample::Sample() { cout << "default constructor" << endl; a = 0; b = 0; } Sample::Sample(int x, int y) { cout << "int, int constructor" << endl; a = x; b = y; } int main() { Sample s; // default constructor Sample k(10, 20); // int parametreli constructor cout << "ok" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yerel sınıf nesneleri için yapıcı fonksiyonlar programın akışı nesnenin tanımlandığı noktaya geldiğinde çağrılmaktadır. Örneğin: int main() { //... Sample s; // akış bu noktaya geldiğinde s için default yapıcı fonksiyon çağrılır //... Sample k; // akış bu noktaya geldiğinde k için default yapıcı fonksiyon çağrılır //.... return 0; } Tabii birden fazla nesne aynı bildirimde de tanımlanabilir. Bu durumda yapıcı fonksiyonlar tanımlama sırasına göre çağrılacaktır. Örneğin: Sample s, k; // önce s için sonra k için yapısı fonksiyon çağrılır Burada önce s için sonra k için yapısı fonksiyon çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Global sınıf nesneleri için yapıcı fonksiyonlar akış henüz main fonksiyonuna girmeden çağrılmaktadır. Çağrılma sırası kaynak dosyadaki (translation unit) yuakarıdan aşağıya ve soldan sağa yönüne göredir. Yani kaynak kodda daha önce tanımlanan nesnenin yapıcı fonksiyonu daha sonra tanımlanandan önce çağrılır. Ancak proje birden fazla kaynak dosyadan oluşyorsa kaynak dosyalardaki global nesnelerin birbirilerine göre yapıcı fonksiyon çağrılma sırası standartlarda "belirsiz (unspecified)" bırakılmıştır. Örneğin: // a.cpp Sample g_a; Sample g_b; // b.cpp Sample g_x Sample g_y; Burada g_a nesnesi için yapıcı fonksiyon g_b nesnesi için yapıcı fonksiyondan daha önce çağrılır. Benzer biçimde g_x için yapıcı fonksiyon g_y için yapıcı fonksiyondan daha önce çağrılacaktır. Ancak kaynak dosyalar arasındaki sıra konusunda bir belirleme yapılmamıştır. Aşağıdaki örneği çalıştırarak durumu inceleyiniz. Ekranda şunları göreceksiniz: Sample(int): 100 Sample(int): 200 Sample(int): 300 main begins... Sample(int): 10 Sample(int): 20 main continues... Sample(int): 30 main ends... --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int x); //... }; Sample::Sample(int x) { cout << "Sample(int): " << x << endl; } Sample x{100}; Sample y{200}; int main() { cout << "main begins..." << endl; Sample s{10}, k{20}; cout << "main continues..." << endl; Sample m{30}; cout << "main ends..." << endl; return 0; } Sample z{300}; /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi static yerel sınıf nesneleri için yapıcı fonksiyonlar ne zaman çağrılmaktadır? İşte static yerel sınıf nesneleri için yapıcı fonksiyonlar "programın akışı nesnenin tanımlandığı noktaya ilk kez geldiğinde yalnızca bir kez" çağrılmaktadır. Bu cümleden iki sonuç çıkmaktadır: 1) Eğer akış static yerel sınıf nesnesinin tanımlandığı noktaya hiç gelmezse onun için yapıcı fonksiyon hiç çağrılmayacaktır. 2) Akış static yerel sınıf nesnelerinin tanımlandığı noktaya birden fazla kez gelirse yalnızca ilk gelişinde nesne için yapıcı fonksiyon çağrılacaktır. Aşağıdaki örneği çalıştırarak sonucu inceleyiniz. Ekrana şunları göreceksiniz: Sample(int): 100 Sample(int): 200 Sample(int): 300 main begins... Sample(int): 10 Sample(int): 20 main continues... Sample(int): 30 foo begins... Sample(int): 1000 foo ends... foo begins... foo ends... main ends... --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int x); //... }; Sample::Sample(int x) { cout << "Sample(int): " << x << endl; } Sample x{100}; Sample y{200}; void foo() { cout << "foo begins..." << endl; static Sample s{1000}; cout << "foo ends..." << endl; } int main() { cout << "main begins..." << endl; Sample s{10}, k{20}; cout << "main continues..." << endl; Sample m{30}; foo(); foo(); cout << "main ends..." << endl; return 0; } Sample z{300}; /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyonun parametre değişkeni bir sınıf türünden olabilir. Örneğin: void foo(Sample k) { //... } Bu durumda biz fonksiyonu tipik olarak aynı sınıf türünden bir nesneyle çağırırız. Örneğin: Sample s; foo(s); Parametre değişikenlerinin fonksiyon çağrıldığında yaratıldığını anımsayınız. O zaman bu örnekte foo fonksiyonunun k isimli parametre değişkeni için yapıcı fonksiyon bu fonksiyon çağrıldığında çağrılacaktır. Tabii fonksiyon her çağrıldığında yeni bir parametre değişkeni yaratıldığına göre yine yapıcı fonksiyon çağrılacaktır. Ancak bu tür durumlarda parametre değişkenleri için ismine "kopya yapıcı fonksiyonu (copy constructor)" denilen özel bir yapıcı fonksiyon çağrılmaktadır. Bu konu ileride ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Eğer programcı sınıf için hiçbir yapıcı fonksiyon yazmamışsa (standart terminolojisi ile sınıfın "user provided" yapıcı fonksiyonu yoksa) bu durumda derleyici sınıf için default yapıcı fonksiyonu public bölümde içi boş olarak inline biçimde (mümkünse aynı zamanda constexpr biçiminde) kendisi tanımlamaktadır. Böylece biz bir sınıf için hiçbir yapıcı fonksiyon yazmadıysak o sınıf türünden default yapıcı fonksiyonun çağrılacağı bir nesne tanımlayabiliriz. Eğer biz bir sınıf için herhangi bir yapıcı fonksiyon yazmışsak bu durumda derleyici default yapıcı fonksiyonu kendisi yazmamaktadır. Örneğin: class Sample { public: Sample(int a); //... }; //... Sample s; // geçersiz! programcı sınıf için bir yapıcı fonksiyon yazmış, derleyici default yapıcı fonksiyonu yazmaz Burada programcı sınıf için bir yapıcı fonksiyon tanımladığından dolayı derleyici default yapıcı fonksiyonu kendisi tanımlamayacaktır. Örneğin: class Sample { public: void foo(); void bar(); //... }; //... Sample s; // geçerli! programcı sınıf için bir yapıcı fonksiyon yazmamış, derleyici default yapıcı fonksiyonu içi boş olarak kendisi yazacak, Sample k{10}; // geçersiz! derleyici yalnızca default yapıcı fonksiyonu biizm için yazmaktadır Burada programcı sınıf için hiçbir yapıcı fonksiyon yazmadığından derleyici default yapıcı fonksiyonu kendisi içi boş olarak yazmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { //... }; class Mample { public: Mample(int a) {} }; int main() { Sample s; // geçerli, default yapıcı fonksiyon derleyici tarafından yazılmış Mample k; // geçersiz! sınıf için programcı yapıcı fonksiyon yazdığından dolayı artık derleyici // default yapıcı fonksiyonu kendisi yazmaz. Mample m(10); // geçerli return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yapıcı fonksiyonlar da default argüman alabilmektedir. Default argümanların imzayı değiştirmediğini anımsayınız. Örneğin: class Sample { public: Sample(); // default constructor Sample(int a = 0); // default constructor olarak kullanılabilir //... }; Burada bu iki üye fonksiyonun birlikte bulunmasının bir sakıncası yoktur. Çünkü bunların parametrik yapıları farklıdır. Fakat örneğin: Sample s; // geçersiz! iki constructor da en uygun ve en uygun Burada derleyici iki yapıcı fonksiyondan birini tercih etmez. Overload resolution kurallarına göre bu durum "ambiguity" oluşturmaktadır. Tabii bu durumda int parametreli yapıcı fonksiyonun default değer almasının da bir anlamı yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta sınıfların yapıcı fonksiyonları programcı tarafından çağrılabilen fonksiyonlar değildir. Yine C++'ta yapıcı fonksiyonların adresleri de alınamamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte "açıkça default hale getirilmiş default yapıcı fonksiyon (explicitly defaulted default constructor)" biçiminde bir kavram da dile eklenmiştir. Bir default yapıcı fonksiyon için prototipten sonra " = default" sentaksı kullanılırsa bu sentaks "default yapıcı fonksiyon içi boş olarak derleyici tarafından yazılsın" anlamına gelmektedir. Örneğin: class Sample { public: Sample() = default; Sample(int a, int b); //... }; Burada sınıfın başka bir yapıcı fonksiyonu olduğu için derleyici default yapıcı fonksiyonu kendiliğinden yazmayacaktır. İşte " = default" sentaksı bunu sağlamaktadır. Tabii "= default" dentaksı yerine biz zaten içi boş bir default yapıcı fonksiyonu kendimiz de oluşturabilirdik. Ancak teknik anlamda aşağıdaki iki tanımlama arasında bazı ince farklılıklar vardır: Sample() = default; Sample() {} Gövdesi boş default yapıcı fonksiyon teknik olarak "programcı tarafından yazılmış (user provided)" yapıcı fonksiyon olarak ele alınmaktadır. Halbuki açıkça default hale getirilmiş yapıcı fonksiyon bu biçimde ele alınmamaktadır. Bu ayrım da bazı konularda bazı ince anlam farklılıklarına yol açmaktadır. Bu konu ileride başka konular içerisinde ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 27. Ders 15/11/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte "açıkça silinmiş default yapıcı fonksiyon (explicitly deleted default constructor)" sentaksı da dile eklenmiştir. Bir default yapıcı fonksiyonda " = delete" sentaksı kullanılırsa artık derleyici sınıfın programcı tarafından yazılmış olan bir yapıcı fonksiyonu olmasa bile default yapıcı fonksiyonunu kendisi yazmaz. Örneğin: class Sample { public: void foo(); void bar(); //... }; Burada sınıf için hiçbir yapıcı fonksiyon yazılmadığı için derleyici default yapıcı fonksiyonu içi boş olarak yazacaktır. Ancak biz default yapıcı fonksiyonu açıkça silinmiş hale getirirsek derleyici artık default yapıcı fonksiyonu bizim yazmayacaktır. Örneğin: class Sample { public: Sample() = delete; // açıkça silinmiş default yapıcı fonksiyon void foo(); void bar(); //... }; Tabii bu durumda biz bu sınıf türünden nesne de yaratamayız. Çünkü nesne yaratımı sırasında sınıfın yapıcı fonksiyonu çağrılacaktır. Bu sınıfta yapıcı fonksiyon olmadığı için yaratım girişimi error ile sonuçlanacaktır. Örneğin: Sample s; // geçersiz! sınıfın default yapıcı fonksiyonu yok Pekiyi açıkça silinmiş default yapıcı fonksiyon oluşturmaya ne gerek vardır? Bir sınıf türürnden hiçbir nesne tanımlayamadıktan sonra sınıfın bulumasının bir anlamı olabilir mi? Şimdiye kadar gördüğümüz konular dikkate alındığında sanki bunun bir anlamı yokmuş gibi bir fikir oluşabilir. Ancak ileride görülecek bazı konulardan sonra bunun bazı kullanımlarının olabileceğini göreceksiniz. Aşağıdaki sınıf bildiriminde default yapıcı fonksiyonun açıkça silinmesi geçerli bir durum oluştursa da anlamsızdır: class Sample { public: Sample() = delete; // geçerli ama anlamsız Sample(int a); void foo(); void bar(); //... }; Burada zaten programcı sınıfa bir yapıcı yerleştirdiği için default yapıcı fonksiyon derleyici tarafından yazılmayacaktır. Zaten yazılmayacak olan fonksiyonun yazılmamasını istemenin bir anlamı yoktur. Açıkça deleted hale getirme dışında C++11 ve sonrasında standartlarda bazı anlatım değişikliklerine gidilmiş ve "özel üye fonksiyonların (special member functions)" bazı durumlarda derleyici tarafından "deleted" yapılacağı standartlara eklenmiştir. Yani örneğin biz sınıfın default yapıcı fonksiyonunu açıkça deleted yapmasak bile bazı koşullar altında derleyici kendisinin yazdığı bu default yapıcı fonksiyonu deleted hale getirmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample() = delete; //... }; int main() { Sample s; // geçersiz! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir sınıf için hiçbir yapıcı fonksiyon yazmamışsak ya da bir yapıcı fonksiyon içerisinde sınıfın veri elemanlarına hiç değer atamadıysak nesnenin bu veri elemanlarında hangi değerler bulunacaktır? Örneğin: class Sample { public: int a, b; void foo(); void bar(); }; //... Sample s; // s'in a ve b elemanlarında hangi değerler vardır? Bu konu ileride MIL sentaksı içerisinde daha detaylı bir biçimde açıklanacaktır. Ancak burada pratik bazı şeyler söylmek istiyoruz. Eğer sınıfın yapıcı fonksiyonlarında veri elemanlarına değer atanmamışsa kabaca (ayrıntılar ileride ele alınacak) şu durumlar söz konusudur: 1) Eğer sınıf nesnesi yerel ise sınıfın temel türlere ilişkin veri elemanlarına herhangi bir değer atanmaz. Dolayısıyla o elemanlarda çöp değerler bulunur. Yani yukarıdaki örnekte s nesnesi yerel ise tanımlama sonrasında a ve b veri elemanlarında çöp değerler (indeterminate values) bulunacaktır. (Buna standartlarda "default-initilize" denilmektedir: 9.4.1-6) 2) Eğer sınıf nesnesi statik ömürlüyse (global ya da static yerel) sınıfın temel türlere ilişkin veri elemanlarına 0 değeri yerleştirilmektedir. (Buna da standartlarda "zero-initialize" denilmektedir: 9.4.1-6). --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standartlarda "kullanıcı tarafından yazılmış olan default yapıcı fonksiyon (user-provided default constructor)" terimi default yapıcı fonksiyonun programcı tarafından açıkça yazıldığını belirtmektedir. Açıkça default hale getirilmiş (explicitly defaulted) ve silinmiş (deleted) default yapıcı fonksiyonlar "user-provided" yapıcı fonksiyonlar" değildir. Bir sınıfta hiçbir yapıcı fonksiyon yoksa derleyicinin yazdığı default yapıcı fonksiyon da "user-provided" değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfların yapıcı üye fonksiyonlarının temelde iki işlevi vardır: 1) Sınıfın konusuyla ilgili bazı ilk işlemleri yapmak. 2) Sınıfın veri elemanlarına bazı ilkdeğeri vermek. Sınıfın yapıcı fonksiyonlarının programcıdan aldığı değerleri sınıfın veri elemanlarına yerleştirmesi çok karşılaşılan bir durumdur. Bu biçimde basit set işlemleri yapan yapıcı fonksiyonların inline biçimde tanımlanması iyi bir tekniktir. Aşağıdaki örnekte Date sınıfının yapıcı fonksiyonları sınıfın veri elemanlarına ilkdeğerlerini vermektedir. Bu örnekte üç parametreli yapıcı fonksiyonun aldığı bu değerleri sınıfın veri elemanlarına yerleştirdiğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Date { public: Date() { day = 1; month = 1; year = 1900; } Date(int d, int m, int y) { day = d; month = m; year = y; } void disp(); int day, month, year; }; void Date::disp() { cout << day << '/' << month << '/' << year << endl; } int main() { Date d; Date k(15, 11, 2023); d.disp(); k.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte Complex sınıfının real ve imag isimli iki veri elemanı bulunmaktadır. Sınıfın iki parametreli yapıcı fonksiyonunun dış dünyadan aldığı değerleri bu veri elemanlarına yerleştirdiğine dikkat ediniz. Sınıfın defautl yapıcı fonksiyonu açıkça defaulted hale getirilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Complex { public: Complex() = default; Complex(double r, double i) { real = r; imag = i; } void disp(); double real; double imag; }; void Complex::disp() { cout << real << "+" << imag << 'i' << endl; } int main() { Complex z; // dikkat! veri elemanlarında çöp değerler var Complex k{2, 4}; k.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte "default member initializer" ismiyle sınıfın veri elemanlarına sınıf bildirimi içerisinde ilkdeğer verme sentaksı da dile eklenmiştir. Aslında sınıfın veri elemanalarına sınıf bildirimi içerisinde ilkdeğer verme zaten Java ve C# gibi dillerde çok önceden beri bulunuyordu. C++ bu özelliği C++11 ile bünyesine katmıştır. Bu özelliğe göre biz sınıfın veri elemanalrına yapıcı fonksiyonun yanı sıra istersek artık sınıf bildirimi içerisinde de ilkdeğer verebiliriz. Örneğin: class Complex { public: Complex() {} double real = 0; double imag = 0; }; Anımsanacağı gibi C'de yapılar için böyle bir sentaks yoktur. Pekiyi bu sentaks ne anlama gelmektedir? Aslında derleyiciler bu durumda veri elemanlarına verilen ilkdeğerlerin hepsini bildirim sırasına göre atama deyimlerine dönüştürerek sınıfın tüm yapıcı fonksiyonlarının ana bloğunun başına gizli bir biçimde yerleştirmektedir. Yani bu ilkdeğerler aslında sınıfın tüm yapıcı fonksiyonlarında ilgili veri elemanlarına atanmış gibi bir durum söz konusu olmaktadır. Standartlar bir veri elemanına hem sınıf bildirimi içerisinde hem de yapıcı fonksiyonda ilkdeğer verildiğinde sınıf bildiriminde verilen ilkdeğerlerin dikkate alınmayacağını (ignore edileceğini) belirtmektedir. Bu anlatım verilen ilkdeğerilerin atama deyimlerine dönüştürülerek tüm yapıcı fonksiyonların başına yerleştirilmesi anlatımı ile aynı anlama gelmektedir. Pekiyi eskiden olmayan bu özelliğin C++11 ile eklenmesinin ne amacı vardır? İşte sınıfın çok veri elemanı ve çok yapıcı fonksiyonu olabilir. Bazı veri elemanlarına her yapıcı fonksiyonda aynı ilkdeğerler veriliyor olabilir. Bu özellik sayesinde programcı bu elemanlara tüm yapıcı fonksiyonalar içerisinde aynı değeri atamak yerine sınıf bildirimi içerisinde bir kez bu atamayı yapabilir. Sınıf bildirimi içerisinde veri elemanlarına verilen ilkdeğerlerin sabit ifadesi olması gerekmez. Aslında bu ilkdeğer verme işlemi sanki yapıcı fonksiyon içerisinde yapılıyormuş gibi düşünülmelidir. Dolayısıyla isim araması buna göre yapılmaktadır. Yani bu atama işlemleri yapıcı fonksiyon içerisinde yapılabiliyorsa sınıf bildirimi içerisinde de yapılabilmektedir. Örneğin: class Sample { public: int a = 10; int b = a + 1; //... }; //... Sample s; Burada s nesnesinin a elemanında 10, b elemanında 11 bulunacaktır. Aşağıdaki örnekte s nesnesinin a elemanında 1, k nesnesinin a elemanında 2 değeri bulunacaktır: int g_x = 0; class Sample { public: int a = ++g_x; //... }; int main() { Sample s, k; cout << s.a << endl; // 1 cout << k.a << endl; // 2 return 0; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarında nesnelere "default olarak ilkdeğer vermekte" anlatımı kolaylaştırmak için üç terim kullanılmaktadır: 1) Default-initialization 2) Zero-initialization 3) Value-initialization Bu tanımlar genel olarak nesneye belli bir ilkdeğer verilmediğinde nesnenin nasıl ilkdeğer alacağı konusunda kullanılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarında bir nesneye ilkdeğer verilmesi için kullanılan "default-initialize" terimi kabaca şu anlama gelmektedir: 1) Eğer nesne bir sınıf türündense nesne için sınıfın default yapıcı fonksiyonu çağrılır. 2) Eğer nesne bir dizi türündense dizinin her elemanı için "default ilkdeğer verme" işlemi uygulanır. 3) Diğer durumlarda (yani nesne temel türlerdense, enum türündense vs.) nesneye herhangi bir ilkdeğer verilmez. Yani nesnede çöp değer kalır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarında bir nesneye ilkdeğer verilmesi için kullanılan "zero-initialize" terimi ise kabaca şu anlama gelmektedir: 1) Eğer nesne temel türlerdense ya da gösterici türündense (standartlarda bu kümeye "scaler types" denilmektedir) nesneye sanki 0 değeri atanıyor gibi işlem yapılır. (Göstericilere 0 değerinin atanmasının onlara NULL adresin atanması anlamına geldiğini anımsayınız.) 2) Eğer nesne sınıf türündense nesnenin tüm statik olmayan veri elemanları "zero-initilize" edilir. Ayrıca padding bitleri de sıfırlanmaktadır. 3) Eğer nesne bir dizi ise dizinin her elemanı "zero-initilize" yapılır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarında bir nesneye ilkdeğer verilmesi için kullanılan "value-initialize" terimi ise kabaca şu anlama gelmektedir: 1) Eğer nesne bir sınıf türündense ve sınıfın default yapıcı fonksiyonu yoksa ya da default yapıcı fonksiyonu silinmişse bu durum geçersizdir (error oluşturmaktadır). 2) Eğer nesne bir sınıf türündense ve sınıfın "user-provided" default yapıcı fonksiyonu varsa nesne default-initilze edilir (yani nesnenin default yapıcı fonksiyonu çağrılır.) 3) Eğer nesne bir dizi türündense dizinin her elemanı "value-initialize" edilir. 4) Diğer bütün durumlarda nesne "zero-initliaze" edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bu terimlerde en çok "default-initialize" ile "value-initialize" birbirleriyle karışıtırılmaktadır. Default-initialize kabaca "sınıflar için default yapıcı fonksiyonu çağır, diğer nesneler için ilkdeğer verme" anlamına gelmektedir. Halbuki value-inialize kabaca "sınıflar için eğer user-provided default yapıcı fonksiyon varsa onu çağır yoksa zero-initiliaze yap" anlamına gelmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarına göre herhangi bir türden yerel nesne tanımlanırken ilkdeğer verilmezse (yani değişken isminden sonra normal parantez ya da küme parantezi kullanılmamışsa) nesne "default-initialize" edilmektedir. Örneğin: { int a; Sample s; //... } Burada a da, s de "default-initialize" edilir. Eğer nesne statik ömürlü ise ve temel türlerdense nesneye ilkdeğer verilmemesi durumunda nesne "zero-initialize" yapılmaktadır. Statik ömürlü sınıf nesneleri işin başında (program yüklenirken) önce "zero-initialize" yapılır. Sonra onlar üzerinde tanımlama biçimine göre "default-initialize", "value-initialize" işlemi uygulanır ya da uygun yapıcı fonksiyon çağrılır. int g_a; Sample g_s; Burada her iki nesnenin de global olduğunu varsayalım. Her iki nesne de başlangıçta "zero-inialize" edilecektir. Ancak g_s nesnesi için default yapıcı fonksiyon da çağrılacaktır. Böylece bu yapıcı fonksiyon sınıfın temel türlere ilişkin bazı veri elemanlarına değer atamamışsa onların içerisinde de 0 olacaktır. g_s'nin ilkdeğer verilmesini şöyle de açıklayabiliriz: Bu nesne için önce "zero-initialize" uygulanır sonra hiç parantezler kullanılmadığı için "default-initialize" uygulanır. Nesne içi boş küme parantezleri ile tanımlanırsa bu durumda "value-initialize" işlemi uygulanmaktadır. Örneğin: int a{}; Sample s{}; Burada a nesnesi "value-initialize" edilecek dolayısıyla içerisine 0 değeri yerleştirilecektir. s nesnesi de "value-initialize" edilecek. (Yani eğer sınıfın "user-provided default yapıcı fonksiyonu" varsa o çağrılacak yoksa nesne sıfırlanacak.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta yapıcı fonksiyonların sınıfın veri elemanlarına ilkdeğerlerini vermesi için ismine "MIL Sentaksı (Member Initializer Syntax)" ya standart terminolojisi ile "ctor-initializer" denilen alternatif bir sentaks da bulunmaktadır. MIL sentaksı başından beri C++'ta zaten vardı. MIL sentaksı bazı durumlarda alternatif bir ilkdeğer verme biçimi olarak, bazı durumlarda da zorunlu bir ilkdeğer verme biçimi olarak kullanılmaktadır. İleride bu sentaksın semantiğine ilişkin ayrıntılı bilgiler vereceğiz. Burada yalnızca sentaksın yalın kullanımını ve anlamını açıklayacağız. MIL sentaksı yalnızca yapıcı fonksiyonların tanımlamalarında kullanılabilen bir sentakstır. Bu sentaks başka üye fonksiyonlarda ya da global fonksiyonlarda kullanılamamaktadır. Sınıfın ismi T olmak üzere sentaksın genel biçimi şöyledir: T::T(...) : veri_elemanın_ismi(ilkeğer_ifadesi), veri_elemanın_ismi{ilkdeğer_ifadesi}, veri_elemanın_ismi{ilkdeğer_ifadesi}, ... { //... } Görüldüğü gibi yapıcı fonksiyonun kapanış parantezinden sonra önce bir ":" atomu sonra da veri elemanın ismi ve parantezler içerisinde ona verilecek ilkdeğer belirtilmektedir. C++11'e kadar buradaki parantezler normal parantez olmak zorundaydı. C++11 ile birlikte "uniform initializer syntax" dile eklenince buradaki parantezler için küme parantezleri de kullanılmaya başlandı. Yine küme parantezleri ile ilkdeğer verilirken "daraltıcı dönüştürmelere (narrowing conversion)" izin verilmemektedir. Aşağıda MIL sentaksının (ctor-initializer) kullanımına bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int x, int y); void disp(); public: int a; int b; //... }; Sample::Sample(int x, int y) : a(x), b(y) {} void Sample::disp() { cout << a << ", " << b << endl; } int main() { Sample s(10, 20); s.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 28. Ders 20/11/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- MIL sentaksı aslında sınıfın başka sınıf türünden veri elemanlarına ilkdeğer vermek için ve taban sınıfa ilkdeğer vermek için de kullanılmaktadır. Yani sentaksın semantiği biraz ayrıntılıdır. Biz burada geldiğimiz yere kadarki konuları dikkate alarak bir semantik açıklama yapacağız. MIL sentaksındaki ilkdeğer ifadesinde (parantezin içerisindeki ifadelerde) kullanılan isimlerin aranması nasıl yapılmaktadır? Yani biz burada hangi değişkenleri kulanabiliriz? İşte bu ilkdeğer ifadesinideki değişkenler sanki yapıcı fonksiyonun ana bloğunun hemen başında kullanılıyormuş gibi bir etki oluşturmaktadır. Yani isim araması sanki buradaki isimler yapıcı fonksiyonun ana bloğunun başına kullanılmış gibi yapılmaktadır. Dolayısıyla biz ilkdeğer ifadesinde parametre değişkenlerini, global değişkenleri ve sınıfın veri elemanlarını kullanabiliriz. Örneğin: class Sample { public: Sample(int x); //... int a, b; }; int g_x = 10; Sample::Sample(int x) : a(x + 1), b(g_x + 2) // geçerli {} C++'ta önce her zaman MIL sentaksında belirtilen ilkdeğer vermeler yapılır. Sonra yapıcı fonksiyonun ana bloğu çalıştırılır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta gelecekteki konuları da kapsayacak biçimde "ilkdeğer verme sırası hiçbir zaman MIL sentaksındaki sıraya göre yapılmamaktadır. Her zaman bildirimdeki sıraya göre yapılmaktadır." Örneğin: class Sample { public: Sample(); //... int a, b, c; }; Sample::Sample() : c(10), b(20), a(30) {} Burada ilkdeğer verme sırası MIL sentaksındaki sıraya göre yapılmayacaktır. İlkdeğer verme her zaman bildirdimdeki sıraya göre yapılmaktadır. Yani burada kesinlikle önce a = 30, sonra b = 20 ve sonra da c = 10 işlemi yapılacaktır. Örneğin: class Sample { public: Sample(); //... int a, b, c; }; Sample::Sample() : c(10), b(c), a(b) {} Biz sınıfın veri elemanlarına MIL sentaksı ile ilkdeğer verirken diğer veri elemanlarını kullanabiliriz. Ancak ilkdeğer verme sırası MIL sentaksındaki sıraya göre değil bildirimdeki sıraya göre yapılmaktadır. Dolayısıyla burada a ve b değişkenlerine çöp değerler atanacaktır. C'de ve C++'ta genel olarak çöp değerleri kullanmak "tanımsız davranış (undefined behavior)" kabul edilmektedir. Yukarıda da belirttğimiz gibi her zaman önce MIL sentaksındaki ilkdeğer verme işlemi bildirimdeki sıraya göre yapılmakta sonra yapıcı fonksiyonun ana bloğu çalıştırılmaktadır. Bu durumda biz sınıfın bir veri elemanına hem MIL sentaksında hem de yapıcı fonksiyonun ana bloğunda değer atarsak ana blokta atadığımız değer nihai değer olarak veri elemanında kalacaktır. Örneğin: Sample::Sample() : a(10), b(20), c(30) { a = 100; } Burada nesnenin a veri elemanında 100 bulunacaktır. C++ derleyicileri tipik olarak ""MIL sentaksındaki ilkdeğer verme işlemlerini atama deyimlerine dönüştürerek"" gizlice yapıcı fonksiyonun ana bloğunun başına yerleştirmektedir. Böylece örneğin: Sample::Sample() : a(10), b(20), c(30) { a = 100; } İşlemi için derleyici aşağıdaki gibi bir kod üretmektedir: Sample::Sample() { a = 10; // derleyici tarafından gizlice yerleştiriliyor b = 20; // derleyici tarafından gizlice yerleştiriliyor c = 30; // derleyici tarafından gizlice yerleştiriliyor a = 100; } C++ standartlarına göre eğer bir veri elemanına MIL sentaksında ilkdeğer verilmemişse o veri elemanı henüz akış yapıcı fonksiyonun ana bloğuna girmeden "default-initialize" edilmektedir. Örneğin: class Sample { public: Sample(); //... int a, b, c; }; Sample::Sample() : c(10), a(30) { b = 20; } Burada standartlara göre şu işlemler gerçekleşecektir: 1) Önce a'ya 30 atanır. 2) b deafult-initialize edilir. b temel türlerden olduğu için onun default-initialize edilmesi ona herhangi bir atamanın yapılmayacağı anlamına gelmektedir. Yani onun içerisinde çöp bir değer olacaktır. 3) c'ye 10 atanır. 4) Yapıcı fonksiyonun ana bloğu çalıştırılır. Orada b'ye 20 atanmıştır. Buradan birkaç sonuç çıkarabiliriz: 1) Sınıfın temel türlere ilişkin veri elemanlarına MIL sentaksıyla ilkdeğer verme ile yapıcı fonksiyonun ana bloğu içerisinde ilkdeğer verme arasında bir farklılık yoktur. (Ancak sınıfın başka sınıf türünden veri elemanları söz konusu olduğunda bu durum değişmektedir.) 2) Biz sınıfın temel türlere ilişkin veri elemanlarına ne MIL sentaksında ne de yapıcı fonksiyonun ana bloğunda ilkdeğer vermemişsek bu durumda o veri elemanında çöp değer bulunacaktır. C++11 ile birlikte sınıfın veri elemanlarına sınıf bildirimi içerisinde ilkdeğer verilebiliyordu (default member initializer). Bu ilkdeğerlerin de derleyiciler tarafından sınıfın yapıcı fonksiyonlarının başına taşındığından bahsetmiştik. Pekiyi hem bir veri elemanına sınıf bildirimi içerisinde ilkdeğer verilmişse hem de MIl sentaksında ilkdeğer verilmişse veri elemanında hangi değer bulunacaktır? C++ standartları bu durumda sınıf bildiriminde verilen ilkdeğerin dikkate alınmayacağını yani MIL sentaksındaki değerin veri elemanında gözükeceğini belirtmektedir. Örneğin: #include using namespace std; class Sample { public: Sample(); void disp(); public: int a = 10; //... }; Sample::Sample() : a(100) {} void Sample::disp() { cout << a << endl; } int main() { Sample s; s.disp(); // 100 return 0; } Pekiyi bu durumda sınıfın veri elemanlarına MIL sentaksında ilkdeğer vermeye gerek var mıdır? Yukarıdaki anlatımdan da anlaşılacağı gibi sınıfın temel türlerden veri elemanları için MIL sentaksının kullanılmasına gerek yoktur. Ancak ileride ele alacağımız konularla birlikte MIL sentaksının bazı durumlarda mecburen kullanılması gerektiğini göreceksiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte dile "delege yapıcı fonksiyon çağrımı (delagating constructors)" özelliği de eklenmiştir. Aslında bu özellik Java ve C# gibi dillerde zaten hep vardı. C++'a C++11 ile eklendi. Delege yapıcı fonksiyon çağrımı aslında bir yapıcı fonksiyonun diğerini çağırabilmesi özelliğidir. Sınıfın çok sayıda yapıcı fonksiyonu varsa ve bu fonksiyonlarda ortak birtakım işlemler ve ilkdeğer vermeler yapılıyorsa eskiden kod tekrarını engellemek için normal üye fonksiyonlar kullanılıyordu. Delege yapıcı fonksiyon çağırımı sayesinde artık çoğu kez buna gerek kalmamaktadır. Örneğin: class Sample { public: Sample(); Sample(int x); void disp(); //... int a; }; Burada iki yapıcı fonksiyonda da ortak şeylerin yapıldığını varsayalım. Eskiden kod tekrarını engellemek için aşağıdaki gibi bir yöntem kullanılıyrodu: Sample::Sample() { common_initialize(); //... } Sample::Sample(int a) { common_initialize(); //... } Halbuki artık bir yapıcı fonksiyon diğerini çağırabildiği için böyle bir ortak fonksiyon oluşturmaya gerek kalmamaktadır. Delege yapıcı fonksiyon çağrımı MIL sentaksında yapıcı fonksiyonun belirtilmesiyle sağlanmaktadır. Örneğin: Sample::Sample() : Sample(0) { //... } Burada default yapıcı fonksiyon int parametreli yapıcı fonksiyonu çağırmıştır. MIL sentaksında delege yapıcı fonksiyon kullanılacaksa artık başka hiçbir ilkdeğer verme uygulanamaz. Yani MIL sentaksının yalnızca delege yapıcı fonksiyon çağrımını içermesi gerekmektedir. Örneğin: Sample::Sample() : Sample(0), a(10) // geçersiz! yalnızca delege yapıcı fonksiyon çağrımı bulunabilir {} Bir yapıcı fonksiyonun MIL sentaksında delege yapıcı fonksiyon çağrımı varsa önce delege yapıcı fonksiyon çağrılır sonra o yapıcı fonksiyonun ana bloğu işletilir. Tabii bir yapıcı fonksiyon başka bir yapıcı fonksiyonu o da başka bir yapıcı fonksiyonu çağırabilir. Ancak bu çağrımlar sırasında döngsüsel bir durum durum oluşursa programın akışı sonsuz döngüye girebilir. Bu durum genel olarak "tanımsız davranış (undefined behavior)" oluşturmaktadır. Yukarıda da belirttiğimiz gibi delege yapıcı fonksiyon çağrımı özellikle sınıfın çok fazla yapıcı fonksiyonu olduğunda ve bu yapıcı fonksiyonlarda orta birtakım şeyler yapıldığında kullanılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(); Sample(int x); Sample(int x, int y); void disp(); int a; int b; //... }; Sample::Sample() : Sample(10, 20) { cout << "default constructor" << endl; } Sample::Sample(int x) : Sample(x, 10) { cout << "int constructor" << endl; } Sample::Sample(int x, int y) { // ortak yapılması gereken şeyler cout << "common codes..." << endl; cout << "int, int constructor" << endl; a = x; b = y; } void Sample::disp() { cout << a << ", " << b << endl; } int main() { Sample s; Sample k(10); s.disp(); k.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 29. Ders 22/11/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesinin yaşamı sona ererken yani sınıf nesnesi bellekten yok edilmeden hemen önce derleyici tarafından otomatik olarak çağrılan üye fonksiyona "yıkıcı fonksiyon (destructor)" denilmektedir. Yıkıcı fonksiyonların isimleri ~sınıf_ismi biçimindedir. (~ ile sınıf ismi bitişik yazılmak zorundadır). Yıkıcı fonksiyonların da geri dönüş değerleri biçiminde bir kavramları yoktur. Yani bunların da geri dönüş değerleri yerine bir şey yazılmaz. Yıkıcı fonksiyonlar overload edilemezler. Yıkıcı fonksiyonlar parametresiz biçimde bulunmak zorundadır. Örneğin: class Sample { public: Sample(); // default constructor ~Sample(); // destructor //... }; Sample::Sample() { //... } Sample::~Sample() { //... } Yıkıcı fonksiyonlar yapıcı fonksiyonlar tarafından yapılan birtakım ilk işlemleri geri almak amacıyla kullanılmaktadır. Ancak yıkıcı fonksiyonlara yapıcı fonksiyonlar kadar çok gereksinim duyulmamaktadır. Çünkü bir sınıf nesnesi için o nesne yok edilirken yapılacak özel bir şeyler olmayabilir. Eğer programcı sınıf için yıkıcı fonksiyon yazmamışsa derleyici yıkıcı fonksiyonu içi boş olarak public inline biçiminde (ve duruma göre constexpr olacak biçimde) yazmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(); ~Sample(); //... }; Sample::Sample() { cout << "default constructor" << endl; } Sample::~Sample() { cout << "dstructor" << endl; } int main() { Sample s; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yerel sınıf nesneleri için yıkıcı fonksiyonlar programın akışı o yerel nesnenin tanımlandığı bloğun sonuna geldiğinde çağrılmaktadır. Örneğin: foo() { //... { Sample s; // default constructor //.... } // ----> destructor //... } Global sınıf nesneleri için yıkıcı fonksiyonlar programın akışı main fonksiyonundan çıktıktan sonra çağrılmaktadır. Örneğin: Sample g_s; // default constructor main() { //... return 0; } Burada g_s global sınıf nesnesi için yapıcı fonksiyon akış main fonksiyonuna girmeden, yıkıcı fonksiyon ise akış main fonksiyonundan çıktığında çağrılacaktır. static yerel sınıf nesneleri için yıkıcı fonksiyonlar "eğer onlar için yapıcı fonksiyon çağrılmışsa" akış main fonksiyonundan çıktıktan sonra çağrılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta şimdiye kadar gördüğümüz konuları ve gelecekte de göreceğimiz konuları kapsayacak biçimde yapıcı ve yıkıcı fonksiyonların çağrılma sıraları hakkında genel bir kural vardır: "Yapıcı fonksiyonlarla yıkıcı fonksiyonlar her zaman ters sırada çağrılırlar." Yani örneğin x sınıf nesnesi için yapıcı fonksiyon y sınıf nesnesi için yapıcı fonksiyondan daha önce çağrılmışsa x sınıf nesnesi için yıkıcı fonksiyon y sınıf nesnesi için yıkıcı fonksiyondan daha sonra çağrılacaktır. Örneğin: int main() { Sample s, k; //... return; } Burada önce s için sonra k için yapıcı fonksiyonlar çağrılır. Ancak yıkıcı fonksiyonlar ters sırada yani önce k için sonra s için çağrılacaktır. Örneğin: int main() { Sample s; //... { Sample k; //... } //... return 0; } Burada akış gereği önce s için sonra k için yapıcı fonksiyon çağrılır. O halde yıkıcı fonksiyonlar da önce k için sonra s için çağrılacaktır. Örneğin: Sample g_x, g_y; int main() { //... return 0; } Burada akış main fonksiyonuna girmeden önce g_x için sonra g_y için yapıcı fonksiyonlar çağrılacaktır. Akış main fonksiyonundan çıktığında ise önce g_y için sonra g_x için yıkıcı fonksiyonlar çağrılacaktır. Aşağıdaki programı derleyerek çalıştırınız. Bu programda hangi sınıf sınıf nesneleri için yapıcı ve yıkıcı fonksiyonların çağrıldığı anlaşılmaktadır. Programın çıktısına bakarak yukarıda ifade ettiğimiz kuralın geçerli olduğunu doğrulayınız. --------------------------------------------------------------------------------------------------------------------------------------------------------------* #include using namespace std; class Sample { public: Sample(int x); ~Sample(); int a; }; Sample::Sample(int x) : a(x) { cout << "int constructor: " << a << endl; } Sample::~Sample() { cout << "destructor: " << a << endl; } void foo() { cout << "foo begins..." << endl; static Sample s(50); cout << "foo ends..." << endl; } Sample s(10), k(20), m(30); int main() { cout << "main begins..." << endl; Sample x(100), y(200); { cout << "nested block begins..." << endl; Sample r(1000), z(2000); cout << "nested block ends..." << endl; } foo(); foo(); cout << "main ends..." << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi yıkıcı fonksiyonlar "yapıcı fonksiyonlarda yapılan birtakım tahsisatları otomatik geri bırakmak" için kullanılmaktadır. Eğer yapıcı fonksiyon içerisinde böylesi bir tahsisat yapılmamışsa sınıf için yıkıcı fonksiyon yazmaya da gerek yoktur. Örneğin aşağıdaki gibi bir sınıf için programcının yıkıcı fonksiyonu yazmasına gerek yoktur. Ancak biz sınıfın aslında yıkıcı fonksiyonunun olduğunu düşünmeliyiz. Çünkü biz bir sınıf için yıkıcı fonksiyon yazmamışsak derleyici onun için yıkıcı fonksiyonu içi boş olarak public inline biçiminde yazmaktadır. (Bazı durumlarda derleyici yıkıcı fonksiyonu yazamayabilir. C++11 ve sonrasında bu duruma "defaulted" yıkıcı fonksiyonun "deleted" yapılması denilmektedir.) Bu konu ileride ele alınacaktır. #include using namespace std; class Complex { public: Complex(double r = 0, double i = 0) { real = r; imag = i; } void disp(); double real, imag; }; void Complex::disp() { cout << real << '+' << imag << 'i' << endl; } int main() { Complex z{3, 2}; z.disp(); return 0; } C++20 ile birlikte sınıf şablonlarında alternatif yıkıcı fonksiyonlar bulunabildiği için standart dokğmanlarında bolca "destructor" yerine "prospective destructor" terimi kullanılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirtitğimiz yıkıcı fonksiyonlar "yapıcı fonksiyonlar içerisinde yapılan birtakım ilk işlemlerin otomatik yok edilmesi amacıyla" kullanılmaktadır. Örneğin yapıcı fonksiyon içerisinde new operatörü ile bir bellek tahsisatı yapılmış olabilir, bu tahsisat yıkıcı fonksiyon içerisinde delete operatörüyle free hale getirilebilir. Aşağıdaki örnekte String sınıfının str isimli veri elemanı için sınıfın yapıcı fonksiyonunda bir tahsisat yapılmıştır. Bu tahsisat sınıfın yıkıcı fonksiyonunda serbest bırakılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class String { public: String(const char *s); ~String(); void disp(); //... char *str; }; String::String(const char *s) { str = new char[strlen(s) + 1]; strcpy(str, s); } String::~String() { delete[] str; } void String::disp() { cout << str << endl; } int main() { cout << "main begins..." << endl; String s{"izmir"}; { String k{"ankara"}; k.disp(); } cout << "main ends..." << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Örneğin bir sınıfın yapıcı fonksiyonu bir dosya açmış olabilir. Bu dosyanın kapatılması yıkıcı fonksiyon tarafından otomatik yapılabilir. Aşağıdaki örnekte yapıcı fonksiyon içerisinde dosya açılmış, yıkıcı fonksiyon içerisinde dosya kapatılmıştır. Bu örnekte bir noktaya dikkat ediniz. Yapıcı fonksiyon içerisinde dosya açım işleminin başarısını kontrol kontrol ettik ancak başarısızlığın gereğini yapamadık. Bu bir kusurdur. Pekiyi başarısızlık durumunda ne yapabilrdik? Programın tümden sonlandırılması iyi bir teknik değildir. Öte yandan bir mesaj veriyor olsak bile program çalışma devam edecek ve tanımsız davranışlar oluşacaktır. İşte yapıcı fonksiyonlardaki başarısızlıklarda genel olarak "exception" fırlatılmaktadır. Biz henüz o konuyu görmedik. Örneğimiz cerr nesnesini de kullandık. cout "stdout dosyasına", cerr ise "stderr dosyasına" yazım yapmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class File { public: File(const char *path, const char *mode = nullptr); ~File(); void type(); //... FILE *f; }; File::File(const char *path, const char *mode) { f = fopen(path, mode == nullptr ? "r" : mode); // başarısızlık kontrol edilip exception fırlatılabilir if (f == nullptr) cerr << "cannot open file: " << path << endl; } File::~File() { fclose(f); } void File::type() { int ch; while ((ch = fgetc(f)) != EOF) cout << (char)ch; if (ferror(f)) cerr << "cannot read file!.." << endl; } int main() { //... { File file("xxxx.cpp"); file.type(); } //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıdaki sınıfı delege yapıcı fonksiyon çağırma sentaksı ile de aşağıdkai gibi oluşturabilirdik. Genel olarak delege yapıcı fonksiyonu çağırma burada bize önemli bir fayda sağlamamaktadır. Dolayısıyla bu tür durumlarda default argüman kullanmayı tercih edebilirsiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class File { public: File(const char *path); File(const char *path, const char *mode); ~File(); void type(); //... FILE *f; }; File::File(const char *path) : File(path, "r") {} File::File(const char *path, const char *mode) { f = fopen(path, mode ); // başarısızlık kontrol edilip exception fırlatılabilir if (f == nullptr) cerr << "cannot open file: " << path << endl; } File::~File() { fclose(f); } void File::type() { int ch; while ((ch = fgetc(f)) != EOF) cout << (char)ch; if (ferror(f)) cerr << "cannot read file!.." << endl; } int main() { //... { File file("sample.cpp"); file.type(); } //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi sınıfın yapıcı fonksiyonları programcı tarafından çağrılamamaktadır. Ancak sınıfın yıkıcı fonksiyonları programcı tarafından çağrılabilir. Yıkıcı fonksiyonların birdeb fazla kez çağrılması da genel olarak tanımsız davranış oluşturmaktadır. Örneğin: { Sample s; //... s.~Sample(); // geçerli } // ancak blok sonunda yine yıkıcı fonksiyon çağrılacaktır, tanımsız davranış Burada sınıfın yıkıcı fonksiyonu iki kez çağrılmış olmaktadır. Dolaysıyla tanımsız davranış söz konusudur. Yıkıcı fonksiyonların açıkça programcı tarafından çağrılması çok seyrek bir gereklilik oluşturmaktadır. (Örneğin placement new fonksiyonu ile birlikte bu özellik kullanılabilmektedir.) Yıkıcı fonksiyon ancak bir nesne ya da gösterici yoluyla çağrılabilir. Bir üye fonksiyon tarafından doğrudan çağrılamaz. Örneğin: void Sample::foo() { //... ~Sample(); // geçersiz! } Yıkıcı fonksiyonların yine adresleri alınamamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte sınıfların yıkıcı fonksiyonları da "defaulted" ve "deleted" yapılabilmektedir. Zaten bir sınıf için yıkıcı fonksiyon yazılmamışsa derleyici tarafından yıkıcı fonksiyonun içi boş olarak yazıldığını belirtmiştik. O halde bir yıkıcı fonksiyonun "defaulted" yapılmasının anlamı ne olabilir? Aslında toplamda yıkıcı fonksiyonun açıkça "defaulted" yapılması ancak teorik açıklamalar için kullanılmaktadır. Yıkıcı fonksiyon açıkça "deleted" yapılırsa artık biz o sınıf türünden nesneyi yok edemeyiz. Çünkü yok ederken yıkıcı fonksiyon çağrılmaktadır. Bu tür durumlarda hata link aşamasında değil derleme aşamasında ortaya çıkmaktadır. Yine açıkça "deleted" yıkıcı fonksiyonlar ancak "static sınıflar için" anlamlı olabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: //... Sample() = default; ~Sample() = delete; }; int main() { Sample s; // geçersiz! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın veri elemanları tüm üye fonksiyonlar içerisinden doğrudan kullanılabilmektedir. İşte bu durum faaliyet alanı bakımından bu veri elemanı isimlerinin yanlışlıkla gizlenmesi sonucunu doğurabilmektedir. Örneğin bir üye fonksiyonun parametre değişkeni sınıfın bir veri elemanının ismiyle aynı olursa biz bu üye fonksiyon içerisinde artık sınıfın bu veri elemanına erişemeyiz: Çünkü C'de ve C++'ta aynı blokta birden fazla aynı isimli değişken faaliyet gösteriyorsa o blokta dar faaliyet alanına sahip olan değişkenlere erişilebilmektedir. Örneğin: class Date { public: Date(int day, int month, int year) { day = day; month = nonth; year = year; } //... int day, month, year; }; Burada yapıcı fonksiyonda biz parametre değişkenlerini sınıfn veri elemanlarına atamamaktayız. Parametre değişkenlerini kendi kendilerine atamaktayız. Yine bir üye fonksiyonu inceleyen kişi oradaki değişkenin bir veri elemanı olup olmadığını çabuk anlarsa kodu daha iyi anlamlandırabilir. İşte bu nedenlerden dolayı C++ programcıları genellikle sınıfın veri elemanlarını özel öneklerle ya da soneklerle isimlendirmektedir. Örneğin Microsoft sınıfların veri elemanlarını m_xxxx biçiminde "m_" öneki ile isimlendirmektedir. Bazı programcılar da "d_" önekini tercih ederler. Bazı programcılar ismin sonuna '_' getirirler. Biz de kursumuzda bundan sonra sınıfların bütün veri elemanlarını m_xxxx biçiminde "m_" öneki ile isimlendireceğiz. Örneğin: class Date { public: Date(int day, int month, int year) { m_day = day; m_month = nonth; m_year = year; } //... int m_day, m_month, m_year; }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Date { public: int m_day, m_month, m_year; Date(int d, int m, int y); void disp(); }; Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::disp() { cout << m_day << '/' << m_month << '/' << m_year << endl; } int main() { Date date(10, 12, 1995); date.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 30. Ders 04/12/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesinin adresi alınabilir. Bu adres aynı türden bir sınıf göstericisine atanabilir. Örneğin: class Sample { public: //... int m_a; int m_b; }; //... Sample s; Sample *ps; ps = &s; Burada &s ifadesi Sample türünden bir adres bilgisi oluşturmaktadır. Yani &s ifadesi Sample * türündendir. Nasıl C'de bir yapı nesnesi yoluyla yapı elemanlarına nokta operatörüyle, yapı türünden adres yoluyla yapı elemanlarına ok operatörüyle eriiliyorsa C++'ta da erişim benzerdir. ps bir sınıf türünden gösterici ve m_a bu sınıfın bir veri elemanı olmak üzere erişim (*ps).m_a ifadesiyle de ps->m_a ifadesiyle de yapılabilir. Benzer biçimde foo bu sınıfın bir üye fonksiyonu olmak üzere ps göstericisi ile bu üye fonksiyon (*ps).foo(...) ifadesi ile de ps->foo(...) ifadesi ile de çağrılabilir. Bir sınıf türünden gösterici ile sınıfın bir üye fonksiyonunu çağırdığımızda üye fonksiyon içerisindeki veri elemanları aslında göstericinin gösterdiği yerdeki nesnenin veri elemanlarıdır. Örneğin: Sample s; ps = &s; ps->foo(); Burada foo fonksiyonu içerisindeki veri elemanları ps göstericisinin gösterdiği yerdeki nesnenin veri elemanlarıdır. ps göstericisinin gösterdiği yerde s nesnesi olduğuna göre buradaki veri elemanları aslında s nesnensinin veri elemanlarıdır. Zaten bu çağrı da aşağıdakiyle eşdeğerdir: (*ps).foo(); Burada *ps zaten s anlamına geldiğine göre yine foo fonksiyonundaki veri elemanları s'in veri elemanlarıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} void disp(); int m_a; int m_b; }; void Sample::disp() { cout << m_a << ", " << m_b << endl; } int main() { Sample s{10, 20}; Sample *ps; ps = &s; ps->disp(); (*ps).disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new operatörüyle sınıf nesnesi dinamik olarak heap'te de tahsis edilebilir. Bu durumda önce programın çalışma zamanı sırasında heap'te sınıfın veri elemanlarını tutacak büyüklükte dinamik bir alan tahsis edilir sonra o alan için sınıfın uygun yapıcı fonksiyonu çağrılır. Yani new operatörü hem dinamik tahsisatı yapmakta hem de tahsis edilmiş olan alan için sınıfın uygun yapıcı fonksiyonunu çağırmaktadır. Örneğin: class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} void disp(); int m_a; int m_b; }; Burada dinamik tahsisatı şöyle yapabiliriz: Sample *ps; ps = new Sample(10, 20); Artık ps göstericisinin gösterdiği yerdeki nesnenin m_a ve m_b veri elemanlarında sırasıyla 10 ve 20 değerleri bulunacaktır. new operatörünün türü belirli bir adres verdiğine dikkat ediniz. Bu örnekte new operatörü Sample türünden bir adres vermektedir. new operatörüyle tahsis edilmiş olan dinamik alanların delete operatörüyle boşaltıldığını anımsayınız. O halde yukarıdaki dinamik tahsisat şöyle boşaltılabilir: delete ps; delete operatörü de boşaltımı yapmadan hemen önce sınıfın yıkıcı fonksiyonunu çağırmaktadır. Nasıl new operatörü sınıfın yapıcı fonksiyonunun çağrılmasına yol açıyorsa delete operatörü de sınıfın yıkıcı fonksiyonun çağrılmasına yol açmaktadır. Bir sınıf türünden gösterici tanımladığımızda sınıf nesnesi tanımlamış olmayız. Bu nedenle bir yapıcı fonksiyonun çağrılmasını beklemeyiniz. Örneğin: Sample *ps; Burada sınıf nesnesi yaratılmamaktadır. Yalnızca bir gösterici yaratılmaktadır. Nesnenin yaratımının new işlemi sırasında yapıldığına dikkat ediniz. Aşağıdaki örnekte Sample sınıfı türünden new operatörüyle dinam ik bir nesne yaratılmış ve sonra nesne delete operatöryle yok edilmiştir. Örnekte new operatörünün heap'te dinamik yaratılan nesne için yapıcı fonksiyonu çağırdığına, delete operatörünün de yıkıcı fonksiyonu çağırdığına dikkat ediniz. Programı çalıştırdığınızda ekranda şunları göreceksiniz: one Sample constructor two Sample destructor three --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a, int b); ~Sample(); void disp(); int m_a; int m_b; }; Sample::Sample(int a, int b) : m_a(a), m_b(b) { cout << "Sample constructor" << endl; } Sample::~Sample() { cout << "Sample destructor" << endl; } void Sample::disp() { cout << m_a << ", " << m_b << endl; } int main() { Sample *ps; cout << "one" << endl; ps = new Sample(10, 20); cout << "two" << endl; delete ps; cout << "three" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new operatörüyle bir sınıf nesnesi için tahsisat yapılırken aşağıdaki dört sentaks biçimi kullanılabilir: 1) new T(...) 2) new T{...} 3) new T; 4) new T() veya new T{}; 1) Birinci biçimde tahsisat sonrasında sınıfın argüman yapısına en uygun olan yapıcı fonksiyonu çağrılır. Yani sınıfın bütün yapıcı fonksiyonları aday fonksiyonlardır. Bunların arasındna uygun olanlar ve nihayet en uygun yapıcı fonksiyon seçilir. 2) İkinci biçimde yine birinci biçimde olduğu gibi en uygun yapıcı fonksiyon seçilir. Ancak bu fonksiyonun "daraltıcı dönüştürme (narrowing converison)" oluşturmaması gerekir. Eğer seçilen yapıcı fonksiyon daraltıcı dönüştürme oluşturursa yaratım geçersiz olur. 3) Burada dinamik yaratılan nesne "default-initialize" yapılmaktadır. Yani sınıfın default yapıcı fonksiyonu varsa o çağrılacaktır, yoksa yaratım geçersizdir. 4) Burada dinamiik yaratılan nesne "value-initialize" yapılmaktadır. Yani sınııfn "user-provided" yapıcı fonksiyonu varsa o çağrılır, ancak default yapıcı fonksiyonu var fakat "user provided" değilse nesne "zero-initialize" edilecektir. Burada üç ve dördüncü maddeler arasındaki ince farka dikkat ediniz: class Sample { public: int m_a; int m_b; }; auto *ps = new Sample; Burada nesnenin m_a ve m_b elemanlarında çöp değerler bulunacaktır. Fakat örneğin: auto *ps = new Sample(); Burada artık nesnenin m_a ve m_b elemanlarında 0 bulunacaktır. Bu iki madde arasındaki farklılık genellikle C++ programcıları tarafından pek bilinmemektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: void disp(); int m_a; int m_b; }; void Sample::disp() { cout << m_a << ", " << m_b << endl; } int main() { auto ps1 = new Sample; ps1->disp(); // dikkat çöp değerler auto ps2 = new Sample(); ps2->disp(); // sıfır değerleri delete ps2; delete ps1; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir sınıf nesnesinin dinamik bir biçimde tahsis edilmesine neden gereksinim duyulmaktadır? İşte biz bir sınıf nesnesini programın belli bir notasında yaratıp belli bir noktasında yok etmek isteyebiliriz. Yerel sınıf nesneleriyle ve global sınıf nesneleriyle bunun yapılması mümkün değildir. Biz bir sınıf nesnesini yerel olarak yarattığımızda yaratım akış tanımlamanın yapıldığı yere geldiğinde yapılmaktadır. Ancak nesnenin yok edilmesi akışın o yerel nesnenin içinde bulunduğu bloğun sonuna gelmesiyle yapılır. Oysa biz new operatörü ile nesneyi herhangi bir yerde yaratıp herhangi bir yerde yok edebiliriz. Örneğin: { Sample *ps; //... { ps = new Sample(); // nesne bu noktada yaratıldı //... } //... delete ps; // nesnene bu noktada yok ediliyor //... } Aşağıdkai örneği incelyiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a, int b); ~Sample(); void disp(); int m_a; int m_b; }; Sample::Sample(int a, int b) : m_a{a}, m_b{b} { cout << "constructor" << endl; } Sample::~Sample() { cout << "destructor" << endl; } void Sample::disp() { cout << m_a << ", " << m_b << endl; } int main() { Sample *ps; cout << "one" << endl; { cout << "two" << endl; ps = new Sample(10, 20); cout << "three" << endl; } ps->disp(); cout << "four" << endl; delete ps; cout << "five" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi pek çok sistemde (ama garantisi yok) program sonlandığında heap alanı boşaltıldığı için dinamik tahsisatlar yok edilmiş gibi olmaktadır. Çünkü Windows gibi UNIX/Linux gibi macOS gibi işletim sistemlerinin bulunduğu sistemlerde heap alanı prosese özgüdür. Tabii C ve C++ standartlarında aslında heap alanının prosese özgü olup olmadığı konusunda bir şey söylenmemiştir. C++'ta bir sınıf nesnesini new operatöryle dinamik bir biçimde tahsis edip onu delete operatöryle yok etmemişsek program sonlandığında onun o dinamik nesne için yıkıcı fonksiyon çağrılmaz. Mevcut sistemlerin hemen hepsinde heap alanı prosese özgü olduğu için bu dinamik nesnenin kapladığı alan proses sonlandığında yok edilecektir ancak yıkıcı fonksiyon çağrılmayacaktır. Başka bir deyişle dinamik tahsis edilmiş olan sınıf nesneleri için eğer delete işlemi yapılmamışsa hiçbir biçimde yıkıcı fonksiyon çalıştırılmamktadır.Örneğin: { Sample *ps = new Sample(); //... } Buarada akış bloğu bitirdiğinde ps göstericisinin faaliyet alanı artık sonlanacak ve bu gösterici stack'ten yok edilecektir. Ancak onun gösterdiği yerdeki nesne heap'te kalmaya devam edecektir. Bu durum en azından bir "bellek sızıntısı (memory leak)" oluşturacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıflar türünden referanslar tanımlanabilir. Bir sınıf türünden sol taraf değeri referansı aynı sınıf türünden bir nesne ile ilkdeğer verilerek tanımlanmak zorundadır. Örneğin: Sample s; Sample &r = s; Bu işlemin eşdeğer gösterici karşılığı şöyle oluşturulabilir: Sample s; Sample *r = &s; Bir referans ile sınıfın elemanlarına erişmek için ok operatörü değil nokta operatörü kullanılmaktadır. Çünkü referansı ilkdeğer verdikten sonra kullandığımızda artık referansın refere ettiği nesneyi kullanmış olmaktayız. Örneğin: Sample s; Sample &r = s; r.foo(); // dikkat ok operatörü değil, nokta operatörü ile erişim yapılıyor Tabii bir sınıf türünden sağ taraf değeri referansı da oluşturulabilir. Ancak bu durumda ilkdeğer olarak verilen nesnenin aynı sınıf türünden bir sağ taraf değeri belirtmesi gerekir. Sınıflar türünden sağ taraf değeri oluşturma ile ilgili konuları henüz görmedik. Bu nedenle buna ilişkin açık bir örnek henüz veremiyoruz: Sample &&r = ; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Sample { public: Sample() = default; Sample(int a) { m_a = a; } void disp() { cout << m_a << endl; } int a() { return m_a; } void set_a(int a) { m_a = a; } private: int m_a; }; int main() { Sample s(10); Sample &r = s; r.disp(); // 10 r.set_a(20); s.disp(); // 20 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesi fonksiyona parametre yoluyla aktarılabilir. Tıpkı C'de bir yapı nesnesinin fonksiyona "atama yoluyla (call by value)" ve adres yoluyla (call by reference) aktarılmasında olduğu gibi C++'ta sınıf nesneleri de atama yoluyla ve adres yoluyla fonksiyonlara aktarılabilmektedir. Örneğin Sample bir sınıf belirtiyor olsun: void foo(Sample k) { //... } //... Sample s; foo(s); Burada atama yoluyla aktarım söz konusudur. Yani s'in veri elemanlarının hepsi k'ya kopyalanacaktır. Genel olarak atama yoluyla aktarım (call by value) kötü bir tekniktir. Ayrıca C++'ta bu tür durumlarda parametre değişkeni için ismine "kopya yapıcı fonksiyonu (copy constructor)" denilen bir yapıcı fonksiyon çağrılmaktadır. Kopya yapıcı fonksiyonları ileride ayrı bir başlık halinde ele alınacaktır. Tıpkı C'deki yapılarda olduğu gibi sınıf nesneleri fonksiyonlara adres yoluyla aktarılmalıdır. Anımsanacağı gibi C++'ta adres yoluyla aktarım göstericilerle ve referanslarla yapılabiliyordu. Bu iki aktarım biçimi arasında bir performans farkı bulunmuyordu. Ancak referanslar bu bağlamda daha güvenli olan göstericiler gibi işlev gördüğü için aktarımda referans kullanımı daha yaygndır. Gösterici yoluyla aktarımda fonksiyonun parametre değişkeni ilgili sınıf türünden gösterici olur. Fonksiyon da aynı sınıf türünden bir nesnenin adresiyle çağrılır. Örneğin: void foo(Sample *ps) { //... } //... Sample s; foo(&s); Fonksiyonun içerisinde ok operatörrüyle sınıfın veri elemanlarına ve üye fonksiyonlarına erişilebilir. Referans yoluyla aktarımda fonksiyonun parametre değişkeni ilgili sınıf türünden referans (sol taraf değeri referansı) olur. Fonksiyon da aynı sınıf türünden nesnenin kendisiyle çağrılır. Örneğin: void foo(Sample &r) { //... } //... Sample s; foo(s); // dikkat! nesnenin adresini programcı değil derleyici almaktadır Fonksiyonun içerisinde sınıfın veri elemanlarına ve üye fonksiyonlarına nokta operatörüyle erişilmektedir. Anımsanacağı gibi C'de fonksiyonun parametre değişkeni bir gösterici ise eğer fonksiyon adresini aldığı nesneyi değiştirmeyecekse parametrenin "gösterdiği yer const olan const bir gösterici" yapılması iyi bir tekniktir. Aynı durum C++'ta parametre değişkeni olan referanslar için de söz konusudur. Ancak biz const nesneleri ve const sınıf göstericilerini ve referanslarını ayrı bir paragrafta ele alacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a, int b); void disp(); int m_a; int m_b; }; Sample::Sample(int a, int b) : m_a{a}, m_b{b} {} void Sample::disp() { cout << m_a << ", " << m_b << endl; } void foo(Sample *ps) { ps->disp(); } void bar(Sample &r) { r.disp(); } int main() { Sample s{10, 20}; foo(&s); bar(s); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- SInıflar türünden diziler de oluşturulabilmektedir. Örneğin Sample bir sınıf belirtmek üzere bu sınıf türünden 5 elemanlı bir diziyi şöyle oluşturabiliriz: Sample s[5]; Burada s dizisinin her elemanı Sample sınıfı türünden bir nesnedir. Dolayısıyla buradaki dizinin her elemanı için sırasıyla sınıfın default yapıcı fonksiyonu çağrılacaktır. C++'ta sınıflar türünden diziler oluştururken sınıfın başka bir yapıcı fonksiyonun çağrılması mümkün değildir. Yani aşağıdaki gibi bir sentaks bunu yapamamaktadır: Sample s[5](10); // geçerli değil C++'ta her zaman yapıcı fonksiyonlarla yıkıcı fonksiyonların ters sırada çağrıldığını belirtmiştik. Bu dizi de yok edileceği zaman yıkıcı fonksiyonlar ters sırada çağrılacaktır. Aşağıdkai örnek kod ile bunu doğrulayabiliriz. Bu örneği çalıştırdığınızda ekranda şunları göreceksiniz: default constructor: 0 default constructor: 1 default constructor: 2 default constructor: 3 default constructor: 4 destructor: 4 destructor: 3 destructor: 2 destructor: 1 destructor: 0 --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(); Sample(int a) :m_a(a) {} ~Sample(); void disp(); int m_a; }; int g_a; Sample::Sample() { m_a = g_a++; cout << "default constructor: " << m_a << endl; } Sample::~Sample() { cout << "destructor: " << m_a << endl; } int main() { Sample s[5]; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 31. Ders 06/12/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Her ne kadar henüz sınıflar türünden geçici yaratmayı görmemiş olsak da bir sınıf türünden dizilerin tanımlanmasında küme parantezleri ile ilkdeğer verilerek dizi elemanları için istenilen yapıcı fonksiyonların çağrılması sağlanabilmektedir. Örneğin: Sample s[10] = {Sample(), Sample(10), Sample(), Sample(20)}; T bir sınıf belirtmek üzere C++'ta T(...) ifadesi "T türünden geçici nesne yarat" anlamına gelmektedir. Örneğimizde yaratılan bu geçici nesne dizi elemanlarına atanacaktır. C++17 ve sonrasında bu tür durumlarda artık "copy elision" zorunlu hale getirilmiştir. Bu paragrafın ne anlam ifade ettiğini ancak bu konuları ele aldıktan sonra anlayabileceksiniz. Aşağıdaki örneği inceleyiniz. Programı çalıştırdıktan sonra aşağıdaki gibi bir çıktı elde edeceksiniz: default constructor: 0 int constructor: 10 default constructor: 0 int constructor: 20 default constructor: 0 default constructor: 0 default constructor: 0 default constructor: 0 default constructor: 0 default constructor: 0 destructor: 0 destructor: 0 destructor: 0 destructor: 0 destructor: 0 destructor: 0 destructor: 20 destructor: 0 destructor: 10 destructor: 0 --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(); Sample(int a); ~Sample(); void disp(); int m_a; }; Sample::Sample() { m_a = 0; cout << "default constructor: " << m_a << endl; } Sample::Sample(int a) { m_a = a; cout << "int constructor: " << m_a << endl; } Sample::~Sample() { cout << "destructor: " << m_a << endl; } int main() { Sample s[10] = {Sample(), Sample(10), Sample(), Sample(20)}; //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new operatörüyle birden fazla sınıf nesnesi için dinamik tahsisat yapabiliriz. Örneğin: Sample *ps; ps = new Sample[10]; Burada heap'te 10 tane Sample türünden ardışıl nesne tahsis edilmiştir. Bu dinamik dizinin başlangıç adresi ps göstericisine atanmıştır. new operatörü dinamik dizinin elemanları için tek tek sırasıyla default yapıcı fonksiyonu çağırmaktadır. Bu biçimde tahsis edilmiş olan sınıf dizileri delete operatörü ile serbest bırakılabilir: delete[] ps; C++'ta her zaman yapıcı fonksiyonlarla yıkıcı fonksiyonların ters sırada çağrıldığını anımsayınız. Burada dizi elemanları için yıkıcı fonksiyonlar sondan başa doğru çağrılacaktır. new operatörü ile bir sınıf dizisi dinamik olarak tahsis edilirken yine ancak dizi elemanlaır için sınıfın default yapıcı fonksiyonları çağrılabilmektedir. Aşağıdaki gibi bir sentaks geçerli değildir: Smaple *ps; ps = new Sample[10](10); // böyle bir sentaks yok --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(); Sample(int a); ~Sample(); void disp(); int m_a; }; int g_a; Sample::Sample() { m_a = g_a++; cout << "default constructor: " << m_a << endl; } Sample::Sample(int a) { m_a = a; cout << "int constructor: " << m_a << endl; } Sample::~Sample() { cout << "destructor: " << m_a << endl; } int main() { Sample *ps; ps = new Sample[10]; //... delete[] ps; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yine dinamik olarak tahsis edilen sınıf dizilerine C++11 ile birlikte geçici nesne yoluyla farkı yapıcı fonksiyonlarla ilkdeğer verilebilmektedir. Örneğin: Sample *ps; ps = new Sample[10]{Sample(), Sample(10), Sample(20)}; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz daha önce sınıfların public, protected ve private olmak üzere üç bölümden oluştuğunu belirtmiştik. Örneklerimizde sınıf elemanlarını hep public bölüme yerleştirdik.Yine anımsanacağı gibi C++'ta class ile struct arasındaki tek fark default bölüm ile ilgiliydi. class tanımlamalarında default bölüm private, struct tanımlamalarında default bölüm public biçimindeydi. Şimdi de bu bölümlerin anlamları üzerinde duracağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıf bildirimi içerisinde bildirilen varlıklara sınıfın elemanları diyebiliriz. Sınıfın elemanları denildiğinde tipik olarak sınıfın veri elemanları ve üye fonksiyonları anlaşılmaktadır. Ancak sınıfın başka elemanları da söz konusu olabilir. Sınıfların public, protected ve private bölümleri sınıf elemanlarına nereden erişilebileceği üzerinde etkili olmaktadır. Sınıflardaki erişim kuralları şöyledir: 1) Sınıfın elemanlarına o elemanlar hangi bölümde olursa olsun sınıf bildirimi içerisinde ve sınıfın üye fonksiyonları içerisinde doğrudan erişilebilir. Yani sınıfın kendisi için bir erişim kısıtı yoktur. Örneğin biz sınıfımızın public bölümündeki bir fonksiyonu içerisinde sınıfımızın private bölümündeki bir elemana erişebiliriz. Başka bir deyişle sınıfın üye fonksiyonları sınıfın tüm bölümlerine doğrudan erişebilmektedir. 2) Sınıfın dışından yani sınıfın üye fonksiyonu olmayan bir fonksiyondan (örneğin global bir fonksiyondan ya da sınıfın başka bir üye fonksiyonundan) sınıfın yalnızca public bölümüne erişilebilir. Yani biz sınıfın dışındaki bir fonksiyondan o sınıf türünden bir nense ya da referans yoluyla nokta operatörünü kullanarak ya da o sınıf türünden bir gösterici yoluyla ok operatörünü kullanarak sınıfın yalnızca public bölümündeki elemanlara erişebiliriz. private ve protected bölümdeki elemanlara erişemeyiz. 3) Sınıfın private bölümündeki elemanlara sınıfın üye fonksiyonu olmayan bir fonksiyon içerisinde o sınıf türünden bir nesne, referans ya da gösterici yoluyla "." ya da "->" operatörünü kullanarak erişemeyiz. Yani private bölümdeki elemanlar sınıfın dışından erişime kapalıdır. Bu elemanlara yalnızca sınıf bildiriminden ya da üye fonksiyonlar içerisinden erişilebilir. 4) Sınıfın protected bölümü dışarıdan (yani sınıfın üye fonksiyonu olmayan fonksiyonlardan) erişime kapalı ancak türemiş sınıf erişimine açık bölümüdür. Sınıfın protected bölümündeki elemanlara türemiş sınıfın üye fonksiyonları doğrudan erişebilmektedir. Ancak private bölümdeki elemanlara türemiş sınıfın üye fonksiyonları tarafından da erişilememektedir. Sınıfın protected ve private bölümündeki elemanlara sınıf içerisinden doğrudan erişilebilir. Ancak dışarıdan erişilemez. Bu iki bölüm arasındaki fark türetme söz konusu olduğunda ortaya çıkmaktadır. protected bölüm türemiş sınıf tarafından erişilebilen bir bölümken privtae bölüm yalnızca sınıfın kendisi tarafından erişilebilen bir bölümdür. Türetme işlemlerinin anlatıldığı bölümde protected bölümün anlamı ayrıntılı olarak ele alınacaktır. Biz de türetme konusunu işleyene kadar protected bölümü hiç kullanmayacağız. Sınıfın en korunaklı bölümü private bölümdür. Bu bölümdeki elemanlara yalnızca sınıfın üye fonksiyonları tarafından yani sınıfın kendisi tarafından erişilebilmektedir. Sınıfın herkese açık bölümü public bölümdür. public bölümdeki elemanlara sınıfın dışından (yani sınıfın üye fonksiyonu olmayan fonksiyonlardan) o sınıf türünden nesne, referans ya da gösterici yoluyla erişilebilmektedir. Sınıfın protected bölümü dışarıdan erişilemeyen ancak türemiş sınıflar tarafından erişilebilen bölümüdür. Sınıfın bölümleri korunaklılık durumuna göre yüksekten alçağa doğru public, protected, private biçimindedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: int m_a; void foo(); protected: void bar(); private: int m_b; void tar(); }; void Sample::foo() { cout << "Sample::foo" << endl; } void Sample::bar() { cout << "Sample:bar" << endl; } void Sample::tar() { cout << "Sample::tar" << endl; } int main() { Sample s; s.foo(); // geçerli, foo public bölümde s.tar(); // geçersiz! tar private bölümde s.m_a = 10; // geçerli, m_a public bölümde s.m_b = 20; // geçersiz! m_b private bölümde s.bar(); // geçersiz! bar protected bölümde return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi sınıfın elemanlarını hangi bölüme yerleştirmeliyiz? 1) Eğer ilgili elemana herkesin erişmesini istiyorsak onu public bölüme yerleştirmeliyiz. 2) Eğer ilgili elemana sınıfın içerisinden erişilmesini istiyorsak ancak sınıfın dışından erişmesini istemiyorsak onu private bölüme yerleştirmeliyiz. 3) Eğer iligili elemana sınıf içerisinden ve o sınıftan türetilen sınıfların erişmesini istiyorsak onu protected bölüme yerleştirmeliyiz. Sınıfın bölümlerine erişimi şuna benzetebiliriz. 1) Ancak kendimizin bildiğimiz bilgiler söz konusu olabilir. Bu bizim private bölümümüzdür. 2) Bizim hakkımızda herkesin bildiği bilgiler söz konusu olabilir. Bu bilgiler bizim public bölümümüzdür. 3) Bizim bazı bilgilerimizi çocuklarımız biliyor olabilir ancak başkaları bilmiyor olabilir. Bunlar da protected bölümümüzü oluşturmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf türünden nesne yaratırken o sınıfın yapıcı ve yıkıcı fonksiyonlarının o anda erişilebilir olması gerekmektedir. Örneğin sınıfın yapıcı ve/veya yıkıcı fonksiyonu private bölümdeyse biz sınıfın dışından o sınıf türündne nesneyi yaratamayız. Örneğin: class Sample { Sample(); // yapıcı fonksiyon private bölümde //... }; void test() { Sample s; // geçersiz! yapıcı fonksiyona erişilemiyor! //... } O halde genel olarak yapıcı ve yıkıcı fonksiyonların sınıfın public bölümünde olması gerekir. Bu fonksiyonların seyrek de olsa private ve protected bölümlere yerleştirilmeleri için gerekçeler de vardır. Bu gerekçelere ileride başka konularda değinilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { Sample(); //... }; Sample::Sample() { //... } int main() { Sample s; // error! yapıcı fonksiyon private bölümde, erişilebilir değil! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- NYPT (Nesne Yönelimli Programlama Tekniği) birtakım anahtar kavramların birleşimi olarak düşünülebilir. Bu anahtar kavramlar birbirleriyle iç içe geçmiş durumdadır. NYPT'nin anahtar kavramlarından biri "kapsülleme (encapsulation)" denilen kavramdır. Kapsülleme bir olguyu bir sınıfla temsil edip, sınıfın dış dünyayı ilgilendirmeyen, iç işleyişe ilişkin kısımlarının private bölüme yerleştirilerek dış dünyadan gizlenmesi anlamına gelmektedir. Kapsülleme aslında gerçek dünyada da karşılaştığımız bir olgudur. Örneğin arabanın önemli fakat kullanıcıyı ilgilendirmeyen öğeleri kaput içerisine gizlenmiştir. Biz televizyonu yalnızca public bölümü temsil eden kumandayla kullanırız. Televizyonun iç devreleri çerçeve içerisinde gizlenmiştir. Bir bankaya gittiğimizde biz yalnızca public bölümdeki memurlarla işlerimizi yürütürüz. Bankanın temizliği ile, yönetmi ile, oradaki kişilerin birbirleriyle ilişkileri ile kafamızı yormayız. Bunlar o olgunun private bölümündeki öğelerdir. Bir sınıf için iki bakış açısı önemlidir: Sınıfı kullananların bakış açısı ve sınıfı yazanların bakış açısı. Sınıfı kullananlar yalnızca public bölüm ile ilgilenirler. Sınıfı yazanlar ise sınıfın her bölümünü bilmek durumundadırlar. Sınıfın kullanıcı için dokümantasyonu yapılırken private bölüm açıklanmaz. Yalnızca public ve protected bölümlerin dokümantasyonu yapılır. Örneğin Sample isimli bir sınıf yazacak olalım. Bu sınıfın do_something_important isimli bir üye fonksiyonu olsun. Bu fonksiyon da işin bazı kısımlarını yapan foo, bar, tar üye fonksiyonlarını çağırıyor olsun. Birisinin bu foo, bar, tar fonksiyonlarını çağırmasının bir anlamı olmadığı gibi bunları çağırması da sorunlara yol açabilir. Bu durumda bizim foo, bar, tar fonksiyonlarını sınıfın private bölümünde gizlememiz uygun olur. Örneğin: class Sample { public: void do_something_important(); private: void foo(); void bar(); void tar(); }; Birisi bu sınıfın bildirimini gördüğünde artık foo, bar ve tar fonksiyonlarıyla ilgilenmez. Çünkü zaten o kişinin bu fonksiyonları çağırm aimkanı da yoktur. O kişi çağırabileceği public bölümdeki do_something_important fonksiyonuyla ilgilenecektir. Bu durum tıpkı bizim bir buzdolabını kullanırken onun üretim detayları ile ilgilenmediğimiz yalnızca bizim kullanımımız için bize verilen kısımla ilgilendiğimiz duruma benzemektedir. Örneğin biz bir fareyi kullanırken o farenin içindeki devreleri genellikle merak etmeyiz. Bizim için fare üç tuşu olan, tekerleği olan, sürüklenen, tıklanan bir nesnedir. Bu fonksiyonlar farenin public bölümünü oluşturmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: void do_something_important(); private: void foo(); void bar(); void tar(); }; void Sample::do_something_important() { //... foo(); //.. bar(); //... tar(); //... } int main() { Sample s; s.do_something_important(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- NYPT'nin diğer önemli bir prensibi de (anahtar kavramı da) "veri elemanlarının gizlenmesi (data hiding)" denilen prensiptir. Bu prensibe göre sınııfn veri elemanları genellikle iç işleyişe ilişkindir ve private bölüme yerleştirilerek dışarıdan gizlenmelidir. Örneğin: class Date { public: Date(int day, int month int year); void disp(); private: int m_day; int m_month; int m_year; }; Sınıfın veri elemanları private bölüme yerleştirildiğinde bu elemanlar sınıfın üye fonksiyonları tarafından kullanılabilirler. Ancak artık dışarıdan bu elemanlara erişilemez. Böylece veri elemanları dış dünyaya kapatılmış olur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Date { public: Date(); Date(int day, int month, int year); void disp(); private: int m_day; int m_month; int m_year; }; Date::Date() { time_t t; struct tm *pt; time(&t); pt = localtime(&t); m_day = pt->tm_mday; m_month = pt->tm_mon + 1; m_year = pt->tm_year + 1900; } void Date::disp() { cout << m_day << '/' << m_month << '/' << m_year << endl; } Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } int main() { Date d; d.disp(); Date k{10, 12, 2005}; k.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 32. Ders 11/12/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın veri elemanlarını private bölüme yerleştirince artık bunlara dışarıdan erişemeyiz. Pekiyi bunlara erişmemiz gerekiyorsa ne yapaliriz? İşte private veri elemanlarına dışarıdan erişebilmek için sınıfın public bölümüne o elemanların içerisindeki değerleri alan "getter" üye fonksiyonlara, o elemanların içerisine değer yerleştiren "setter" üye fonksiyonlara gereksinim duyulmaktadır. Bu tür fonksiyonlara "erişimci fonksiyonlar (accessors)" de denilmektedir. Erişimci fonksiyonlar genellikle (ama her zaman değil) küçük fonksiyonlar olma eğilimindedir. Bu nedenle bu fonksiyonlar genellikle sınıf içeisinde inline olarak yazılırlar. Sınıfın her private veri elemanı için bir getter ve setter fonksiyonların yazılması gerekmemektedir. Ancak dışarıdan erişilmesi istenen veri elemanları için bu fonksiyonlar yazılmalıdır. Bazı elemanların dışarıdan yalızca değerleri elde edilmek istenir. Bu tür veri elemanları için yalnızca getter üye fonksiyonu yazılabilir. Benzer biçimde seyrek de olsa bazı veri elemanlarına yalnızca dışarıdan değer atanması istenebilir. Bu tür veri elemanları için yalnızca setter fonksiyonların yazılması uygun olur. Nihayet bir veri elemanına hem dışarıdan değer atanması hem de onun değerinin elde edilmesi isteniyorsa onun için hem getter hem de setter fonksiyon bulundurulur. Aşağıdaki örnekte Date sınıfının m_day, m_month ve m_year elemanlarını get ve set eden üye fonksiyonlar oluşturulmuştur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Date { public: Date(int day, int month, int year); void disp(); // accessors int get_day() { return m_day; } void set_day(int day) { m_day = day; } int get_month() { return m_month; } void set_month(int month) { m_month = month; } int get_year() { return m_year; } void set_year(int year) { m_year = year; } private: int m_day; int m_month; int m_year; }; Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::disp() { cout << m_day << '/' << m_month << '/' << m_year << endl; } int main() { Date date(10, 12, 2005); auto result = date.get_day(); cout << result << endl; date.set_day(11); date.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın private veri elemanlarına erişmek için kullanılan getter ve setter üye fonksiyonlar genellikle birkaç biçimde isimlendirilmektedir. Sınıfın private veri elemanı m_xxx olmak üzere (örneklerde Date sınıfının m_day veri elemanını kullanacağız): 1) get_xxx ve set_xxx biçiminde. Örneğin: int get_day() { return m_day; } void set_day(int day) { m_day = day; } 2) xxx ve set_xxx biçiminde. Örneğin: int day() { return m_day; } void set_day(int day) { m_day = day; } 3) getXxx ve setXxx biçiminde. Deve notasyonunun kullanıldığı sistemlerde bu isimlendirme biçimine rastlanmaktadır. Örneğin: int getDay() { return m_day; } void setDay(int day) { m_day = day; } 4) GetXxx ve SetXxxx. Pascal notasyonunu kullanan programcılar de genellikle bu isimlendirmeyi tercih etmektedir. Örneğin Microsoft Windows ortamında C++'ta Pascal tarzı fonksiyon isimlendirmelerini kullanmaktadır. Örneğin: int DetDay() { return m_day; } void SetDay(int day) { m_day = day; } 5) xxx ve xxx biçiminde. Getter ve setter fonksiyonların parametreleri farklı olduğu için onlara aynı isimler de verilebilmektedir. Örneğin C++'ın standart kütüphanesinde bu isimlendirme biçimi kullanılmaktadır: int day() { return m_day; } void day(int day) { m_day = day; } Örneğin yukarıda yazdığımız Date sınıfını C++ standart kütüphanesindeki gette/setter isimlendirmesine göre aşağıdaki gibi yazabiliriz: --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Date { public: Date(int day, int month, int year); void disp(); // accessors int day() { return m_day; } void day(int day) { m_day = day; } int month() { return m_month; } void month(int month) { m_month = month; } int year() { return m_year; } void year(int year) { m_year = year; } private: int m_day; int m_month; int m_year; }; Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::disp() { cout << m_day << '/' << m_month << '/' << m_year << endl; } int main() { Date date(10, 12, 2005); auto result = date.day(); cout << result << endl; date.day(11); cout << date.day() << endl; date.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi veri elemanlarının private bölümde gizlenmesinin gerekçesi nedir? İşte temelde bunun için 4 gerekte gösterilebilir. Ancak programcı için burada açıklayacağımız 4 gerekçenin hiçbiri geçerli değilse bu durumda veri elemanlarının privet bölüme yerleştirilmesine de gerek yoktur. Doğrudan veri elemanları dışarıdan erişilecek biçimde public bölüme de yerleştirilebilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Gerekçe 1: Deneyimler sınıfın veri elemanlarının tür ve isim bakımından sıkça değiştirildiğini göstermektedir. Eğer veri elemanları sınıfın public bölümüne yerleştirilseler onları programcı doğrudan kullanabileceği için onlarda yapılacak değişiklik onları kullanan kodları geçersiz hale getirecektir. Ancak veri elemanları prvate bölüme yerleştirildiğinde ve onlara public getter/setter üye fonksiyonlarla erişildiğinde onlarda değişikler yapıldığında bu getter/setter fonksiyonların içi yeniden düzenlenerek onları kullanmış olan kodların bu değişiklikten etkilenmemesi sağlanabilmektedir. Normal olarak sınıfı kullanan kodlar sınıfın kendi kodlarından çok daha fazla olma eğilimindedir. Üsteli sınıfı kullanan kodlar başka programcılar tarafından yazılmış olabilir. Sınıfın private veri elemanları değiştirildiğinde bu değişikliği yapan programcı sınıfın üye fonksiyolarının içini yeniden yazmak zorunda kalabilir. Ancak toplamda onları kullanan kodlar çok daha fazla olduğu için onları kullanan kodlarda bir değişikliğin yapılmaması önemli bir kazanç olacaktır. Tabii programcı sınıfın veri elemanları üzerinde bir değişiklik yapmayacağını öngörebilir. Bu durumda bu gerekçe o sınıf için geçerli olmayabilir. Aşağıdaki örnekte bu durum tesmsil edilmiştir. "date1.cpp" kodunda Date sınıfının tarih bilgisi private bölümde olan üç int türden üç veri elemanında tutulmuştur. "datec.pp" kodunda ise Date sınıfının tarih bilgisini tutan veri elemanı char türden bir dizi biçiminde değiştirilmiştir. Yani sınıfı tasarlayan kişi tarih bilgisini artık "dd/mm/yyyy" biçiminde char türden bir dizide yazı olarak saklamak istemiştir. Bu durumda sınıfın kendi kodları yeni duruma göre değiştirilmiş ancak onları kullanan kodlarda bir değişiklik gerekmemiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // date1.cpp #include using namespace std; class Date { public: Date(int day, int month, int year); void disp(); // accessors int day() { return m_day; } void day(int day) { m_day = day; } int month() { return m_month; } void month(int month) { m_month = month; } int year() { return m_year; } void year(int year) { m_year = year; } private: int m_day; int m_month; int m_year; }; Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::disp() { cout << m_day << '/' << m_month << '/' << m_year << endl; } int main() { Date date(10, 12, 2005); date.disp(); date.day(12); date.month(11); date.year(2010); cout << date.day() << '/' << date.month() << '/' << date.year() << endl; return 0; } // date2.cpp #include #include using namespace std; class Date { public: Date(int day, int month, int year); void disp(); // accessors int day() { return atoi(m_date); } void day(int day) { sprintf(m_date, "%02d", day); m_date[2] = '/'; } int month() { return atoi(m_date + 3); } void month(int month) { sprintf(m_date + 3, "%02d", month); m_date[5] = '/'; } int year() { return atoi(m_date + 6); } void year(int year) { sprintf(m_date + 6, "%04d", year); } private: char m_date[11]; }; Date::Date(int day, int month, int year) { sprintf(m_date, "%02d/%02d/%04d", day, month, year); } void Date::disp() { cout << m_date << endl; } int main() { Date date(10, 12, 2005); date.disp(); date.day(12); date.month(11); date.year(2010); cout << date.day() << '/' << date.month() << '/' << date.year() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Gerekçe 2: Eğer biz veri elemanlarını sınıfın private bölümüne yerleştirip onlara değer atamayı setter fonksiyonlarına yaptırırsak bu durumda onlara atanacak değerin sınamasını bu setter fonksiyonlarının içerisinde yapabiliriz. Sınamanın başarısyz olduğu durumda bir exception fırlatılabiliriz. Eğer sınıfın veri elemanlarını public bölüme yerleştirirsek onlara programcı istediği gibi sınır dışında değerler atayabilir. Bu durum derleme aşamasında denetlenemez. Dolayısıyla böceklere zemin hazırlar. Tabii bu gerekçe de ilgili sınıf için söz konusu olmayabilir. Yani sınıfın veri elemanlarına set işlemi yapılırken bir sınama (validation) gerekmeyebilir. Aşağıdaki örnekte Date sınıfının setter fonksiyonlarında sınır kontrolü (validation) uygulanmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Date { public: Date(int day, int month, int year); void disp(); // accessors int day() { return m_day; } void day(int day); int month() { return m_month; } void month(int month) { if (month < 0 || month > 12) throw invalid_argument("month out of range"); m_month = month; } int year() { return m_year; } void year(int year) { m_year = year; } private: bool isleap(int year) { return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; } private: int m_day; int m_month; int m_year; }; Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::day(int day) { if (day < 0) throw invalid_argument("day out of range"); switch (m_month) { case 1: case 3: case 5: case 7: case 8: case 10: case 12: if (day > 31) throw invalid_argument("day out of range"); break; case 2: if (isleap(m_year)) { if (day > 29) throw invalid_argument("day out of range"); } else if (day > 28) throw invalid_argument("day out of range"); break; case 4: case 6: case 9: case 11: if (day > 30) throw invalid_argument("day out of range"); break; } m_day = day; } void Date::disp() { cout << m_day << '/' << m_month << '/' << m_year << endl; } int main() { Date date(10, 2, 2000); date.disp(); date.day(29); // bu noktada exception oluşacak! date.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Gerekçe 3: Bazen sınıfın veri elemanları arasında birtakım ilişkiler söz konusu olabilir. Yani bir veri elemanının değerini değiştirdiğimizde başka veri elemanlarının değerlerini ona göre değiştirmek durumunda kalabiliriz. İşte bu tür durumlarda sınıfın veri elemanlarını public bölüme yerleştirirsek bu durumda tüm ilişkiyi programcının bilmesi ve uygulaması gerekir. Halbuki bu veri elemanlarını private bölüme yerleştirirsek bu ilişki setter fonksiyonlarında arka planda oluşturulabilir. Tabii sınıfın veri elemanları arasında herhangi bir ilişki de olmayabilir. Bu durumda bu sınıf için bu gerekçe geçerli olmayacaktır. Aşağıdaki örnekte Circle sınıfının m_radius elemanı m_area elemanı ile ilişkilidir. radius setter fonksiyonunda işleminde m_area elemanı da değiştirilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Circle { public: Circle(double x, double y, double radius) { m_x = x; m_y = y; m_radius = radius; m_area = 3.14159 * radius * radius; } double x() { return m_x; } void x(double x) { m_x = x; } double y() { return m_y; } void y(double y) { m_y = y; } double radius() { return m_radius; } void radius(double radius) { m_radius = radius; m_area = 3.14159 * radius * radius; } void disp(); void foo(); // m_area'yı kullandığını varsayalım void bar(); // m_area'yı kullandığını varsayalım void tar(); // m_area'yı kullandığını varsayalım private: double m_x; double m_y; double m_radius; double m_area; }; void Circle::disp() { cout << m_x << ", " << m_y << ", " << m_radius << ", " << m_area << endl; } int main() { Circle c(1, 2, 3); c.disp(); c.radius(4); c.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Gerekçe 4: Bazen sınıfın bir veri elemanı üzerinde işlem yaparken arka planda başka birtakım işlemlerin de yapılması gerekebilmektedir. Örneğin SerialPort isimli bir sınıfta seri portun hızı sınıfın public m_baudrate elemanında tutuluyor olsun. Baud rate alınmak istendiğinde hemen bu veri elemanından alınabilecektir. Ancak biz şimdi bu elemana değer yerleşirdiğimizde seri portu hızı değişmeyecektir. Çünkü seri portun hıznını değiştirmek için UART işlemcisinin programlanması gerekir. Ancak biz m_baudrate elemanını private bölümde tutup onu setter fonksyonu ile değer atamayı zorlarsak bu setter fonksiyonu bu işlemi kendi içerisinde yapacak ve programcının bu ayrıntırları bilmesine gerek kalmayacaktır. Tabii bir veri elemanı set ederken ya da get ederken arka planda birtakım işlemlerin yapılmasına gereksinim olmayabilir. Bu durumda bu gerekçe bu sınıf için geçerli olmayacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tabii yukarıda açıkladığımız dört gerekçenin hiçbiri bizim geçerli olmayabilir. Yani biz sınıfın veri elemanlarını değiştirmeyeceğmizden eminsek, o veri elemanları üzerinde sınır kontrolü gerekmiyorsa, o veri elemanı başka bir veri elemanı ile ilişkili değilse, o veri elemanını kullanırken başka işlemler yapmamız gerekmiyorsa o zaman pekala biz veri de elemanlarımızı public bölüme yerleştirebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 33. Ders 13/12/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ *------------------------------------------------------------------------------------------------------------------------------------------------------------- Projelerin tek bir kaynak dosya biçimind oluşturulması kötü bir tekniktir. Çünkü bu durumda en küçük bir değişiklikte tüm kaynak dosyanın yenidne derlenmesi gerekir. Bunun için küçük olmayan projelerin kaynak dosyalara bölünmesi hem bu sorunu kısmen ortadan kaldırmakta hem de projenin daha iyi ele alınmasını sağlamaktadır. Farklı kaynak dosyaların derlenip elde edilen amaç dosyaların (object files) link edilip hedef dosyanın (çalıştırılabilir dosya ya da kütüphane dosyası) elde edilmesi sürecine "build işlemi" ya da "make" işlemi denilmektedir. Projeyi oluşturan hangi kaynak dosyada değişilik yapıldığının belirlenerek ve bu sürecin etkin bir biçimde yürütülmesi için "build otomasyon araçları (build automation tools)" denilen araçlar kullanılmaktadır. Bunların en ünlüsü "GNU'nun make" isimli aracıdır. Microfost'un GNU make aracınaq benzer "nmake" isimli bir aracı da vardır. Yine Microsoft'un Visual IDE'sine entegre ettiği "msbuild" denilen bir nuild otomasyon aracı da bulunmaktadır. Qt dünyasında "qmake" isimli araç çokça tercih edilmektedir. Tipik bir build otomasyon aracında önce ne yapılmak istendiği özel bir script dosyasında özel bir dille yazılır. Sonra bu script dosyası build otomasyon aracına verilir. Bu araç da o dosyadaki yönergeleri izler. Örneğin GBU make aracında programcı ismine "make dosyası (make file)" denilen bir dosya oluşturur. Bu dosyanın içerisine yönergeleri yazar. Sonra "make" isimli programı çalıştırarak build işlemini gerçekleştirir. GNU make aracını öğrenmek biraz zaman alıcı bir işlemdir. Ancak en basit haliyle bir make dosyası "kurallardan (rules)" oluşmaktadır. Kurallar bir hedef (target), bir koşul (prerequiste) ve bu koşul sağlandığında yapılacak işlemlerdne oluşturulur. Örneğin: sample.o: sample.cpp gcc -c sample.cpp Burada "sample.o" ve "sample.cpp" iki dosyadır. Eğer "sample.cpp" dosyasının tarih ve zamanı "sample.o" dosyasının tarih ve zamanından daha ileride ise aşağıdaki satırdaki işlemler yapılacaktır. Tabii kurallar biribirine bağlı olabilir. Bu kurallardan bir graf üretip işlemleri sıraya koymak make programının görevidir. Örneğin: sample: sample.o mample.o g++ -o sample sample.o mample.o sample.o: sample.cpp g++ -c sample.cpp mample.o: mample.cpp g++ -c mample.cpp make programı dosya ismi belirtilmeden çalıştırılırsa ilgili dizindeki bazı isimli dosyalara bakmaktadır. Bunlardna biri Makefile isimli dosyadır. Örneğin: make -f mymake.mak Burada make programı "mymake.mak" dosyasını işletecektir. Fakat örneğin: make Burada make programı "Makefile" dosyasını işletecektir. Build otomasyon araçları genel olarak IDE'lere entegre edilmiş durumdadır. Böylece bir IDE'de bir kaynak dosya projeye eklendiğinde zaten bu mekanizma görsel biçimde oluşturulur. IDE'lerin çoğu görsel olarak yapılan işlemlerden otomasyon aracının kullandığı script dosyasını oluşturur. Build işlemi yine ilgili otomasyon aracı tarafından yapılır. make aracı çok klasik bir araçtır. Ancak karmalıktır. Karmaşık işlemlerde make dosyası oluşturmak için aracın iyi kullanılması gerekmektedir. make aracı karmaşık olduğu için işlemleri basitleştirmek amacıyla üst düzey build otomasyon araçları da oluşturulmuştur. Örneğin "cmake" isimli üst düzey aracın daha basit bir dili vardır. cmake aslında ürün olarak make dosyası oluşturmaktadır. Yani cmake aracının ürettiği make dosyasının ayrıca make edilmesi gerekir. Aynı durum qmake aracında da söz konusudur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta proje geliştirirken tipik olarak her sınıf iki kaynak dosya biçiminde yazılmaktadır. Sınıfın ismi X olmak üzere bu dosyalar "x.hpp" (ya da "x.h") ve "x.cpp" dosyalarıdır. Sınıfın bildirimleri, sınıfla ilgili sembolik sabitler ve inline fonksiyon tanımlamaları "x.hpp" dosyası içerisine yerleştirilir. Sınıfın üye fonksiyon tanımlamaları, global nesne tanımlamaları ise "x.cpp" dosyasına yerleştirilir. Böylece sınıfın kullanılacağı her yerde "x.hpp" dosyası include edilir. "x.cpp" dosyası derlenerek link aşamasında kullanılır ya da derlenerek kütüphane yerleştirilir. Tabii "x.hpp" dosyası aynı zamanda "x.cpp" dosyasından de include edilmelidir. Projede çok fazla sınıf varsa ve bu sınıfların bazıları küçükse onların birkaçı gruplanarak tek bir ".hpp" ve ".cpp" dosyalarına yerleştirilebilir. using namespace direktifleri başlık dosyalarında bulunmamlıdır. Çünkü o başlık dosyası include edildiğinde isim aramsı include eden programcının isteğinin dışında using namespace direktifinde belirtilen dizinde de yapılacaktır. Bir sınıfın bir kaynak dosyadan kullanılabilmesi için o sınıfın bildiriminin görülmesi yeterlidir. Sınıfın üye fonksiyonları zaten link aşamasında linker tarafından çalıştırılabilir dosyayla ilişkilendirilecektir. Örneğin Date sınıfını biz "date.hpp" ve "date.cpp" biçiminde iki dosyada oluşturmuş olalım. Biz "date.hpp" dosyasını include ederek o sınıfı herhangi bir ".cpp" dosyasında kullanabiliriz. Örneğin Date sınıfını aşağıdaki gibi iki dosya olarak organize edebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // date.hpp #ifndef DATE_HPP_ #define DATE_HPP_ class Date { public: Date(); Date(int day, int month, int year); void disp(); int day() { return m_day; } void day(int day) { m_day = day; } int month() { return m_month; } void month(int month) { m_month = month; } int year() { return m_year; } void year(int year) { m_year = year; } private: int m_day; int m_month; int m_year; }; #endif // date.cpp #include #include "date.hpp" using namespace std; Date::Date() { m_day = 1; m_month = 1; m_year = 1900; } Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::disp() { cout << m_day << '/' << m_month << '/' << m_year << endl; } // app.cpp #include #include "date.hpp" using namespace std; int main() { Date date(3, 12, 2004); date.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Başkaları tarafından yazılmış bir sınıfı kullanmak istediğimizde hangi dosyalara sahip olmamız gerekir? Sınıfı yazan kişi kaynak kodu gizlemek istediği için bize .cpp dosyasını vermeyebilir. Bu durumda bize derlenmiş amaç dosyayı (yani o.obj ya da .o dosyasını) ya da amaç dosyanın yerleştirildiği kütüphane dosyasını verecektir? Tabii kişinin bize sınf bildiriminin bulunduğu .hpp (ya da .h) dosyasını da vermesi gerekir. Biz de bu dosyayı include ederiz. Link aşamasında ilgili amaç dosyanın ya da kütüphane dosyasının işleme sokulmasını sağlarız. Gerçekten de genel olarak başkaları tarafından yazılmış sınıf kütüphaneleri için bize link aşamasına dahil edilecek kütüphane dosyaları ve sınıfların bildirimlerinin bulunduğu başlık dosyaları verilmektedir. Tabii eğer kütüphane açık kaynak kodluysa kütüphanenin tüm kaynak kodları da istenirse kişilere verilebilmektedir. Bir sınıfı kullanmak için sınıfın private bölümündeki bildirimlere gerek var mıdır? Ne de olsa programcı bu bölüme erişememektedir. Sınıf bildiriminde sınıfın bütün veri elemanlarının derleyici tarafından görülmesi gerekir. Aksi takdirde derleyici kod üretemektedir. Ancak private üye fonksiyonlar kod derlendikten sonra sınıf bildiriminden silinebilirler. Tabii bunun için sınıf içi inline fonksiyonların bu private üye fonksiyonları kullanmaması gerekir. C'de ve C++'ta bir başlık dosyasının içerisinde başka bir başlık dosyasındaki sembolik sabitler, typedef isimleri gibi bildirimler kullanılacaksa bunların bulunduğu başlık dosyası bunları kullanan başlık dosyasının içerisinde include dilmelidir. Çünkü başlık dosyaları kendi kendine yeter durumda olmalıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 34. Ders 18/12/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıf bildirimi içerisinde typedef bildirimi yapılabilir. Bu durumda typedef ismi sınıf faaliyet alanında olur. Yani bu isme üye fonksiyonlar içerisinden doğrudan erişilebilir. Ancak sışarıdan "eğer bildirim public bölümde yapılmışsa" sınıf ismi ve çözünürlük operatörü ile erişilebilir. Örneğin: class Sample { public: void foo(); typedef int I; //... }; void Sample::foo() { I i; // geçerli //... } int main() { Sample::I a; // geçerli //... return 0; } Tabii typedef bildirimini C++11 ile birlikte dile eklenen using bildirimi ile de benzer biçimde sınıf içerisinde yapabiliriz: class Sample { public: void foo(); using I = int; //... }; void Sample::foo() { I i; // geçerli //... } int main() { Sample::I a; // geçerli //... return 0; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Falliyet alanlı olmayan enum türleri de (unscoped enumeration) sınıf bildirimi içerisinde bildirilebilir. Bu durumda tıpkı typedef isimleri gibi bu enum isimleri ve enum sabitleri sınıf faaliyet alanı içerisinde olur. Yani üye fonksiyonlar içerisinde bunlar doğrudan kullanılabilirler. Ancak dışarıdan "eğer sınıfın public nölümündelerse" sınıf ismi ile niteliklendirilerek kullanılabilirler. Örneğin: class Sample { public: void foo(); enum Color { Red, Green, Blue }; //... }; void Sample::foo() { Color c = Green; // geçerli //.. } int main() { Sample::Color c; c = Sample::Green; // geçerli //... return 0; } Tabii sınıf bildirimi içerisinde faaliyet alanlı enum türleri de (scoped enumeration) bildirilebilir. Anımsanacağı gibi bu enum türlerinin enum sabitleri enum ismiyle niteliklendirilerek kullanılmak zorundaydı. Sınıfın kendisi zaten bir faaliyet alanı belirttiği için sınıf içerisinde faaliyet alanlı enum kullanımı çoğu kez programcılar tarafından eğer bir çakışma durumu da yoksa tercih edilmemektedir. Örneğin: class Sample { public: void foo(); enum class Color { Red, Green, Blue }; //... }; void Sample::foo() { Color c = Color::Green; // geçerli //... } int main() { Sample::Color c; c = Sample::Color::Green; // geçerli //... return 0; } Genellikle sınıf bildirimleri içerisinde faaliyet alanlı olmayan enum bildirimleriyle karşılaşırız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta "isim araması (name lookup)" konusu ileride ayrı bir paragrafta ele alınacaktır. Ancak bu aşamada birkaç nokta üzerinde durmak istiyoruz. Bir üye fonksiyon içerisindeki isimler üye fonksiyonun yerel bloklarında arandıktan sonra sınıf bildiriminin her yerinde aranır. Dolayısıyla örneğin bir üye fonksiyonu inline biçimde sınıf bildiriminin içerisinde tanımladığımızda daha sonra tanımlanmış olan sınıf elemanlarını bu üye fonksiyonda kullanabiliriz. Örneğin: class Sample { public: void foo() { m_a = 10; // geçerli bar(); // geçerli //... } void bar() { //... } private: int m_a; }; Ancak sınıf bildirimi içerisinde kullanılan isimlerin sınıf içerisinde aranması kullanım yerinden yukarıdaki bölgede yapılmaktadır. Örneğin: class Sample { public: void foo() { I a; // geçerli //... } private: I m_a; // geçersiz! typedef int I; }; Burada I ismi sınıf bildirimi içerisinde kullanılmıştır.Ancak derleyici bu I ismini sınıf içerisinde kullanım yerinden yukarıdaki alanda aramaktadır. Dolayısıyla bu kullanım geçersizdir. Ancak sınıf bildirimi aşağıdaki gibi olsaydı kullanım geçerli olurdu: class Sample { public: void foo() { I a; // geçerli //... } private: typedef int I; I m_a; // geçerli }; Bir üye fonksiyonun parametresi ve geri dönüş değeri sınıf içerisindeki bir typedef ismi türünden olabilir. Parametre parantezinin içerisi sanki üye fonksiyonun içiymiş gibi ele alınmaktadır. Ancak geri dönüş değeri böyle değildir. Geri dönüş değerindeki isimler sınıf faaliyet alanında aranmamaktadır. Tanımlama nereye yerleştirilmişse orada aranmaktadır.Örneğin: class Sample { public: using I = int; I foo(I a) // geçerli { //... } I bar(I a); // geçerli }; Buraki bar fonksiyonunun dışarıdaki tanımlamasına dikkat ediniz: I Sample::bar(I a) // geçersiz! { //... } Buradaki parametre parantezi içerisinde bulunan I ismi sanki üye fonksiyonun içerisindeymiş gibi sınıf faaliyet alanında aranacak ve bulunacaktır. Ancak geri dönüş değerindeki I ismi sınıf faaliyet alanında aranmamaktadır. Üye fonksiyonun tanımlandığı yerde aranmaktadır. Bu nedenle yukarıdaki tanımlama geçersizdir. Bu tanımlama aşağıdaki gibi yapısaydı geçerli olurdu: Sample::I Sample::bar(I a) // geçerli { //... } Özetle parametre parantezinin içi sanki sınıfın üye fonksiyonun içiymiş gibi kabul edilirken geri dönüş değerinin yazıldığı yer üye fonksiyonun içiymiş gibi kabul edilmemektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın üye fonksiyonları içerisinde aynı sınıfın her bölümündeki elemanlara doğrudan erişebildiğimizi belirtmiştik. Bir fonksiyon içerisinde aynı sınıf türünden bir nesne, gösterici ya da referans ile de bunların belirttiği nesnelerin ber bölümüne erişebiliriz. Örneğin: class Sample { public: void foo(); //... private: int m_a; }; void Sample::foo(Sample &s) { Sample k; m_a = s.m_a; // geçerli, sınıfın her bölümündeki elemanlara erişebiliriz k.m_a = m_a; // geçerli, sınıfın her bölümündeki elemanlara erişebiliriz //... } Yani bir sınıfın üye fonksiyonunda o sınıf türünden her nesne ile o nesnenin her bölümüne erişebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pek çok nesne yönelimli programlama dilinde onların standart kütüphanelerinde yazısal işlemleri yapmak için bir "string" sınıfı bulunmaktadır. C++'ın standart kütüphanesinde de böyle bir sınıf vardır. Biz önce böyle bir sınıfı basit bir düzeyde yazmaya çalışıp sonra standart kütüphanede olan string sınıfını tanıtacağız. String sınıfları mecburen yazıyı dinamik bir alanda tutarlar. Yazının sonunda özel bir karakter varsa (tipik olarak null karakter) yazının uzunluğunu tutmaya gerek olmayabilir. Ancak yazı uzunluğu değişik işlemlerde gerekebildiği için yazının uzunluğunun da ayrıca bir veri elemanında tutulması uygun olmaktadır. Bu durumda basit string sınıfının veri elemanları aşağıdaki gibi olabilir: class String { public: //... private: char *m_str; // yazının başlangıç adresini tutuyor size_t m_size; // yazının uzunluğunu tutuyor }; Bir string'e yazı eklenmesi ya da insert edilmesi çok karşılaşılan durumdur. Bu işlemlerde dinamik alanın büyütülmesi zaman kaybına yol aöabileceği için pek çok string sınıfı aslında gerekenden daha büyük bir alanı tahsis edip yeniden tahsisat işlemini (reallocation) azaltmaya çalışmaktadır. Bu tür durumlarda asıl tahsis edilen alanın uzunupuna genellikle "kapasitge (capacity)" denilmektedir. Böyle bir tasarım yapılacaksa sınıfın veri elemanları aşağıdaki gibi olacaktır: class String { public: //... private: char *m_str; // yazının başlangıç adresini tutuyor size_t m_size; // yazının uzunluğunu tutuyor size_t m_capacity; // tahsis edilmiş olan alanın uzunluğu }; Biz gerçekleştirimde önce kapasite elemanını kullanmayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 35. Ders 20/12/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıda yazılar üzerinde işlemler yapan String isimli bir sınıfın yazımı verilmiştir. Buradaki String sınıfı C++'ın standart kütüphanesindeki string sınıfına benzemektedir. Ancak C++'ın standart kütüphanesindeki string sınıfı daha ayrınrılı işlemler yapabilen üye fonksiyonlara sahiptir. İzleyen paragraflarda C++'ın standart string sınıfı ana hatlarıyla ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // string.hpp #ifndef STRING_HPP_ #define STRING_HPP_ #include namespace CSD { const int DEF_CAPACITY = 8; class String { public: using size_type = std::size_t; enum : size_type { npos = static_cast(- 1) }; // constructors String(); String(const char *str); String(size_type, char ch); String(const char *str, size_type n); ~String(); // getters size_type size() { return m_size; } const char *c_str() { return m_str; } size_type capacity() { return m_capacity; } // utilities void reserve(size_type capacity); void append(char ch); void append(const char *str); void append(const char *str, size_type n); inline void append(String &r); bool insert(size_type index, size_type count, char ch); bool insert(size_type index, const char *str); bool insert(size_type index, const char *str, size_type count); inline bool insert(size_type index, String &r); bool erase(size_type index = 0, size_type count = npos); inline void clear(); void resize(size_type count); void resize(size_type count, char ch); void shrink_to_fit(); bool replace(size_type pos, size_type count, const char *str); bool replace(size_type pos, size_type count, String &s); size_type find(char ch, size_type pos = 0); size_type find(const char *str, size_type pos = 0); char &at(size_type pos) { return m_str[pos]; } char &front() { return m_str[0]; } char &back() { return m_str[m_size - 1]; } void disp(); private: char *m_str; size_type m_size; size_type m_capacity; }; inline void String::append(String &r) { append(r.m_str); } inline bool String::insert(size_type index, String &r) { return insert(index, r.m_str, r.m_size); } inline void String::clear() { m_str[0] = '\0'; m_size = 0; } } #endif // string.cpp #include #include #include "string.hpp" using namespace std; namespace CSD { String::String() { m_str = new char[DEF_CAPACITY]; m_str[0] = '\0'; m_size = 0; m_capacity = DEF_CAPACITY; } String::String(const char *str) { m_size = strlen(str); m_str = new char[m_size + DEF_CAPACITY]; strcpy(m_str, str); m_capacity = m_size + DEF_CAPACITY; } String::String(size_type n, char ch) { m_str = new char[n + DEF_CAPACITY]; m_str[n] = '\0'; memset(m_str, ch, n); m_size = n; m_capacity = n + DEF_CAPACITY; } String::String(const char *str, size_type n) { m_str = new char[n + DEF_CAPACITY]; strncpy(m_str, str, n); m_str[n] = '\0'; m_size = n; m_capacity = n + DEF_CAPACITY; } String::~String() { delete[] m_str; } void String::reserve(size_type capacity) { if (capacity <= m_capacity) return; char *newstr = new char[capacity]; strcpy(newstr, m_str); delete[] m_str; m_str = newstr; m_capacity = capacity; } void String::append(char ch) { size_type new_size = m_size + 1; if (new_size + 1 > m_capacity) reserve(new_size * 2); m_str[m_size++] = ch; m_str[m_size] = '\0'; } void String::append(const char *str) { size_type new_size = m_size + strlen(str); if (new_size + 1 > m_capacity) reserve(new_size * 2); strcat(m_str, str); m_size = new_size; } void String::append(const char *str, size_type n) { size_type new_size = m_size + n; if (new_size + 1 > m_capacity) reserve(new_size * 2); strncat(m_str, str, n); m_size = new_size; } bool String::insert(size_type index, size_type count, char ch) { if (index > m_size) return false; size_type new_size = m_size + count; if (new_size + 1 > m_capacity) reserve(new_size * 2); memmove(m_str + index + count, m_str + index, m_size - index); memset(m_str + index, ch, count); m_size = m_size + count; m_str[m_size] = '\0'; return true; } bool String::insert(size_type index, const char *str) { if (index > m_size) return false; size_type len_str = strlen(str); size_type new_size = m_size + len_str; if (new_size + 1 > m_capacity) reserve(new_size * 2); memmove(m_str + index + len_str, m_str + index, m_size - index); memcpy(m_str + index, str, len_str); m_size = m_size + len_str; m_str[m_size] = '\0'; return true; } bool String::insert(size_type index, const char *str, size_type count) { if (index > m_size) return false; size_type new_size = m_size + count; if (new_size + 1 > m_capacity) reserve(new_size * 2); memmove(m_str + index + count, m_str + index, m_size - index); memcpy(m_str + index, str, count); m_size = m_size + count; m_str[m_size] = '\0'; return true; } bool String::erase(size_type index, size_type count) { if (index > m_size) return false; if (count == npos) count = m_size - index; memmove(m_str + index, m_str + index + count, count); m_size = m_size - count; m_str[m_size] = '\0'; return true; } void String::resize(size_type count) { if (count > m_capacity) reserve(count + DEF_CAPACITY); if (count > m_size) memset(m_str + m_size, 0, count - m_size); m_size = count; m_str[m_size] = '\0'; } void String::resize(size_type count, char ch) { if (count > m_capacity) reserve(count + DEF_CAPACITY); if (count > m_size) memset(m_str + m_size, ch, count - m_size); m_size = count; m_str[m_size] = '\0'; } void String::shrink_to_fit() { char *newstr = new char[m_size + 1]; strcpy(newstr, m_str); delete[] m_str; m_str = newstr; m_capacity = m_size + 1; } bool String::replace(size_type pos, size_type count, const char *str) { if (!erase(pos, count)) return false; return insert(pos, str); } bool String::replace(size_type pos, size_type count, String &s) { if (!erase(pos, count)) return false; return insert(pos, s); } String::size_type String::find(char ch, size_type pos) { for (size_type i = pos; i < m_size; ++i) if (m_str[i] == ch) return i; return npos; } String::size_type String::find(const char *str, size_type pos) { char *result; if ((result = strstr(m_str + pos, str)) == nullptr) return npos; return static_cast(result - m_str); } void String::disp() { cout << m_str << ", size = " << m_size << ", capacity = " << m_capacity << endl; } } // app.cpp #include #include "string.hpp" using namespace std; using namespace CSD; int main() { String s{"ankara"}; s.disp(); String::size_type result; result = s.find("kar"); if (result == String::npos) cout << "cannot find!.." << endl; else cout << "found at index: " << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 36. Ders 25/12/2023 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında yukarıda da belirttiğimiz gibi C++'ın standart kütüphanesinde zaten yazsısal işlemlerin yapılabilmesi için tasarlanmış string isimli bir sınıf bulunmaktadır. string sınıfının bildirimi başlık dosyası içerisindedir. C++'ın standart kütüphanesindeki öğelerin std isim alanı içerisinde bulunduğunu anımsayınız. Aslında yazısal işlem yapan sınıfın asıl ismi basic_string biçimindedir. basic_string sınıfı "sınıf şablonu olarak" oluşturulmuştur. Aslında bu sınıfın char açılımı string sınıfını belirtmektedir: typedef basic_string string; sınıf şablonları kursumuzda iler bölümlerde ele alınmaktadır. C++11 ile birlikte ve C++11'den sonra string sınıfı üzerinde bazı değişiklikler ve eklemeler de yapılmıştır. Orijinal string sınıfı da yeniden tahsisat miktarını azaltmak için bir kapasite ile çalışmaktadır. Yani sınıf aslında tutacağı yazıdan daha büyük bir alanı tahsis etmekte ve böylece ekleme ve insert gibi işlemlerde yeniden tahsisat yapılma olasılığını azaltmaktadır. Tasarım büyük ölçüde yukarıda yazmış olduğumuz String sınıfına benzemektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının const char * parametreli yapıcı fonksiyonu bizden bir yazı alıp onu dinamik bir biçimde tahsis etmiş olduğu char türden dizi içerisinde tutar. cout nesnesi zaten standart string nesnelerini de yazdırabilmektedir. Örneği: string s{"ankara"}; cout << s << endl; // ankara --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{ "ankara"}; cout << s << endl; // ankara return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının diğer çok kullanılan bir yapıcı fonksiyonu bizden bir sayı ve bir karakter alır. Nesneyi o karakterden o sayıda olacak biçimde oluşturur. Örneğin: string s(10, 'a'); cout << s << endl; // aaaaaaaaaa --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s(10, 'a'); cout << s << endl; // aaaaaaaaaa return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Belli bir string nesnesinin belli bir index'inden başlanarak onun belli sayıda karakterlerinden string nesnesi yapan bir yapıcı fonksiyon da vardır. Örneğin: string s("ankara"); string k(s, 2, 3); cout << k << endl; // kar --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"); string k(s, 2, 3); cout << s << endl; // ankara cout << k << endl; // kar return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tıpkı bizim yazdığımız string sınıfında olduğu gibi standart string sınıfında da sınıfın public bölümünde bildirilmiş olan size_type isimli bir tür ve bu türden npos isimli bir sembolik sabit de (aslında sınıfın const static bir veri elemanı) bulunmaktadır. size_type türü kullanlan "allocator" nesnesine bağlı olarak değişebilmekle birlikte default durumda size_t türündendir. Kursumuzun son bölümlerinde allocator kavramı üzerinde duracağız. npos değeri bazı fonksiyonlarda default argüman geçildiğini belirlemek için ya da başarısızlığı belirlemek için kullanılmaktadır. Bu size_typeve npos isimleri sınıf içerisinde bildirildiği dışarıdan string::size_type ve string::npos biçiminde kullanılmalıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir string nesnesinin tuttuğu yazının uzunluğu size ya da length üye fonksiyonlarıyla elde edilebilmektedir. Bu üye fonksiyonlar string::size_type türünden değer vermektedir. size üye fonksiyonu ile length üye fonksiyonu arasında da hiçbir farklılık yoktur. string sınıfı yendien tahsisat işlemini azaltmak için kapasite kullanılarak gerçekleştirilmektedir. Nesnenin kapasite değeri (yani yazı için gerçekten tahsis edilen alanın uzunluğu) capacity üye fonksiyonuyla elde edilebilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}; string::size_type len; len = s.size(); cout << len << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İki string nesnesi '+' operatörü ile toplama işlemine sokulabilir. Bu durumda soldaki string'in cuna sağdaki string eklenmekte ve bu içerikte yeni bir string nesnesi oluşturulmaktadır. Yani biz iki string nesnesini '+' operatörü ile topladığımızda aslında onların uçuca eklenmesindne oluşan yeni bir string nesnesi elde etmiş oluruz. Örneğin: string s{"ankara"}, k{"izmir"}, result; result = s + k; cout << result << endl; // ankaraizmir İki sınıf nesnesinin ya da bir sınıf nesnesi ile temel türlere ilişkin bir değerin işlemlere sokulabilmesi için söz konusu sınıfta ismine "operatör fonksiyonları" denilen fonksiyonların bulunuyor olması gerekmektedir. Standart string sınıfında bu işlemi yapabilecek bir '+' operatör fonksiyonu vardır. Operatör fonksiyonlarına "operator overloading" de denilmektedir. C++'ın dışında diğer bazı nesne yönelimli dillerde de operatör fonksiyonu oluşturabilme özelliği vardır. Ancak bazı nesne yönelimli dillerde bu özellik bulunmamaktadır. Yukarıdaki örnekte bir toplama sonucunda elde edilen yeni string nesnesi başka bir string nesnesine atanmıştır. Anımsanacağı gibi C'de aynı türden iki yapı nesnesi birbirine atandağında yapının karşılıklı elemanları birbirine atanmaktadır. Fakat C++'ta aynı türdne iki sınıf nesnesi birbirine atandığında aslında ismine "kopya atama operatör fonksiyonu" ya da "taşıma atama operatör fonksiyonu" denilen bir fonksiyon devreye girmektedir. Bu konu ileride aytı bir paragrafta ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"); string k("izmir"); string result; result = s + k; cout << result << endl; // ankaraizmir return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının + operatör fonksiyonu char türden bir gösterici ile toplama işlemi de yapabilmektedir. Bu durumda operatör fonksiyonu C tarzı bir string ile C++ string nesnesini toplamış gibi olmaktadır. Tabii yine bu toplama işleminden iki yazının uçuca eklenmesinden oluşan yeni bir string nesnesi elde edilecektir. Örneğin: string s("ankara"); char k[] = "izmir"; string result; result = s + k; cout << result << endl; // ankaraizmir result = k + s; cout << result << endl; // izmirankara result = s + "bursa"; cout << result << endl; // ankarabursa --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"); char k[] = "izmir"; string result; result = s + k; cout << result << endl; // ankaraizmir result = k + s; cout << result << endl; // izmirankara result = s + "bursa"; cout << result << endl; // ankarabursa return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir string nesnesi ile bir char değer de toplanabilir. Bu durumda yine stirng'teki yazı ile söz konusu char değerin uçuca eklenmesinden oluşan yeni bir string nesnesi yaratılmaktadır. Örneğin: string s("ankara"); string result; result = s + 'x'; cout << result << endl; // ankarax result = 'x' + s;; cout << result << endl; // xankara --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"); string result; result = s + 'x'; cout << result << endl; result = 'x' + s;; cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının karşılaştırma operatör fonksiyonları da vardır. Biz bir string nesnesi ile başka bir string nesnesini ya da C tarzı bir yazıyı (yani char * türünü) karşılaştırma işlemine sokabiliriz. Buradaki karşılaştırma strcmp fonksiyonunda olduğu gibi leksikografik bir biçimde yapılmaktadır. (Yani eşit olduğu sürece ilerlenir, ilk eşit olmayan karakterin durumuna bakılır.) Örneğin s ve k birers tring nesnesi olmak üzere aşağıdaki gibi işlemler yapılabilmektedir: if (s > k) { //... } else if (s < k) { //... } else if (s == k) { // bilerek yerleştirilmiştir //... } C++20 ye kadar aşğıdaki 6 karşılaştırma operatörü de string sınıfında bulunuyordu: <, >, <=, >=, == != Ancak C++20 ile birlikte == dışındaki operatör fonksiyonları string sınıfından kaldırılmış bunun yerine <=> operatörü eklenmiştir. <=> operatörüne "üç yönlü karşılaştırma operatörü (three way comparison operator)" ya da "uzay gemisi operatür (space ship operator)" denilmektedir. Dolayısıyla C++20 ve sonrasında string sınıfında <, >, <=, >=, != operatörlerine ilişkin operatör fonksiyonları bulunmamaktadır. Ancak derleyiciler bu operatörleri geçmişe doğru uyumu korumak için halen barındırmaktadır. Pekiyi C++20 ile eklenen <=> operatörü nasıl bir değer üretmektedir? Bu operatör aslında strong_ordering, partial_ordering, weak_ordering gibi bazı sınıflar türünden değerler üretmektedir. Biz bu sınıfların hepsini xxx_ordering sınıfı biçiminde ifade edebiliriz. Bu xxx_ordering sınıfları "> 0", "< 0" ve "== 0" karşılaştırmalarını yapabilecek operatmr fonksionlarına sahiptir. Dolayısıyla biz bu <=> operatöründen elde edilen değeri bu biçimde karşılaştırma işlemine sokabiliriz. Örneğin: auto result = a <=> b; Eğer result > 0 işlemi true değerini verirse buradan a > b sonucu çıkartılmalıdır. Eğer result < 0 işlemi true değerini verirse buradan a < b sonucu çıkartılmalıdır. Eğer result == 0 işlemi true verirse buradan da a == b sonucu çıkartılmalıdır. Benzer biçimde result != 0 karşılaştırması da yapılabilmektedir. Ancak !result işlemi yapılamamaktadır. Örneğin: string s("ankara"); string k("ankastre"); auto result = s <=> k; if (result > 0) cout << "s > k" << endl; else if (result < 0) cout << "s < k" << endl; else if (result == 0) cout << "s == k" << endl; Standartlara göre <=> operatörünün her iki operandı da tamsayı türlerine ilişkinse karşılaştırma sonucunda strong_ordering sınıfı türünden bir değer elde edilmektedir. Eğer operand'lardan biri gerçek sayı türlerine ilişkin ise bu durumda <=> operatör partial_ordering sınıfı türünden bir değer üretmektedir. string sınıfında genel olarak elde edilen ürün weak_ordering sınıfı türündendir. Ancak bu konunun bazı ayrıntıları vardır. Konu ileride başka bir paragrafta bağımsız olarak yeniden ele alınacaktır. Üç yönlü karşılaştırma operatörü C++ standartlarına göre diğer karşılaştırma operatörlerinden daha yüksek öncelik durumdadır. Dolayıısyla aşağıdaki gibi bir işlem geçerlidir: if (s <=> k > 0) { //... } Ancak Microsoft derleyicilerinde bu konuda bir sorun vardır. Microsoft derleyicileri bu operatörü daha düşük öncelikliymiş gibi ele almaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 37. Ders 27/12/2023 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bazı programlama dillerinde o dillerin standart kütüphanelerinde bulunan string sınıfları "değiştirilemez (immutable)" sınıflardır. Örneğin Java, C#, Python gibi billerde bir string nesnesi oluşturulduktan sonra onun karakterlerini herhangi bir biçimde dğiştiremeyiz. Halbuki C++'ın standart string sınıfı "değiştirilebilir (mutable)" bir sınıftır. Yani biz yaratılmış olan bir string nesnesindeki yazı üzerinde değişiklikler yapabiliriz. /*------------------------------------------------------------------------------------------------------------------------------------------------------------- /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının append isimli üye fonksiyonları mevcut yazının sonuna yeni bir yazı ya da karakter eklemektedir. Yani üye fonksiyonları ile şu eklemeleri yapabiliriz: 1) string'e C tarzı string ekleyebiliriz. Bunun için sınıfın const char * parametreli append fonksiyonu vardır. 2) string'e başka bir string nesnesini ekleyebiliriz. 3) string'e belli bir sayıda bir karakterden ekleyebiliriz. 4) string'e C tarzı string'in ilk n karakterini ekleyebiliriz. 5) string'e başka bir string'in belli bir indeksinden itibaren n tane karakterini ekleyebiliriz. C++11 ile birlikte initializer_list içeren bir append fonksiyonu da sınıfa eklenmiştir. Ayrıca iteratör yoluyla ekleme yapan append üye fonksiyonları da vardır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"), k("eskisehir"); s.append("izmir"); // 1 s.append(k); // 2 s.append(10, 'x'); // 3 s.append("erzurum", 3); // 4 s.append(k, 3, 2); // 5 cout << s << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının += operatör fonksiyonu da bulunmaktadır. Bu operatör fonksiyonu sayesinde biz append üye fonksiyonu ile yaptığımız bazı işlemleri operatör sentaksıyla da yapabiliriz. += operatöründe sol taraf operand string olduğunda sağ taraftaki operand şunlardan biri olabilir: 1) Başka bir string nesnesi 2) Tek bir karakter 3) C tarzı bir string (yani const char *) C++11 ile birlikte initializer_list içeren bir += operatör fonksiyonu da sınıfa eklenmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}, k{"izmir"}; s += k; cout << s << endl; // ankaraizmir (1) s += 'x'; cout << s << endl; // ankaraizmirx (2) s += "istanbul"; cout << s << endl; // ankaraizmirxistanbul (2) return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının erase isimli üye fonksiyonları string'ten karakter silmek için kullanılmaktadır. iki parametreli erase fonksiyonu belli bir indeksten itibaren n tane karakteri silmektedir. Bu iki parametre de girilmezse yazının tamamı silinmektedir. Birinci parametre girilip ikinci parametre girilmezse o indeksten itibaren yazının geri kalanı silinmektedir. Sınıfın dieğr erase üye fonksiyonları iterator konusyla ilgilidir. Dolyaısıyla biz onları bu konu görülene kadar açıklamayacağız. eğer index değeri nesne içerisindeki yazıdan büyükse exception oluşmaktadır. Ancak silinecek karakter miktarının kalan karakter sayısından büyük olması durumunda herhangi bir exception oluşmamakta geri kalan karakterlerin hepsi silinmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"); s.erase(2, 3); cout << s << endl; // ana return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir string nesnesinin herhangi bir karakterine [] operatörüyle erişebiliriz. Yine [] operatörü ile string'in herhangi bir karakterini de değiştirebiliriz. Örneğin: string s{"ankara"}; char ch; ch = s[4]; cout << ch << endl; // r s[4] = 'x'; cout << s << endl; // ankaxa Köşeli parantez içerisindeki değer yazı uzunluğuna eşit olabilir. Bu durumda '\0' karakter elde edilir. Ancak köşeli parantez içerisindeki değer yazı uzunluğuna eşit olduğu durumda bir atama yapılmak istendiğinde atanan değer '\0' değilse tanımsız davranış oluşmaktadır. Eğer bu ibdeks değeri yazının uzunluğundan büyükse yine tanımsız davranış oluşmaktadır. Operatör fonksiyonu tarafından herhangi sınır kontrolü yapılmamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}; char ch; ch = s[4]; cout << ch << endl; // r s[4] = 'x'; cout << s << endl; // ankaxa return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- at üye fonksiyonu da belli bir indeksteki karaktere erişmek için kullanılmaktadır. Ancak bu fonksiyonun [] operatör fonksiyonundan farkı sınır kontrolü uygulamasıdır. Eğer indeks değeri yazının uzunluğundan büyük ya da yazının uzunluğuna eşitse bu fonksiyon exception (std::out_of_range) oluşturmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}; char ch; ch = s.at(4); cout << ch << endl; // r s.at(4) = 'x'; cout << s << endl; // ankaxa ch = s.at(100); // exception oluşacak! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir string aralık tabanlı for döngüleriyle de karakter karakter dolaşılabilmektedir. Her yinelemede yazının sıradaki karakteri elde edilmektedir. Örneğin: string s{"ankara"}; for (char c : s) cout << c << " "; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}; for (auto c : s) cout << c << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tabii aralık tabanlı for döngüsünde referans da kullanabiliriz. Bu durumda bu referans string içerisindeki karakterleri gösterir. Yani onun güncellenmesi string'in karakterlerinin güncellenmesi anlamına gelecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { string s("ankara"); for (auto &ch : s) ch = toupper(ch); cout << s << endl; // ANKARA return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının substr isimli üye fonksiyonu belli bir indeksten itibaren string'in n tane karakterini bir string olarak elde etmek için kullanılmaktadır. İkinci parametre girilmezse string'in sonuna kadarki tüm karakterler elde edilir. İki parametre de girilmezse yazının aynısı elde edilmektedir. Bu da kopyalama anlamına gelir. Örneğin: string s("ankara"); string result; result = s.substr(2, 2); cout << result << endl; // ka result = s.substr(2); cout << result << endl; // kara result = s.substr(); cout << result << endl; // ankara substr fonksiyonunda indeks belirten değer yazının uzunluğundan büyükse exception (std::out_of_range) oluşmaktadır. Eğer indeks belirten değer yazının uzunluğuna eşitse exception oluşmaz boş string elde edilir. Elde edilecek karakter sayısı büyükse exception oluşmaz geri kalan karakterlerin hepsi elde edilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"); string result; result = s.substr(2, 2); cout << result << endl; // ka result = s.substr(2); cout << result << endl; // kara result = s.substr(); cout << result << endl; // ankara return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınfın replace isimli üye fonksiyonları string içeriisndeki yazının belli bir kısmını başka bir yazıyla yer değiştirmektedir. Yani bu belli kısım önce silinip sonra o yere insert işlemi yapılıyor gibi bir etki oluşturmaktadır. Sınıfın önemli replace fonksiyonları şunları yapmaktadır: 1) String nesnesinin belli bir kısmını başka bir string nesnesindeki yazı ile yer değiştiren replace fonksiyonu 2) String nesnesinin belli bir kısmını başka bir string nesnesindeki yazının belli bir kısmı ile yer değiştiren replace fonksiyonu 3) String nesnesinin belli bir kısmını C tarzı bir string'in ilk n karakteri ile yer değiştiren replace fonksiyonu 4) String nesnesinin belli bir kısmını bir karakterden n tane ile yer değiştiren replace fonksiyonu Fonksiyonlarda indeks belirten değerde sınır kontrolü uygulanmaktadır. Eğer indeks belirten değerler string'in belirttiği yazının uzunluğundan büyük olursa exception (out_of_range) oluşmaktadır. Aynı durum değiştirilecek yazı için de söz konusudur. Ancak yazılardaki karakter miktarını belirteen değerlerde sınır kontrolü uygulanmamaktadır. Bu değer büyük olursa bu durum "geri kana hepsi" anlamına gelmektedir. Ancak indeks belirten değer yazı uzunluğuna eşitse bu durumda exception oluşmaz. Bu durum "ekleme yapma" anlamına gelir. Örneğin: string s{"istanbul"}, k{"ankara"}; s.replace(2, 3, k); cout << s << endl; // isankarabul Sınıfın overload edilmiş diğer replace fonkiyonlarını şimdilik burada ele almayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"istanbul"}, k{"ankara"}, m{"izmir"}, r{"edirne"}; s.replace(2, 3, k); // 1 cout << s << endl; // isankarabul k.replace(2, 3, m, 2, 2); // 2 cout << k << endl; // anmia m.replace(0, 2, "kastamonu", 3); // 3 cout << m << endl; // kasmir r.replace(1, 3, 5, 'x'); // 4 cout << r << endl; // exxxxxne return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının overload edilmiş insert fonksiyonları vardır. Bu insert fonksiyonları string'in belirttiği yazıya insert işlemi uygulamaktadır. Bu insert fonksiyonlarının bazıları şunlardır: 1) string'in belli bir indeksine belli bir karakterden n tane insert eden fonksiyon 2) string'in belli bir indeksine C tarzı bir string'i insert eden fonksiyon 3) string'in belli bir indeksine C tarzı bir string'in ilk n karakterini insert eden fonksiyon 4) string'in belli bir indeksine başka bir string'i insert eden fonksiyon 5) string'in belli bir indeksine başka bir string'in bir kısmını insert eden fonksiyon Bu fonksiyonlarda indeks değeri eğer string nesnesinin belirttiği yazıdan büyük olursa exception (std::length_error) oluşmakta ancak eklenecek karakter sayısı büyük olursa "geri kalan hepsi" etkisi oluşmaktadır. Eğer indeks belirten değer yazının uzunluğu kadarsa bu durumda fonksiyonlar ekleme yapmaktadır. Sınıfın diğer insert fonksiyonlarını şimdilik burada ele almayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"istanbul"}, k{"ankara"}, m{"izmir"}, r{"edirne"}, t{"van"}, n{"afyon"}, v{"rize"}; s.insert(4, 5, 'x'); // 1 cout << s << endl; // istaxxxxxnbul m.insert(2, "adana"); // 2 cout << m << endl; // isadanamir r.insert(2, "izmit", 3); // 3 cout << r << endl; // edizmirne t.insert(1, n); // 4 cout << t << endl; // vafyonan n.insert(1, v, 1, 2); // 5 cout << n << endl; // aizfyon return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 38. Ders 03/01/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının find isimli üye fonksiyonları yazı içerisinde bir karakteri ya da başka bir yazıyı bulmak için kullanılmaktadır. Eğer söz konusu karakter ya da yazı string sınıfının belirttiği yazı içerisinde bulunursa fonksiyonlar bulunduğu yerin indeks numarasıyla, bulunamazsa string::npos değeriyle geri dönmektedir. find fonksiyonlarının geri dönüş değerleri string sınıfı içerisinde typedef edilmiş olan size_type türündendir. Sınıfın en çok kullanılan find üye fonksiyonları şunlardır: - Yazının belli bir indeskinden başlayarak belli bir string nesnesinin belirttiği yazıyı arayan fonksiyon - Yazının belli bir indeskinden başlayarak belli bir C tarzı string'i arayan fonksiyon - Yazının belli bir indeskinden başlayarak belli bir C tarzı string'in ilk n karakterini arayan fonksiyon - Yazının belli bir indeskinden başlayarak belli bir karakteri arayan fonksiyon. Sınıfın diğer find fonksiyonlarını şimdilik burada ele almayacağız. find fonksiyonlarında indeks belirten değer yazının uzunluğuna büyük ya da onunla eşit olursa fonksiyon doğrudan başarısız olur ve string::npos değeri ile geri döner. find fonksiyonu bir exception oluşturmamaktadır. Örneğin: #include #include using namespace std; int main() { string s{ "ankara" }; string::size_type result; if ((result = s.find("aralik", 2, 3)) == string::npos) cout << "cannot find" << endl; else cout << "found: " << result << endl; return 0; } Burada arama "ankara" yazısının 2'inci indeksin başlatılmaktadır ve "aralik" yazısının ilk üç karakterinden oluşan yazı aranmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"); string::size_type pos; pos = s.find('k'); if (pos == string::npos) cout << "cannot find..." << endl; cout << pos; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının rfind isimli üye fonksiyonları ilgili karakterin ya da yazının son bulunduğu yerin indeks numarasını vermektedir. Başka bir deyişle bu fonksiyonlar aramayı sondan başa doğru yaparlar. Önemli rfind fonksiyonları şunlardır: - Yazının belli bir indeskinden başlayarak belli bir string nesnesinin belirttiği yazıyı sondan itibaren arayan fonksiyon - Yazının belli bir indeskinden başlayarak belli bir C tarzı string'i belirttiği yazıyı sondan itibaren arayan fonksiyon - Yazının belli bir indeskinden başlayarak belli bir C tarzı string'in ilk n karakterini belirttiği yazıyı sondan itibaren arayan fonksiyon - Yazının belli bir indeskinden başlayarak belli bir karakteri belirttiği yazıyı sondan itibaren arayan fonksiyon Fonksiyonlardaki indeks parametresi her zaman aramanın yapılacağı yazının başından itibaren bir indeks belirtmektedir. Arama [0, index] aralığında yapılmaktadır.Örneğin: #include #include using namespace std; int main() { string s{"anastas"}; string::size_type result; if ((result = s.rfind("as", 3)) == string::npos) cout << "cannot find" << endl; else cout << "found: " << result << endl; // 2 return 0; } Burada "ankara" yazısının 3'üncü indeksinden geriye doğru arama yapılmaktadır. Sınıfın diğer rfind fonksiyonlarını şimdilik burada ele almayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"anastas"}; string::size_type result; if ((result = s.rfind("as", 3)) == string::npos) cout << "cannot find" << endl; else cout << "found: " << result << endl; // 2 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın find_first_of üye fonksiyonları da yazı içerisinde karakter aramaktadır. Ancak aranan karakterler belli bir gruptan herhangi birisi olabilmektedir. Sınıfın önemli find_first_of fonksiyonları şunlardır: - Yazının belli bir indeksinden itibaren bir string nesnesinin içerisindeki karakterlerin herhangi birini arayan fonksiyon - Yazının belli bir indeksinden itibaren C tarzı bir string'in karakterinden herhangi birini arayan fonksiyon - Yazının belli bir indeksinden itibaren C tarzı bir string'in ilk n karakterinden herhangi birini arayan fonksiyon - Yazının belli bir indeksinden itibaren tek bir karakteri arayan fonksiyon (bunun find fonksiyonundan bir farkı yoktur) Örneğin: #include #include using namespace std; int main() { string s{ "ankara" }; string::size_type result; if ((result = s.find_first_of("ka")) == string::npos) cout << "cannot find" << endl; else cout << "found: " << result << endl; // 0 return 0; } Burada "ankara" yazısı içerisinde 'a' ya da 'k' karakteri aranmıştır. Yazının 0'ıncı indeksinde 'a' karakteri bulunduğu için fonksiyon 0 iler geri dönecektir. Sınıfın diğer find_first_of fonksiyonlarını şimdilik burada ele almayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}, k{"ka"}; string::size_type result; if ((result = s.find_first_of(k)) == string::npos) cout << "cannot find" << endl; else cout << "found: " << result << endl; // 0 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının find_first_not_of üye fonksiyonları string'in belirttiği yazının içerisinde belli bir karakter grubunda olmayan ilk karakteri bulmak için kullanılmaktadır. Önemli find_first_not_of fonksiyonları şunlardır: - Yazının belli bir indeksinden itibaren bir string nesnesinin içerisindeki karakterlerin herhangi birinden olmayan ilk karakteri arayan fonksiyon - Yazının belli bir indeksinden itibaren C tarzı bir string'in karakterlerinden herhangi birinden olmayan ilk karakteri arayan fonksiyon - Yazının belli bir indeksinden itibaren C tarzı bir string'in ilk n karakterinden herhangi birinden olmayan ilk karakteri arayan fonksiyon - Yazının belli bir indeksinden itibaren tek bir karakteri arayan fonksiyon (bunun rfind fonksiyonundan bir farkı yoktur) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}, k{"ka"}; string::size_type result; if ((result = s.find_first_not_of("akn")) == string::npos) cout << "cannot find" << endl; else cout << "found: " << result << endl; // 1 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının find_last_of ve find_last_not_of fonksiyonları find_first_of ve find_first_not of fonksiyonları gibidir. Ancak bu fonksiyonlar aramayı sondan başa doğru yapmaktadır. Yukarıda da belirttiğimiz gibi buradaki indeks parametresi aramanın sondan yapılacağı yerin başını göstermektedir. Yani arama o indeksten itibaren başa doğru yapılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının starts_with ve ends_with üye fonksiyonları string nesnesinin belirttiği yazının başının ve sonunun belli bir karakterler ya da yazı ile başladığını ya da bittiğini belirlemek için kullanılmaktadır. Bu üye fonksiyonların geri dönüş değerleri bool türdendir. Aşağıdaki parametrik yapılara ilişkin starts_with ve ends_with üye fonksiyonları vardır: - Belli bir karakter ile başlama ya da bitmenin tespit edilmesini sağlayan fonksiyon - Belli bir C tarzı string ile başlama ya da bitmenin tespit edilmesini sağlayan fonksiyon - Belli bir string nesnesinin belirtitği yazı ile başlama ya da bitmenin tespit edilmesini sağlayan fonksiyon By üye fonksiyonlar string sınıfına C++20 ile eklenmiştir. Örneğin: #include #include using namespace std; int main() { string s{"ankara"}; cout << (s.ends_with("ara") ? "ok" : "not ok") << endl; return 0; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++23 ile birlikte string nesnesinin içerisinde bir karakterin ya dayazının olup olmadığını belirlemek için contains isimli üye fonksiyonlar da sınıfa eklenmiştir. Şu parametrik yapılara sahip contains üye fonksiyonları vardır: - Yazının içerisinde tek bir karakterin olup olmadığını belirlemek için kullanılan fonksiyon - Yazının içerisinde bir string nesnesinin belirttiği yazının olup olmadığını belirlemek için kullanılan fonksiyon - Yazının içerisinde C tarzı bir stirng'in olup olmadığını belirlemek için kullanılan fonksiyon contains fonksiyonları da bool değere geri dönmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfın tıpkı strcmp fonksiyonunda olduğu gibi karşılaştırma yapan compare fonksiyonları da vardır. Bu fonksiyonların geri dönüş değerleri int türdendir. Asıl yazı parametre belirtilen yazıdan büyükse bu fonksiyonlar pozitif herhangi bir değere, küçükse negatif herhangi bir değere ve eşitse sıfır değerine geri dönmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string ınıfının reserve fonksiyonu kapasiteyi büyütmek için kullanılmaktadır. C++20'ye kadar yeni kapasite değeri size değerine kadar indirilebiliyordu. Ancak C++20 ile birlikte artık kapasite düşümü yapılmamaktadır. Yani reserve fonksiyonuna geçilen argüman eğer nesnenin mevcut kapasitesine eşit ya da ondan küçükse fonksiyon hiçbir şey yapmamaktadır. Standartlara göre reserve fonksiyonu parametresiyle belirtilen miktardan daha büyük bir kapasite de oluşturabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}; cout << s << ", size: " << s.size() << ", capacity: " << s.capacity() << endl; s.reserve(100); cout << s << ", size: " << s.size() << ", capacity: " << s.capacity() << endl; s.reserve(100); cout << s << ", size: " << s.size() << ", capacity: " << s.capacity() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının shrink_to_fit metodu kapasiteyi size için yeterli düzeye çekmek amacıyla kullanılmaktadır. Fonksiyon yeni capacity değerinin size değerine eşit olacağını garanti etmemektedir. Ancak bu fonksiyonun fazla kapasiteden kurtulmak için kullanılabileceğini belirtmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"ankara"}; cout << s << ", size: " << s.size() << ", capacity: " << s.capacity() << endl; s.reserve(100); cout << s << ", size: " << s.size() << ", capacity: " << s.capacity() << endl; s.shrink_to_fit(); cout << s << ", size: " << s.size() << ", capacity: " << s.capacity() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- stdin dosyasından bir satırlık yazıyı okuyup bir string nesnesinin içerisine yerleştirmek için başlık dosyasında prototipi bulunan getline isimli global fonksiyon aşağıdaki gibi kullanılmaktadır: string s; getline(cin, s); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s; cout << "Bir yazi giriniz:"; getline(cin, s); cout << s << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte C++'a "user defined literals" diye isimlendirilen bir özellik eklenmiştir. string sınıfı da bu özelliği kullanmaktadır. Bu konu operatör fonksiyonlarının anlatıldığı bölümde zaten ele alınacaktır. Biz burada yalnızca basit bir açıklama ile yetineceğiz. İki tırnak ifadesinin sonuna onunla yapışık 's' karakteri getirilirse bu durum "bu yazıdan oluşan bir string nesnesi yarat" anlamına gelmektedir. Örneğin "ankara"s biçiminde bir yazı aslında bir string nesnesi belietmektedir. Örneğin: string result; result = "ankara"s + "izmir"; Burada aslında içerisinde "ankara" yazısı bulunan bir string nesnesi ile "izmir" yazısı toplanarakve yeni bir string nesnesi elde edilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string result; result = "ankara"s + "izmir"; cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte string kütüphanesine stoxxx biçiminde global fonksiyonlar da eklenmiştir. Bu fonksiyonların listesi şöyledir: stoi stol stoll stoul stoull stof stod stold Bu fonksiyonlar string nesnesi içeisindeki sayısal yazıyı ilgili temel nümerik türlere türüne dönüştürürler. (Yani bunlar atoi, atol, atof gibi standart C fonksiyonlarının yaptığı işleri yapmaktadır.) Fonksiyonların geri dönüş değerleri buradaki xxx türündendir. Bu fonksiyonlar yine yazının başındaki ve sonundaki boşluk karakterlerini dikkate almamaktadır. İlk tıpkı atoi, atof gibi fonksiyonlarda olduğu gibi sayısal olmayan karakterde işlemini sonlandırmaktadır. Ancak yazının başında hiçbir sayılsal karakter yoksa bu fonksiyonlar exception (invalid_argument) oluşturmaktadır. Örneğin: string s{"123ankara"}; int result; result = stoi(s); cout << result << endl; // 123 Aslında bu fonksiyonların default argüman almış size_t * ve int türden parametreleri de vardır. Bu parametreler strtol, strtoul standrat C fonksiyonlarındaki parametrelerle aynı anlamda kullanılmaktadır. Yani dönüştürme bittiğinde biten yerin indeksi ve dönüştürmenin hangi tabana göre yapılacağını belirtmektedir. Yukarıdaki fonksiyonların overload edilemeyeceğine dikkat ediniz. Çünkü bu fonksiyonların parametreleri aynı geri dönüş değerleri farklıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{"123ankara"}; int result; size_t pos; result = stoi(s, &pos); cout << result << ", " << pos << endl; // 123, 3 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yine C++11 ile birlikte string kütüphanesine bir grup overload edilmiş global to_string fonksiyonları da eklenmiştir. Bu fonksiyonlar stoxxx fonksiyonlarının tersini yapmaktadır. Yani parametreleriyle belirtilen int, long, double gibi değerleri alıp o sayıları string nesnesi biçiminde bize verirler. to_string fonksiyonlarının farklı parametrik yapılarla overload edildiğine dikkat ediniz. Yani tek bir to_string fonksiyonu yoktur. Farklı parametrik yapılara ilişkin aynı isimli farklı to_string fonksiyonları vardır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int val = 1234; string s; s = to_string(val); cout << s << endl; s = to_string(12.34); cout << s << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte bir Windows'ta bir yol ifadesinin sonundaki dosya ismi elde edilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string path, fname; cout << "Bir yol ifadesi giriniz:"; getline(cin, path); auto pos = path.find_last_of("\\/"); if (pos != string::npos) fname = path.substr(pos + 1); else fname = path; cout << fname << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının atama operatör fonksiyonları da vardır. Böylece biz bir string nesnesine "=" operatörü ile C tarzı bir string'i ya da başka bir string nesnesini atayabiliriz. Bu tür atamalarda atanan string nesnesi başka bir yazıyı tutuyorsa onun boşaltımı yapılmaktadır. Yani herhangi bir bellek sızıntısı oluşmamaktadır. Böylesi atamalarda "içerik kopyalaması" yapılmaktadır. Yani kaynak yazı yeni bir alan tahsis edilerek hedefe kopyalanmaktadır. Örneğin: string s("ankara"); string k("izmir"); k = s; cout << s << endl; // ankara cout << k << endl; // ankara s.append("kayseri"); cout << "---------" << endl; cout << s << endl; // ankarakayse cout << k << endl; // ankara cout << "---------" << endl; k = "istanbul"; cout << k << endl; // istanbul --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s("ankara"); string k("izmir"); k = s; cout << s << endl; cout << k << endl; s.append("kayseri"); cout << "---------" << endl; cout << s << endl; cout << k << endl; cout << "---------" << endl; k = "istanbul"; cout << k << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- string sınıfının c_str isimli üye fonksiyonu string nesnesinden C tarzı bir string elde etmek için kullanılmaktadır. c_str fonksiyonun parametresi yoktur. Geri dönüş değeri const char * türündendir. Böylece biz elimizde bir string nesnesi varsa o nesnenin tuttuğu yazıyı const char * türünden sonu null karakter ile biten bir yazı biçiminde elde edebiliriz. Örneğinİ string s{"ankara"}; puts(s.c_str()); Sınıfın c_str üye fonksiyonun verdiği adres nesne yaşadığı sürece geçerli bir biçimde kalmaktadır. Ancak standartlara göre sınııfn const olmayan bir üye fonksiyonu çağrıldığında bu adresin gösterdiği yerdeki yazı da değişebilir. Örneğin aşağıdaki fonksiyon bir gösterici hatasına yol açacaktır: const char *foo() { string s{"ankara"}; return s.c_str(); } Burada foo fonksiyonu bittiğinde string nesnesi boşaltılacağı için geri dönüş değeri olarak iletilen adres de geçirsiz hale gelmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 39. Ders 08/01/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın static olmayan ye fonksiyonları "const üye fonksiyonlar" yapılabilir. Bunun için fonksiyonun parametre parantezinden sonra "const" anahtar sözcüğü kullanılmaktadır. Buradaki const anahtar sözcüğü hem prototipte hem de tanımlama sırasında bulundurulmak zorundadır. Örneğin: class Sample { public: void foo() const; //... }; void Sample::foo() const { //... } Global fonksiyonlar ya da static üye fonksiyonlar const yapılamamaktadır. Ayrıca sınıfın (constructor) yapıcı ve yıkıcı (destructor) üye fonksiyonları da const üye fonksiyon yapılamamaktadır. Sınıfın const üye fonksiyonları sınıfın static olmayan veri elemanlarını kullanabilirler ancak onları değiştiremezler. Yani bir üye fonksiyonu const yapan programcı derleyiciye "o üye fonksiyon içerisinde sınıfın (static olmayan) bir veri elemanını değiştirmeyeceği sözünü" vermektedir. Tabii eğer programcı bu sözünde durmazsa bu durumda program geçersiz (ill formed) olur ve derleme zamanında error oluşur. Örneğin: #include using namespace std; class Sample { public: Sample(int a, int b); void disp() const; //... private: int m_a; int m_b; }; Sample::Sample(int a, int b) { m_a = a; m_b = b; } void Sample::disp() const { cout << m_a << ", " << m_b << endl; // geçerli m_a = 100; // geçersiz! const üye fonksiyon sınıfın const olmayan veri elemanlarını değiştiremez } int main() { Sample s{10, 20}; s.disp(); return 0; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a, int b); void disp() const; //... private: int m_a; int m_b; }; Sample::Sample(int a, int b) { m_a = a; m_b = b; } void Sample::disp() const { cout << m_a << ", " << m_b << endl; } int main() { Sample s{10, 20}; s.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi const bir üye fonksiyon içerisinde biz sınıfın veri elemanlarının değerlerini değiştiren başka bir üye fonksiyonu çağırmaya çalışsak (yani başka bir üye fonksiyon yoluyla sözümüzde durmamaya çalışsak) ne olur? İşte const üye fonksiyonlar const olmayan (ve static olmayan) üye fonksiyonları çağıramamaktadır. Sınıfın const üye fonksiyonları yalnızca sınıfın const üye fonksiyonlarını çağırabilmektedir. Örneğin: class Sample { public: Sample(int a, int b); void disp() const; void foo(); //... private: int m_a; int m_b; }; Sample::Sample(int a, int b) { m_a = a; m_b = b; } void Sample::disp() const { cout << m_a << ", " << m_b << endl; foo(); // geçersiz! const bir üye fonksiyon sınıfın const olmayan (ve static olmayan) üye fonksiyonunu çağıramaz. } void foo() { //... } Tabii derleyici const olmayan üye fonksiyonun sınıfın veri elemanlarını değiştirip değiştmediğine bakmamaktadır. Yani yukarıdaki örnekte foo fonksiyonu sınıfın veri elemanlarını değiştirmiyor olsa bile vonst üye fonksiyonlar tarafından çağıralamaz. Tabii sınıfın const olmayan bir üye fonksiyonunun const bir üye fonksiyonu çağırmasında herhangi bir sakınca yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesi const yapılabilir. const sınıf nesneleri için çağrılan yapıcı ve yıkıcı fonksiyonlar o nesnenin veri elemanlarında değişiklik yapabilirler. Ancak bu istisna durum dışında const nesnenin veri elemanları herhangi bir biçimde (yani doğrudan ya da dolaylı olark) değiştirilemezler. const bir sınıf nesnesi ile sınıfın yalnızca const üye fonksiyonları çağrılabilmektedir. Çünkü const üye fonksiyonların sınıfın veri elemanlarını değiştirmeyeceği zaten derleyici tarafından denetlenmektedir. Yapıcı fonksiyonların bir "ilkdeğer verme" işlemi de yaptıklarına dikkat ediniz. Biz const nesnelere ilkdeğer verebilmekteyiz. Ancak ilkdeğer verdikten sonra artık onları değiştiremeyiz. Sınıfın yıkıcı fonksiyonları birtakım kaynakları serbest bırakırken sınıfın veri elemanlarını da değiştirmek zorunda kalabilmektedir. Bu nedenle yıkıcı fonksiyonların da nesne const olsa bile sınıfın veri elemanlarını değiştirebilmesine olanak sağlanmıştır. Örneğin: #include using namespace std; class Sample { public: Sample(int a, int b); void disp() const; void foo(); //... private: int m_a; int m_b; }; Sample::Sample(int a, int b) { m_a = a; m_b = b; } void Sample::disp() const { cout << m_a << ", " << m_b << endl; } void Sample::foo() { disp(); } int main() { const Sample s{10, 20}; s.disp(); // geçerli, const nesne ile const üye fonksiyonlar çağrılabilir s.foo(); // geçersiz! const üye fonksiyonlarla const olmayan üye fonksiyonlar çağrılamaz! return 0; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfn veri elemanları üzerinde değişiklik yapmayan üye fonksiyonların const üye fonksiyon yapılması iyi bir tekniktir. Ya da tersten söylersek sınıfın veri elemanlarını değiştirmeyen üye fonksiyonların const üye fonksiyon yapılamamsı kötü bir tekniktir. Bunun tipik olarak üç nedeni vardır: 1) Eğer üye fonksiyon sınıfın veri elemanlarını değiştirmediği halde onu const yapmazsak onun const nesnelerle çağrılabilirliğini engellemiş oluruz. Örneğin: using namespace std; class Sample { public: Sample(int a, int b); void disp(); //... private: int m_a; int m_b; }; Burada disp üye fonksiyonu const yapılabileceği halde yapılmamıştır. Bu durumda biz onu aslında const bir nesneyle çağırabileceğimiz halde çağıramaz duruma geliriz: const Sample s{int a, int b}; s.disp(); // geçersiz! 2) const üye fonksiyonlar okunabilirliği artırmaktadır. Yani onların çağrıldığını gören kişiler onun nesnenin durumunu değiştirmediğini yalnızca veri elemanlarını kullandığını anlarlar ve kodu daha iyi anlamlandırılar 3) const üye fonksiyonlar optimizasyon konusunda da fayda dsağlayabilmektedir. Örneğin foo fonksiyonun const bir üye fonksiyon olduğunu kabul edelim: s.foo(); Derleyici bu üye fonksiyonu CALL etmeden önce o nesnenin veri elemanlarının bazılarını CPU yazmaçlarında tutmuş olabilir. Fonksiyon çağrıldıktan sonra nesnenin veri elemanları değişmeyeceğine göre yazmaçtaki değer yeniden yüklemeden kullanbilir. Üye fonksiyonlarda const durumu tutarlı bir biçimde kullanıldığında artık "const olmayan üye fonksiyonların sınıfın veri elemanlarını değiştirdiği" sonucu da çıkartılabilir. (Çünkü eğer böyle bir const olmayan fonksiyon sınııfn veri elemanlarını değiştiriyor olmasaydı zaten const yapılırdı. Demek ki değiştirmemektedir. Bundan sonra kursumuzda artık sınıfın veri elemanlarını değiştirmeyen tüm üye fonksiyonlarını const üye fonksiyon yapacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın private veri elemanlarının değerlerini elde etmek için kullanılan "getter" fonksiyonlar tipik const üye fonksiyon olmaya aday fonksiyonlardır. Örneğin: class Date { public: //... int day() const { return m_day;} int month() const { return m_month;} int year() const { return m_year;} //... private: int m_day, m_month, m_year; }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Üye fonksiyonun const olması onun imzasını değiştirmektedir. Yani sınıfta aynı isimli ve aynı parametrik yapıya sahip biri const olan diğeri const olmayan üye fonksiyonlar birlikte bulnabilir. Örneğin: class Sample { public: //... void foo(); // geçerli void foo() const; // geçerli //... }; Böylesi bir durumda eğer üye fonksiyon const bir nesne ile çağrılırsa const üye fonksiyonun, const olmayan bir nesne ile çağrılırsa const olmayan üye fonksiyonun çağrılmış olduğu kabul edilmektedir. const bir nesneyle const olmayan üye fonksiyon zaten çağrılamamaktadır. Overload resulotion kurallarına göre const olmayan nesne için const olmayan üye fonksiyon daha iyi dçnüştürme sağlamaktadır. Çrneğin: const Sample s; Sample k; s.foo(); // const olan foo çağrılıyor k.foo(); // const olmayan foo çağrılır --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: //... void foo(); void foo() const; //... }; void Sample::foo() { cout << "non-const foo" << endl; } void Sample::foo() const { cout << "const foo" << endl; } int main() { Sample s; const Sample k; s.foo(); k.foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- const üye fonksiyonlar konusunda programcıların kafalarının karıştığı bir durum vardır. Sınıfın veri elemanı bir dizi ise const üye fonksiyon içerisinde bu dizinin bütün elemanları const gibidir. Böylece dizinin ismi de "gösterdiği tyer const olan" const bir adres gibidir. Örneğin: class Sample { public: //... void foo() const; private: char m_name[32]; }; void Sample::foo() const { m_name[0] = 'x'; // geçersiz! strcpy(m_name, "kaan"); // geçersiz! m_name gösterdiği yer const olan bir adres gibi } Buradaki const üye fonksiyon olan foo içerisinde biz m_name dizisinin hiçbir elemanını değiştemeyiz. Bu işlemi dolaylı olarak strcpy fonksiyonuyla da yapamayız. Çünkü strcpy fonksiyonun birinci paramtesi gösterdiği yer const olmayan bir göstericidir. Halbuki const üye fonksiyon içerisinde m_name ifadesi gösterdiği yer const olan const bir adres gibidir. Pekiyi sınıfın veri elemanı bir gösterici olsaydı ne olacaktı? Örneğin: class Sample { public: Sample() : m_name(new char[64]) {} void foo() const; private: char *m_name; }; const üye fonksiyon olan foo içerisinde m_name göstericisinin kendisi const gibidir,onun gösterdiği yer const değildir. Dolayısıyla foo içerisinde m_name göstericisinin gösterdiği yer değiştirilebilir ancak onun kendisi değiştirilemez: void Sample::foo() const { m_name[0] = 'x'; // geçerli, çünkü m_name nesnesinin kendisi const, gösterdiği yer değil strcpy(m_name, "kaan"); // geçerli, çünkü m_name nesnesinin kendisi const, gösterdiği yer değil } Fakat burada mantıksal bir karışıklık söz konusu olabilmektedir. Örneğin bir String sınıfında replcae simli bir üye fonksiyon tutulan yazının belli bir karakterini belli bir karakterle yer değiştiriyor olsun: class String { public: //... void replace(char x, char y); private: char *m_str; size_t m_size; size_t m_capacity; }; Burada replace fonksiyonu aslında sınıfın veri elemanları üzerinde bir değişiklik yapmamaktadır. m_str göstericisinin gösterdiği yerdeki yazıda değişiklik yapmaktadır. Dolayısıyla aslında replace fonksiyonu const yapılabilir. Fakat mantıksal bakımdan durum ele alındığında sınıfı kullanan kişilerin sınıfın iç yapısını bilmek zorunda olmadığına göre, sınıfı kullan kişiler için replace işlemi "değişiklik yapan" bir işlemdir. Dolayısıyla her kadar bu fonksiyon const yapılabilirse de mantıksal bakımdan const yapılmaması daha uygun olabilmektedir. Yani buradaki replace fonksiyonu aslında const bir fonksiyon oalbilirse de mantıksal bakımdan const değildir. Dolayısıyla bu tür durumlarda böylesi fonksiyonların const yapılmamsı daha uygundur. Yukarıda biz aynı isimli ve aynı parametrik yapıya sahip const olan ve vonst olmayan üye fonksiyonların aynı sınıfta bulunabileceğini söylemiştik. Pekiyi bunun ne anlamı olabilir? String sınıfının at isimli ğye fonksiyonun yazının bir karakterinin adresiyle (ya da referabsıyla) geri döndüğünü varsayalım: class String { public: //... char &at(size_t index); private: char *m_str; size_t m_size; size_t m_capacity; }; char &String::at(size_t index) { return m_str[index]; } Aslında buradaki at fonksiyonu const üye fonksiyon yapılabilir. Çünkü const üye fonksiyon içerisinde m_str göstericisinin kendisi const durumdadır, onun gösterdiği yer const değildir. Dolayısıyla m_str[index] ifadesi const bir nesne belirtmez. Ancak burada da yine mantıksal bir uygunsuzluk vardır. Sınıfı kullanan kişiler bu at ile yazıda değişiklik yapabileceklerini anlarlar ve fonksiyonun const olması mantıksal bakımdan onlara çelişkili gelir. Bu tür durumlarda yine böylesi fonksiyonların aslında const yapılabileceği halde const yapılmaması uygun olur. Tabii biz artın onu const bir nesneyle çağıramayız. Örneğin: const String s{"ankara"}; s.at(5) = 'x'; // geçersiz! const nesne ile const olmayan üye fonksiyon çağrılamaz. Ancak burada da şöyle bir sıkıntı vardır: const bir nesne ile yazının belli bir karakterine erişip orayı değiştirmeyen bir at fonksiyonun da olması gerekir. İşte bu durumda sınıfta biri const olan diğeri olmayan iki at fonksiyonu bulundurulabilir. Tabii const olan at fonksiyonunun geri dönüş değeri const referans olmalıdır. Örneğin: class String { public: //... char &at(size_t index); const char &at(size_t index) const; private: char *m_str; size_t m_size; size_t m_capacity; }; char &String::at(size_t index) { return m_str[index]; } const char &String::at(size_t index) const { return m_str[index]; } Böylece artık aşağıdakine benzer işlemler yapılabilecektir: String s{"ankara"}; const String k{"ankara"}; s.at() = 'x'; // geçerli, const olmayan at çağrılacak k.at() = 'x'; // geçersiz! const olan at çağrılacak ancak const nesne değiştirilemez cout << k.at(5) << endl; // geçerli, const olan at çağrılacak ama referansın gösterdiği yer değiştirilmiyor --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- const bir üye fonksiyon içerisinde sınıfın bütün veri elemanlarının const kabul edildiğini belirtmiştik. Ancak bazen const üye fonksiyonların sınıfın bazı veri elemanlarını değiştirmesi gerekebilmektedir. İşte bunun sağlanması için veri elemanın başına mutable belirleyicisi getirilir. mutable elemanlar const üye fonksiyonlar içerisinde bile const olarak ele alınmazlar. Örneğin: #include using namespace std; class Sample { public: Sample(int val) { m_val = val; } const char *c_str() const { sprintf(m_text, "%d", m_val); return m_text; } private: int m_val; mutable char m_text[64]; }; Normal olarak buradaki c_str üye fonksiyonu const bir üye fonksiyon olduğu için sprimtf ile m_text dizisini değiştiremez. Ancak m_text dizisi mutable yapılarak const üye fonksiyonların bu diziyi değiştirmesi mümkün hale getirilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int val) { m_val = val; } const char *c_str() const { sprintf(m_text, "%d", m_val); return m_text; } private: int m_val; mutable char m_text[64]; }; int main() { Sample s{10}; cout << s.c_str() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf türünden referans const olabilir. Referans refere edilen nesneyi temsil ettiğine göre bu durumda referansın gösterdiği yerdeki (refere ettiği yerdeki) nesne değiştirilemez. Dolayısıyla biz const bir sınıf referansı ile sınıfın veri elemanlarını doğrudan ya da dolaylı bir biçimde değiştiremeyiz. const bir sınıf referansı ile sınıfın ancak const üye fonksiyonlarını çağırabiliriz. Örneğin: Sample s; const Sample &r = s; Burada r referansı ile Sample sınıfının ancak const üye fonksiyonları çağrılabilir. Benzer durum göstericiler için de aynı biçimde söz konusudur. Örneğin: Sample s; const Sample *ps = &s; Burada ps göstericisi "gösterdiği yer const olan" const bir göstericidir. Dolayısıyla biz ps göstericisi ile ancak sınıfın const üye fonksiyonlarını çağırabiliriz. Örneğin: void foo(const Sample &r) { //... } void bar(const Sample *ps) { //... } Burada fonksiyonlar adresini aldıkları nesne üzerinde değişiklik yapmama sözünü vermiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarında bir sınıfın veri elemanlarına ilkdeğer verilmesi MIL sentaksıyla (ctor sentaksıyla) yapılmaktadır. Yapıcı fonksiyonn gövdesi içerisinde değer atama ilkdeğer verme anlamına gelmemektedir. Örneğin: class Sample { Sample(int a, int b); //... private: int m_a; int m_b; }; Sample::Sample(int a, int b) : m_a(a) // m_a'ya ilkdeğer verilmiş { m_b = b; // bu bir ilkdeğer verme değil ilk kez değer atama } Burada her ne kadar m_a ile m_b veri elemanlarına değer atama arasında anlamsal bir farklılık yoksa da standart bağlamında m_a elemanına yapılan atamaya ilkdeğer verme denilmektedir. Örneğin sınıfın veri elemanı const ise bizim ona MIL sentaksıyla ilkdeğer vermemiz gerekir. Çünkü const nesnelere ilkdeğer verilmesi gerekmektedir ve ilkdeğer vermek standartlara göre MIL sentaksıyla değer vermektir. Örneğin: class Sample { public: Sample(); //... private: const int m_a; }; Sample::Sample() { m_a = 10; // geçersiz! bu ilk değer vermek değil //... } Burada m_a veri elemanı const olduğu için ona MIL sentaksında ilkdeğer vermek gerekiyordu: class Sample { public: Sample(); //... private: const int m_a; }; Sample::Sample() : m_a(10) // geçerli { //... } Artık biz const veri elemanına ilkdeğer vermiş durumdayız. /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 40. Ders 10/01/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ Sınıfın referans türünden bir veri elemanı da olabilir. Referanslar ilkdeğer verilerek tanımlanmak zorunda olduğuna göre veri elemanı olan referanslara MIL sentaksı ile ilkdeğer verilmesi gerekir. Örneğin: class Sample { public: Sample(int &r); int &r() const { return m_r; } //... private: int &m_r; }; Sample::Sample(int &r) : m_r(r) { //... } Burada m_r veri elemanına aynı türden bir nesne ile ilkdeğer verildiğine dikkat ediniz. Bu örnekte m_r parametre değişkeni ile belirtilen referansın gösterdiği nesneyi refere etmektedir. Tabii nesne yaşadığı sürece m_r referansının refere ettiği nesnenin de yaşaması gerekir. Örneğin: int x = 10; Sample s{x}; s.r() = 20; cout << x << endl; Burada aslında 20 değeri x değişkenine atanmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int &r); int &r() const { return m_r; } //... private: int &m_r; }; Sample::Sample(int &r) : m_r(r) { //... } int main() { int x = 10; Sample s{x}; s.r() = 20; cout << x << endl; // 20 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesi constexpr olabilir. Anımsanacağı gibi constexpr nesneler aynı zamanda const nesne kabul ediliyordu. Yine constexpr nesnelere verilen ilkdeğerlerin sabit ifadesi olması gerektiğini anımsayınız. C++11 ile birlikte constexpr sınıf nesneleri için çağrılacak yapıcı fonksiyonların constexpr yapıcı fonksiyonu olması gerekmektedir. Buradan da anlaşılacağı gibi yapıcı fonksiyonlar constexpr olabilirler. constexpr sınıf nesneleri için constexpr yapıcı fonksiyonları çağrıldığında sınıfın tüm veri elemanlarına sabit ifadeleri ile MIL sentaksında ilkdeğer veriliyor olması gerekmektedir. Örneğin: class Sample { public: constexpr Sample(); void disp() const; //... int m_a; int m_b; }; constexpr Sample::Sample() : m_a(10), m_b(20) { //... } void Sample::disp() const { cout << m_a << ", " << m_b << endl; } Burada constexpr yapıcı fonksiyonunda sınıfın m_a ve m_b veri elemanlarına sabit ifadeleriyle ilkdeğer verildiğine dikkat ediniz. Artık bu sınıf türünden constexpr bir nesne yaratıp kullanabiliriz: constexpr Sample s; s.disp(); // geçerli s constexpr yani aynı zamanda const, disp de const bir üye fonksiyon Burada artık constexpr sınıf nesnesi constexpr yapıcı fonksiyonu ile oluşturulmaktadır ve aynı zamanda sınıfın veri elemanlarına sabit ifadeleriyle ilkdeğer verilmiştir. constexpr bir sınıf nesnesinin veri elemanları constexpr biçimindedir. Dolayısıyla bu nesnenin veri elemanları sabit ifadesi olarak kullanılabilir. Örneğin: constexpr Sample s; constexpr int x = s.m_a // geçerli artık s.m_a sabit ifadesi belirtir constexpr yapıcı fonksiyonunda sınıfın veri elemanlarına parametredeki değişkenler atanabilir. Ancak bu durumda parametrelere sabit ifadesi biçiminde argüman geçirilmesi gerekmektedir. Örneğin: class Sample { public: constexpr Sample(int a, int b); void disp() const; //... int m_a; int m_b; }; constexpr Sample::Sample(int a, int b) : m_a(a), m_b(b) { //... } //... constexpr Sample s{10, 20}; // geçerli constexpr int x = s.m_a + s.m_b // geçerli Aşağıdaki yaratımın geçerli olmadığına error oluşturacağına dikkat ediniz: int a = 10, b = 20; constexpr Sample s{a, b}; // geçersiz! m_a ve m_b veri elemanlarına sabit ifadeleriyle ilkeğer verilmemiş oluyor Tabii constexpr yapıcı fonksiyonları normal nesneleri oluşturmak için de kullanılabilir. Örneğin: int a = 10, b = 20; Sample s{a, b}; // geçerli, s constexpr değil constexpr int x = s.m_a + s.m_b; // geçersiz! m_a ve m_b veri elemanları constexpr değil Burada s nesnesi constexpr değildir. Ancak bu nesneye constexpr yapıcı fonksiyonu ile ilkdeğer verilebilmektedir. Tabii bu durumda nesnenin m_a ve m_b veri elemanları constexpr elemanlar olmaz. Özetle sınıfın constexpr yapıcı fonksiyonları aynı zamanda normal yapıcı fonksiyon olarak da kullanılabilmektedir. Yukarıda da belirttiğimiz gibi C++'ın sürümü ilerledikçe constexpr fonksiyonlar gevşetilmiş ve adeta constexpr olmayan fonksiyonlar gibi bir yapıya bürünmüştür. Dolayısıyla constexpr olmayan nesneler constexpr yapıcı fonksiyonlarıyla oluşturulabnilmektedir. Bu durumda nesnenin veri elemanlarına sbit ifadesi ile ilkdeğer verilmeyebilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın üye fonksiyonları da constexpr olabilir. Bu üye fonksiyonlar çağrıldığında global constexpr fonksiyonların çağrılmasında olduğu gibi fonksiyona verilen argümanlar ve return ifadesi de sabit ifadesiyse fonksiyonun geri dönüş değeri de sabit ifadesi olarak ele alınmaktadır. Tabii constexpr üye fonksiyonlarda eğer üye fonksiyonun çağrıldığı nesne constexpr ise sınıfın veri elemanları da sabit ifadesi gibi kullanılabilmektedir. Ayrıca bir fonksiyonun geri dönüş değerinin sabit ifadesi olarak kullanılabilmesi için "onun constexpr olmasının gerek koşul ancak yeter koşul olmadığını" anımsayınız. Örneğin constexpr bir sınıf nesnesinin veri elemanlarını private bölüme yerleştirince bizim onlara public getter fonkisyonlarla erişmemiz gerekir. İşte bu getter fonksiyonların geri dönüş değerlerinin sabit ifadesi belirtmesi için onların constexpr üye fonksiyon olması gerekmektedir. Örneğin: class Sample { public: constexpr Sample(int a, int b); void disp() const; constexpr int a() const { return m_a;} constexpr int b() const { return m_b;} //... private: int m_a; int m_b; }; constexpr Sample::Sample(int a, int b) : m_a(a), m_b(b) { //... } Burada a ve b getter fonksiyonlarının constexpr olduğuna dikkat ediniz. Artık bu fonksiyonlar constexpr sınıf nesneleriyle çağrıldığında bunların geri dönüş değerleri sabit ifadesi belirtecektir: constexpr Sample s{10, 20}; // geçerli constexpr int x = s.a() + s.b(); // geçerli s.a() ve s.b() çağrıları sabit ifadesi belirtiyor Bu örnekte a ve b getter fonksiyonlarının aynı zamanda const üye fonksiyon olması gerektiğine dikkat ediniz. Eğer bu fonksiyonlar const üye fonksiyon yapılmazsa constexpr nesne ile bu fonksiyonlar çağrılamazdı (constexpr nesnelerin aynı zamanda const nesneler olduğunu anımsayınız.) constexpr üye fonksiyonlar constexpr olmayan nesnelerle de çağrılabilmektedir. Pekiyi biz bir sınıfın bir üye fonksiyonunun constexpr olduğunu gördüğümüzde ne anlamalıyız? İşte eğer üye fonksiyon constexpr ise bu üye fonksiyon çağrıldığında elde edilecek geri dönüş değeri uygun koşullar sağlanmışsa sabit ifadesi olarak ele alınabilecektir. Biz bir sınıf için hiç yapıcı fonksiyon yazmamışsak derleyici default yapıcı fonksiyonu kendisi içi boş olarak yazıyordu. İşte derleyicinin yazdığı bu default yapıcı fonksiyon bazı koşullar sağlanıyorsa (standartlar buna "constexpre suitable class" denilmektedir) constexpr kabul edilmektedir. Ancak anımsanacağı gibi derleyicinin kendisinin yazdığı default yapıcı fonksiyon MIL sentaksında hiçbir şey olmayan gövdesi boş olan bir yapıcı fonksiyondur. Örneğin: class Sample { //... }; constexpr Sample s; // geçerli Buradaki kod geçerlidir. Çünkü sınıfın bir veri elemanı olmadığı için constexpr sınıf nesnesinde sabit ifadeleriyle ilkdeğer verilmesi gereken bir veri elemanı yoktur. Yani yukarıdaki kod aşağıdaki ile eşdeğerdir: class Sample { public: constexpr Sample() {} }; constexpr Sample s; // geçerli Şimdi sınıfın static olmayan bir veri elemanı bulunuyor olsun: class Sample { public: int m_a; }; constexpr Sample s; // geçersiz! Atrık constexpr nesnenin derleyici tarafından yazılmış olan default yapıcı fonksiyon ile oluşturulması geçersizdir. Çünkü standartlara göre yukarıdaki kodun eşdeğeri şöyledir: class Sample { public: constexpr Sample() {} int m_a; }; constexpr Sample s; // geçersiz! Görüldüğü atrık constexpr nesne için çağrılan yapıcı fonksiyon sınııfn m_a veri elemanına bir sabit ifadesi ile ilkdeğer vermemiş durumdadır. Sınıfların yıkıcı fonksiyonları da constexpr olabilmektedir. C++ standartlarına göre constexpr bir sınıf nesnesi için çağrılacak yıkıcı fonksiyonun constexpr olması gerekmektedir. Örneğin: class Sample { public: constexpr Sample() : m_a(10) {} ~Sample() {} private: int m_a; }; Burada aşağıdaki gibi constexpr bir nesne yaratamayız: constexpr Sample s; // geçersiz! yıkıcı fonksiyon constexpr değil constexpr nesnenin tanımlanabilmesi için yıkıcı fonksiyonun constexpr olması gerekirdi: class Sample { public: constexpr Sample() : m_a(10) {} constexpr ~Sample() {} private: int m_a; }; constexpr Sample s; // geçerli Standartlara göre biz bir sınıf için yıkıcı fonksiyon yazmasak derleyicinin yazdığı içi boş yıkıcı fonksiyon özel bazı koşulları da sağlıyorsa ("constexpr suitable" ise) constexpr biçimdedir. Yukarıda açıkladığımız durum açıkça defaulted hale getirilmiş yapıcı ve yıkıcı fonksiyonlar için de geçerlidir. Örneğin: class Sample { public: Sample() = default; ~Sample() = delete; }; constexpr Sample s; // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- const üye fonksiyonlarla constexpr üye fonksiyonların farklı anlamlara geldiğine dikkat ediniz. const üye fonksiyonlar const nesnelerle çağrılırlar. constexpr fonksiyonlar da eğer constexpr nesnelerle çağrılırsa sabit ifadesi belirtmektedir. En normal olan durum constexpr üye fonksiyonların aynı zamanda const olmasıdır. Mevcut standartlarda bir üye fonksiyon constexpr olduğu halde const üye fonksiyon olmayabilir. Ancak böyle fonksiyonlar constexpr nesnelerle çağrılamazlar. Üye fonksiyonun constexpt olduğu halde const olmaması çok seyrek bazı durumlarda esneklik kazandırmaktadır. Ancak belirttiğimiz gibi en normal durum constexpr üye fonksiyonların aynı zamanda const üye fonksiyonlar olmasıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi constexpr olmayan bir sınıf nesnesine constexpr bir yapıcı fonksiyon ile ildeğer verilmiş olsa bile nesnenin veri elemanları constexpr kabul edilmemektedir. Örneğin: class Sample { public: constexpr Sample(int a) : m_a(a) {} int m_a; }; //... Sample s{10}; constexpr int a = s.m_a; // geçersiz! s constexpr değil, dolayısıyla s.m_a da constexpr değil Ancak mevcut C++ standartlarına göre constexpr yapıcı fonksiyon ile oluşturulmuş olan geçici sınıf nesnelerine eğer constexpr nesne gibi ilkdeğer verilmişse bu durumda bu geçici nesne nesne ile sınıfın veri elemanlarına erişildiğinde bu veri elemanları constexpr nesneler gibi ele alınmaktadır. Örneğin: class Sample { public: constexpr Sample(int a) : m_a(a) {} int m_a; }; //... constexpr int a = Sample(10).m_a; // geçerli, artık yaratılan geçici nesne constexpr gibi ele alınıyor Biz henüz sınıflar türünden geçici nesnelerin oluşturulmasını görmedik. Bu nedenle bu konu üzerinde şimdilik çok takılmayınız. Sınıflar türünden geçici nesnelerin yaratılması ve kullanılması izleyen paragraflarda ele alınmaktadır. Şimdi neden constexpr üye fonksiyonların default olarak const üye fonksiyon kabul edilmediğini merak ediyor olabilirsiniz. Mevcut C++ standartlarına göre eğer constexpr yapıcı fonksiyon ile oluşturulmuş olan geçici sınıf nesnelerine constexpr nesne gibi ilkdeğer verilmişse bu durumda bu geçici nesne yoluyla sınıfın constexpr olan ancak const olmayan üye fonksiyonları çağrılabilir ve bu çağrı ifadesi diğer koşulları sağlıyorsa sabit ifadesi olarak ele alınabilmektedir. Örneğin: class Sample { public: constexpr Sample(int a) : m_a(a) {} constexpr int set_and_inc(int a) { m_a = a; return m_a; } private: int m_a; } //... constexpr int a = Sample(10).set_and_inc(20); // geçerli, geçici nesne yoluyla sınıfın const olmayan ama constexpr olan üye fonksiyonu çağrılmış --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new operatörü sabit ifadesi yaratmakta mıdır? Mevcut C++ standartlarına göre new operatörünün sabit ifadesi yaratabilmesi için new operatörü ile tahsisatın yapıldığı ifade sonlandığında o tahsisatın delete operatörü ile siliniyor olması gerekir. Örneğin: class Sample { public: constexpr Sample(int a) : m_pa(new int(a)) {} constexpr ~Sample() { delete m_pa; } constexpr int a() { return *m_pa;} private: int *m_pa; }; Burada biz Sample sınıfı türünden constexpr bir nesneyi aşağıdaki gibi yaratamayız: constexpr Sample s{10}; // geçersiz! Buradaki sorun derleme aşamasında ele alınan new operatörü için delete işleminin çalışma zamanında yapılmasıdır. Yani bu haliyle s nesnesi aslında sabit ifadesi biçiminde değerlendirilememektedir. Ancak bunun şöyle bir istisnası vardır: Eğer nesne geçici nesne olarak yaratılırsa bu durumda ilgili ifadenin sonunda zaten yıkıcı fonksiyon çalışacağı için bu nesne de yalnızca o ifadede kullanılabileceği için new işlemi sabit ifadesi oluşturmada bir soruna yol açmamaktadır. Örneğin: constexpr int x = Sample{10}.a(); // geçerli Yukarıdaki Sample{10}.a() ifadesi artık sabit ifadesi oluşturmaktadır. Bu ifadede her ne kadar yine new operatörü kullanılmışsa da delete ile boşaltım ifadenin sonunda yapılacağı için söz konusu bu ifade bir yan etkiye yol açmadan derleme aşamasında yapılabilecektir. Tabii bu anlatımın anlaşılabilebilmesi için "geçici sınıf nesnelerinin yaratılması" konusunun biliniyor olması gerekmektedir. İzleyen paragraflarda geçici sınıf nesnelerin yaratılması konusu el alınmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın standart kütphanesinde zamanla değişik sürümlerde eski sınıfların pek çok üye fonksiyonları constexpr haline getirilmiştir. Ancak bu sınıfları kullanırken mevcut kütüphanenin (ve derleyicinin) bunu destekleyip desteklemediğine dikkat ediniz. Microsoft'un ve GNU'nun standart C++ kütüphaneleri standartlara göre biraz geriden gelmektedir. Derleyicileri yazanlar ile standart kütüphaneyi yazanlar genellikle farklı proje gruplarıdır. Derleyicinin sürümünün ileri olması standart kütüphanenin o sürümde olmasını sağlamayabilmektedir. Örneğin standart string sınıfının bazı yapıcı fonksiyonları ve bazı üye fonksiyonları zamanla constexpr haline getirilmiştir. örneğin string sınıfın const char * parametreli yapıcı fonksiyonu C++20 ile birlikte constexpr yapılmıştır. Benzer biçimde size üye fonksiyonu da C++20 ile birlikte constexpr yapılmıştır. Ancak yukarıdaki paragrafta belirttiğimiz nedenlerden dolayı biz yine C++20 bile olsa aşağıdaki gibi string sınıfı türünden constexpr bir nesneyi tanımlayamayız: constexpr string s{"ankara"}; // geçersiz! Ancak yine yukarıdaki paragrafta belirttiğimiz gerekçelerden dolayı aşağıdaki gibi geçici nesne yoluyla sabit ifadeleri oluşturulabilmektedir: constexpr string::size_type n = string{"ankara"}.size(); // geçerli Ancak maalesef mevcut standartlara göre (C++20) yukarıdaki tanımlama geçerli lolduğu halde henüz g++ derleyicisi bu kodu geçerli bir biçimde derleyememektedir. Microsoft'un güncel C++ derleyicileri yukarıdaki kod derleyebilmektedir. clang++ derleyicisi "-std=c++2b" seçeneği ile derleyebilir hale gelmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Mademki artık C++ ile birlikte neredeyse constexpr fonksiyonlar normal fonksiyon gibi de kullanılabilmektedir. O halde genel olarak şu tavsiyede bulunulmaktadır: "Eğer "inline olabilecek global bir fonksiyonu ya da üye fonksiyonu constexpr olarak yazabiliyorsanız constexpr olarak yazınız." constexpr fonksiyonların aynı zamanda inline olduğunu anımsayınız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta kursta geldiğimiz konular çerçevesinde ilkdeğer verme anlamına gelen üç tipik durum vardır: 1) Açıkça tanımlamada tanımlanan nesneye ilkdeğer verilmesi durumu. Örneğin: int a = 10; int b{10}; int c(10); 2) Fonksiyon çağrılırken argümanlardan parametre değişkenlerine aktarımın yapıldığı durum. Örneğin: void foo(int a) // int a = 10 { //... } //... foo(10); 3) return işleminde geri dönüş değerinin oluşturulmasındaki durum. Örneğin: int foo() // int temp = 10 { //... return 10; } C++ stanadartlarına göre bir nesneye normal parantezlerle ya da küme parantezleriyle (yani '=' atomu kullanılmadan) ilkdeğer verme işlemine "direct initialization", '=' atomu kullanılarak ilkdeğer verme işlemine, fonksiyon çağrımı sırasında parametre değişkenine argüman yoluyla ilkdeğer verme işlemine ve return deyimi ile geçici nesneye ilkdeğer işlemine ise "copy initialization" denilmektedir. Örneğin: int a{10}; // direct initialization int a(10); // direct initialization int b = 10; // copy initialziation void foo(int a) { //... } foo(10); // copy initialization int bar() { //... return 10; // copy initialization } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesinin aynı türden bir sınıf nesnesiyle ilkdeğer verilerek tanımlandığını düşünelim. Örneğin: Sample s; Sample k{s}; Bu durum bir fonksiyonun çağrılması sırasında da söz konusu olabilir. Çünkü fonksiyon çağırma işlemi aslında parametre değişkenine ilkdeğer verme işlemi gibi ele alınmaktadır. Örneğin: void foo(Sample k) { //... } //... Sample s; foo(s); Burada da aslında Sample k = s gibi bir işlem yapılmaktadır. Benzer biçimde return işlemi de aslında return işlemiyle yaratılacak olan geçici nesnye ilkdeğer verme işlemidir. Örneğin: Sample foo() { Sample s; // return s; } Burada da aslında Sample temp = s gibi bir işlem yapılmaktadır. Bir sınıf nesnesini aynı sınıf türünden bir nesneyle ilkdeğer vererek yarattığımız durumda ne olmaktadır? Örneğin: Sample s; Sample k{s}; C'de aynı türden iki yapı nesnesinin birbirilerine atandığı durumda onların karşılıklı elemanlarının birbirine atandığını biliyorsunuz. Gerçekten de C++'ta da bu tür durumlarda default olarak nesnenin karşılıklı veri elemanlarını birbirine atanmaktadır. Örneğin: class Complex { public: Complex(double real, double imag) : m_real(real), m_imag(imag) {} //... private: double m_real, m_imag; }; //... Complex x{3, 2}; Complex y{x}; Burada C++'ta default durumda x'in m_real veri elemanı y'nin m_real veri elemanına, x'in m_imag veri elemanı y'nin m_imag veri elemanına atanacaktır. Ancak yapılan bu default atama bazı durumlarda sorunlara yol açabilmektedir. Örneğin sınıfın bir gösterici veri elemanı olduğunda karşılıklı veri elemanlarının birbirlerine atanması gösterici içerisindeki adreslerin birbirine atanmasına yol açacağı için sorun oluşturma potansiyeline sahiptir. Daha önce yazmış olduğumuz String sınıfını çok yalın bir biçimde aşağıdaki gibi yeniden yazalım: class String { public: String(const char *str); ~String(); void disp() const; private: char *m_str; size_t m_size; }; String::String(const char *str) { m_size = strlen(str); m_str = new char[m_size + 1]; strcpy(m_str, str); } String::~String() { delete[] m_str; } void String::disp() const { cout << m_str; } Şimdi string nesnesinin yine bir String nesnesi ile ilkdeğer verilerek tanımlanmış olduğu aşağıdaki örneğe dikkat ediniz: String s{"ankara"}; { String k{s}; // dikkat! adres kopyalaması yapılmaktadır //... } s.disp(); Burada String k{s} tanımlamasında s'nin karşılıklı veri elemanları k'ya kopyalanırsa bu durumda s.m_str ile k.m_str aynı nesneyi gösterir hale gelir. Daha sonra ömrü kısa olan k için yıkıcı fonksiyon çağrıldığında bu fonksiyon k nesnesinin m_str göstericisi ile gösterilen alanı boşaltacağı için artık s nesnesinin m_str elemanının gösterdiği yer de boşaltılmış olacaktır. Böylece iç bloktan çıkıldığında s nesnesi bozuk bir durumda olacaktır. Buradan çıkan sonuç şudur: Bu tür durumlarda karşılıklı veri elemanlarının kopyalanması özellikle gösterici veri elemanları söz konusu olduğunda sorunlara yol açabilmektedir. Pekiyi bu problem nasıl çözülebilir? En uygun çözüm bu tür durumlarda "içerik kopyalamasının" yapılmasıdır. Yani gösterici veri elemanlarının gösterdiği yerin de kopyasından çıkartılması böylese iki nesnenin gösteri veri elemanlarının farklı alanları göstermesinin sağlanmasıdır. Bu tür kopyalamalara diğer programlama dilelrinin bazılarında "derin kopyalama (deep copy)" de denilmektedir. İşte C++'ta bir sınıf nesnesinin aynı sınıf türünden bir nesneyle ilkdeğer verilerek yaratılması durumunda yaratılan nesne için "kopya yapıcı fonksiyonu (copy constructor)" denilen bir yapıcı fonksiyon çağrılmaktadır. Örneğin: Sample s; Sample k{s}; Burada s için default yapıcı fonksiyon k için ise kopya yapıcı fonksiyon çağrılmaktadır. Eğer programcı sınıfı için kopya yapıcı fonksiyonu yazmazsa kopya yapıcı fonksiyonu derleyici tarafındna sınıfın karşılıklı veri elemanlarını birbirine atayacak biçimde (ilkdeğer verecek biçimde) yazılmaktadır. İşte çoğu kez derleyicinin kendisinin yazdığı bu yapıcı fonksiyon işimizi görmektedir. Ancak yukarıdaki String örneğinde olduğu gibi bazı durumlarda bizim bu fonksiyonu içerik kopyalaması yapacak biçimde yazmamız gerekir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın kopya yapıcı fonksiyonu sınıfın kendi türünden referans parametreli yapıcı fonksiyonudur. Sınıfın ismi T olmak üzere aşağıdaki parametrelere sahip yapıcı foksiyonların hepsi kopya yapıcı fonksiyon (copy contructor) olarak kullanılabilir: const T & T & volatile T & const volatile T & Sınıfın en çok kullanılan kopya yapıcı fonksiyonu ""const T &" parametreli yapıcı fonksiyonudur. Sınıfın T parametreli (referans olmayan parametreli) yapıcı fonksiyonunun kopya yapıcı fonksiyonu olarak ele alınmadığına dikkat ediniz. İleride de anlayacağız gibi eğer böyle bir yapıcı fonksiyon olabilseydi bazı durumlarda sonsuz döngü oluşurdu. Sınıfın en çok kullanılan kopya yapıcı fonksiyonunun "const T &" parametreli kopya yapıcı fonksiyonu olduğunu söylemiştik. Bunun nedenini izleyen paragraflarda anlayacaksınız. Tabii kopya yapıcı fonksiyonları da overload edilebilir. Örneğin. class Sample { public: //... Sample(const Sample &r); Sample (Sample &r); //... }; Yukarıdaki iki kopya yağıcı fonksiyon sınıfta birlikte bulunabilir. /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 42. Ders 17/01/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Kopya yapıcı fonksiyonu bir sınıf türünden nesnenin aynı sınıf türünden bir nesneyle ilkdeğer verilerek yaratılması durumlarında çağrılmaktadır. Daha açık bir ifade ile kopya yapıcı fonksiyonları tipik olarak şu durumlarda çağrılmaktadır: 1) Bir sınıf nesnesinin aynı sınıf türünden bir nesneyle doğruadan ya da '=' atomuyla ilkdeğer verilerek tanımlanması durumunda. Örneğin: Sample s; Sample k{s}; Burada ilkdeğer vermenin doğurdan (direct initialization) ya da '=' atomu ile yapılması (copy initialization) arasında hiçbir farklılk yoktur. Yani örneğin aşağıdaki tanımlamada da kopya yapıcı fonksiyonu çağrılacaktır: Sample s; Sample k = s; İleride de ele alacağımız gibi aslında doğrudan ilkdeğer verme ile '=' atomu ile ilkdeğer verme arasında bazı farklılıklar oluşabilmektedir. Ancak kaynak tür yaratılacak hedef türle aynı sınıf türündense burada bir farklılık oluşmamaktadır. 2) Fonksiyonun parametre değişkeni bir sınıf türünden ise ve u fonksiyon aynı sınıf türünden bir sınıf nesnesiyle çağrıldığında parametre değişkeni için de kopya yapıcı fonksiyonu çağrılır. Örneğin: void foo(Sample k) { //... } //... Sample s; foo(s); Burada parametre değişkeni olan k için kopya yapıcı fonksiyonu çağrılır. 3) Fonksiyonun geri dönüş değeri bir sınıf türünden olabilir. Bu durumda return işleminde return anahtar sözcüğünün yanında aynı sınıf türünden bir nesne olmalıdır. İşte geri dönüş değeri için yaratılacak olan geçici nesne için sınıfın kopya yapıcı fonksiyonu çağrılır. Örneğin Sample foo() { Sample s; //... return s; } Burada return işlemi oluşturulacak geçici nesne için kopya yapıcı fonksiyonu çağrılır. Yukarıda da belirtitğimiz gibi sınıfın kopya yapıcı fonksiyonu içerik kopyalaması (derin kopyalama) yapacak biçimde yazılmalıdır. Kopya yapıcı fonksiyonu derleyici tarafından çağrıldığında ilkdeğer olarak verilen nesnenin adresi kopya yapıcı fonksiyonunun referans parametresine aktarılır. Kopya yapıcı fonksiyonunu yazan programcı da o nesnenin içeriğini yeni yaratılan nesnede oluşturmaya çalışır. Örneğin: class Sample { public: Sample(); // default constructor Sample(const Sample &r); // copy constructor //... }; Sample::Sample(const Sample &r) { //... } //.... Sample s; Sample k = s; Burada k nesnesi için kopya yapıcı fonksiyonu çağrılır. s nesnesinin adresi fonksiyonun r referans parametresine aktarılır. Sınıfın başka sınıf türünden veri elemanlarının bulunduğu durumda sınıfın kopya yapıcı fonksyonu bu veri elemanları için kendi sınıflarının kopya yapıcı fonksiyonlarının çağrılmasını sağlamalıdır. Eğer programcı bunu sağlamazsa bu veri elemanları için kopya yapıcı fonksiyonu değil default yapıcı fonksiyon çağrılmaktadır. Bu işlemin nasıl yapılacağı ileride ele alınacaktır. Aşağıda basitleştirilmiş bir String sınıfı için kopya yapıcı fonksiyonun yazılmasına bir örnek verilmişti. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class String { public: String(const char *str); ~String(); String(const String &r); void disp() const; private: char *m_str; size_t m_size; }; String::String(const char *str) { m_size = strlen(str); m_str = new char[m_size + 1]; strcpy(m_str, str); } String::String(const String &r) { m_str = new char[r.m_size + 1]; strcpy(m_str, r.m_str); m_size = r.m_size; } String::~String() { delete[] m_str; } void String::disp() const { cout << m_str << endl; } void foo(String k) // String k = s { k.disp(); } int main() { String s{"ankara"}; foo(s); s.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda sınıfın en çok kullanılan kopya yapıcı fonksiyonunun "const T &" parametreli yapıcı fonksiyon olduğunu belirtmiştik. Pekiyi yapıcı fonksiyonu biz "T &" parametreli yazarsak ne olur? Örneğin: class Sample { public: //... Sample(Sample &r); //... }; Bu durumda ildeğer olarak verilen nesne const ise derleme aşamasında error oluşacaktır. Örneğin: const Sample s; Sample Sample k{s}; // geçersiz! const bir nesnenin adresi const olmayan bir sol taraf değeri referansına bind edilemez. Halbuki kopya yapıcı fonksiyonunun parametresi "const T &" olsaydı bir sorun oluşmayacaktı. Tabii pek karşılaşılmasa da sınıfta "const T &" ve "T &" parametreli iki ayrı kopya yapıcı fonksiyonu da bulunabilir. Örneğin: class Sample { public: //... Sample(const Sample &r); Sample(Sample &r); //... }; //... Sample a; const Sample b; Sample c{a}; // Sample & parametreli kopya yapıcı fonksiyonu çağrılır Sample d{b}; // const Smaple & parametreli kopya yapıcı fonksiyonu çağrılır Eğer biz sınıf için hiç kopya yapıcı fonksiyonu yazmazsak derleyici tarafından yazılan kopya yapıcı fonksiyonu normal olarak "const T &" parametrelidir. Ancak özel bir durum da söz konusudur. Eğer sınıfın başka sınıf türünden veri elemanları varsa (aynı durum sınıfın taban sınıfı için de geçerlidir) ve bu elemanlara ilişkin (ya da taban sınıfa ilişkin) sınıfların "const T &" parametreli yapıcı fonksiyonları yoksa bu durumda derleyicinin yazdığı default kopya yapıcı fonksiyonu "T &" parametreli olmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirtitğimiz gibi biz sınıfımız için kopya yapıcı fonksiyonunu yazmazsak derleyici bizim için kopya yapıcı fonksiyonunu sınıfın karşılıklı veri elemanlarını kopyalayacak biçimde (ilkdeğer verecek biçimde) kendisi yazmaktadır. Bu tür kopyalamalara C++'ta İngilizce "memberwise copy" denilmektedir. Örneğin: class Complex { public: Complex() = default; Complex(double real, double imag = 0); void disp() const; private: double m_real; double m_imag; }; //... Complex x{3, 2}; Complex y{x}; Böyle bir sınıfta kopya yapıcı fonksiyonun yazılmasına hiç gerek yoktur. Zaten biz böyle bir sınıf için kopya yapıcı fonksiyonunu yazacak olsak derleyicinin default olarak yazacağı kopya yapıcı fonksiyonu ile aynı şeyi yaparız. Yani kaynak nesnenin m_real ve m_imag elemanlarını hedef nesnenin karşılıklı olarak m_real ve m_imag elemanına atarız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf için kopya yapıcı fonksiyonu yazılmadığında derleyicinin yazdığı kopya yapıcı fonksiyonu karşılıklı veri elemanlarının atanması işlemini ilkder veriliyormuş gibi yapmaktadır. Başka bir deyişle derleyicinin yazdığı kopya yapıcı fonksiyonu sanki MIL sentaksında kerşılıklı veri elemanları ilkdeğer verilerek kopyalanaıyormuş gibi işlem görmektedir. Örneğin: class Complex { public: Complex() = default; Complex(double real, double imag = 0); void disp() const; private: double m_real; double m_imag; }; Burada sınıf için kopya yapıcı fonksiyonu yazılmamıştır. Derleycinin yazdığı kopya yapıcı fonksiyon "memberwise copy" işlemini aşağıdaki gibi yapmaktadır: Complex::Complex(const Complex &r) : m_real(r.m_real), m_imag(r.m_imag) {} --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi kopya yapıcı fonksiyonunun yazımı sınıfın bir elemanının gösteri olduğu durumda mı gerekmektedir? Aslında bazı durumlarda veri elemanı bir gösterici olmasa da kopya yapıcı fonksiyonun yazılması gerekebilmektedir. Örneğin UNIX/Linux sistemlerinde bir dosya açıldığında dosyayı betimleyen handle değeri int türdendir. Aynı handle değeri aynı dosyayı belirtmektedir. Böylesi bir durumda sınıfın veri elemanı "file descriptor" denilen bu handle değerini tutuyorsa bu değerin hedef nesneye aynı gerekçelerle doğrudan atanmaması gerekir. Burada içerik kopyalaması handle değerinin çiftlenmesi yoluyla yapılır. Aşağıda böyle bir temaya örnek verilmiştir. Biz bu örnekte throw deyimi ile exception fırlattık. Exception konusu kursumuzun sonlarına doğru ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include using namespace std; constexpr int BUFFER_SIZE = 4096; class File { public: File(const char *path); File(const File &r); ~File(); void disp() const; private: int m_fd; }; File::File(const char *path) { if ((m_fd = open(path, O_RDONLY)) == -1) return; } File::File(const File &r) { if ((m_fd = dup(r.m_fd) )== -1) throw runtime_error("file cannot open"); } File::~File() { close(m_fd); } void File::disp() const { char buf[BUFFER_SIZE + 1]; ssize_t result; lseek(m_fd, 0, 0); while ((result = read(m_fd, buf, BUFFER_SIZE)) > 0) { buf[result] = '\0'; cout << buf; } if (result == -1) throw runtime_error("cannot read file"); cout << endl; } void foo(File f) { //... } int main() { File f("xsample.cpp"); foo(f); f.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 43. Ders 22/01/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C'de bir fonksiyonun yapı gibi bileşik bir nesneyle geri dönmesi genel olarak iyi teknik kabul edilmemektedir. Örneğin iki karmaşık sayıyı toplayan bir fonksiyon şöyle olabilir:: struct COMPLEX add(const struct COMPLEX *z1, const struct COMPLEX *z2); Burada fonksiyonun geri dönüş değerinin bir yapı nesnesi oldupuna dikkat ediniz. O halde bu fonksiyonun return ifadesi de aynı türden bir yapı nesnesi olmalıdır: struct COMPLEX add(const struct COMPLEX *z1, const struct COMPLEX *z2) { struct COMPLEX result; result.real = z1->real + z2->real; result.imag = z1->imag + z2->imag; return result; } Fonksiyonun şöyle kullanıldığını varsayalım: struct COMPLEX z1 = {3, 2}; struct COMPLEX z2 = {7, 6}; struct COMPELX z3; z3 = add(&z1, &z2); Burada hem return ifadesinde hem de atama ifadesinde yapı nesnesi iki kere bütünsel olarak kopyalanmaktadır. Alternatif taarım şöyle olabilirdi: void add(const struct COMPLEX *z1, const struct COMPLEX *z2, struct COMPLEX *result) { result->real = z1->real + z2->real; result->imag = z1->imag + z2->imag; } Bu fonksiyon şöyle kullanılabilir: struct COMPLEX z1 = {3, 2}; struct COMPLEX z2 = {7, 6}; struct COMPELX z3; z3 = add(&z1, &z2, &z3); İşte bu nedenle C'de programcılar genellikle fonksiyonun geri dönüş değerini yapı türünden yazpmazlar. Ancak C++'ta bir fonksiyonun bir sınıf nesnesiyle geri dönmesi C'deki gibi kötü teknik kabul edilmemektedir. Tabii aslında arka planda sınıf nesneleri C'deki yapı gibi organize edildiğine göre benzer zaman kaybı yine oluşaşaktır. Ancak C++'ın C'den daha yüksek seviyeli olduğunu anımsayınız. Bu nedenle fonksiyonların sınıf nesnelerine geri dönmesi C++'ta C'deki gibi kötü teknik kabul edilmemektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Matlab gibi, R gibi bazı ortamlar ve dillerin vektörel işlem yapabilme özelliği bulunmaktadır. Vektörel işlem demekle iki dizinin karşılıklı elemanlarının tek hamlede işleme sokulması anlaşılmaktadır. Bu özellik matematiksel uygulamalarda programcının işini çok kolaylaştırmaktadır. Python Programlama Dilinde NumPy isimli üçüncü parti kütüphane de bu amaca hizmet etmektedir. Aşağıda kopya yapıcı fonksiyonun gerekliliğine ilişkin diğer bir örnek verilmiştir. Bu örnekte vektörel işlem yapabilmek için VArray isimli bir sınıf bulunmaktadır. Bir VArray nesnesi dinamik olarka tahsis edilen double türden bir dizinin adresini ve uzunluğunu tutmaktadır. Sınııfn içerisindeki add, sub, mul, div üye fonksiyonları iki VArray nesnesinin karşılıklı elemanlarını işleme sokup yeni bir VArray nesnesine geri dönmektedir. Bu fonksiyonların geri dönüş değerleri oluşturulurken geçici nene için kopya yapıcı fonksiyonun çağrılacağına dikkat ediniz. Sınıftaki kopya yapıcı fonksiyonu "içerik kopyalaması" yapacak biçimde yazılmıştır. Örneğimizde kullanımı kolaylaştırmak için sınıfta "initializer_list" parametreli bir yapıcı fonksiyon bulundurkduk. Bu yapıcı fonksiyon sayesinde küme parantezleriyle nesnemize ilkdeğer verilemesini sağladık. Örneğin: VArray x = {1, 2, 3, 4, 5}; Bu konu ileride ayrı bir başlık altında ele alınacaktır. İki vektörün bu biçimde işleme sokulması için vektörlerin eşit uzunlukta olması gerektiğine dikkat ediniz. Biz de örneğimizde henüz görmemiş olsak da başlangıçta bu kontrolü yaptık. Eğer üişleme soktuğumuz nesnelere ilişkin diziler eşit uzunlukta değilse exception fırlattık. Exception konusunu da henüz görmedik. İleride bu konu da ayrı bir bölümde ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // varray.hpp #ifndef VARRAY_HPP_ #define VARRAY_HPP_ #include #include class VArray { public: VArray() = default; VArray(size_t size); VArray(const double *v, size_t size); VArray(std::initializer_list il); VArray(const VArray &va); ~VArray(); VArray add(const VArray &va) const; VArray add(double d) const; VArray sub(const VArray &va) const; VArray sub(double d) const; VArray mul(const VArray &va) const; VArray mul(double d) const; VArray div(const VArray &va) const; VArray div(double d) const; VArray pow(double d) const; double sum() const; double mean() const; size_t size() const { return m_size; } void disp() const; private: double *m_v; size_t m_size; }; #endif // varray.cpp #include #include #include #include #include "varray.hpp" using namespace std; VArray::VArray(size_t size) { m_v = new double[size]; m_size = size; } VArray::VArray(const double *v, size_t size) : VArray(size) { ::memcpy(m_v, v, sizeof(double) * size); } VArray::~VArray() { delete[] m_v; } VArray::VArray(initializer_list il) { m_v = new double[il.size()]; m_size = il.size(); for (size_t i = 0; auto val : il) m_v[i++] = val; } VArray::VArray(const VArray &va) { m_v = new double[va.m_size]; m_size = va.m_size; ::memcpy(m_v, va.m_v, sizeof(double) * m_size); } VArray VArray::add(const VArray &va) const { if (m_size != va.m_size) throw invalid_argument("Varrays must be the same size"); VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] + va.m_v[i]; return result; } VArray VArray::add(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] + d; return result; } VArray VArray::sub(const VArray &va) const { if (m_size != va.m_size) throw invalid_argument("Varrays must be the same size"); VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] - va.m_v[i]; return result; } VArray VArray::sub(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] - d; return result; } VArray VArray::mul(const VArray &va) const { if (m_size != va.m_size) throw invalid_argument("Varrays must be the same size"); VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] * va.m_v[i]; return result; } VArray VArray::mul(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] * d; return result; } VArray VArray::div(const VArray &va) const { if (m_size != va.m_size) throw invalid_argument("Varrays must be the same size"); VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] / va.m_v[i]; return result; } VArray VArray::div(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] / d; return result; } VArray VArray::pow(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = ::pow(m_v[i], d); return result; } double VArray::sum() const { double total = 0; for (size_t i = 0; i < m_size; ++i) total += m_v[i]; return total; } double VArray::mean() const { return sum() / m_size; } void VArray::disp() const { cout << '['; for (size_t i = 0; i < m_size; ++i) cout << m_v[i] << " "; cout << ']' << endl; } // app.cpp #include #include "varray.hpp" using namespace std; int main() { VArray x = {1, 2, 3, 4, 5}; VArray y = {4, 5, 8, 9, 1}; x.disp(); y.disp(); VArray result = x.mul(10).add(y); // x * 10 + y result.disp(); cout << "sum: " << x.sum() << endl; cout << "mean: " << x.mean() << endl; double std = sqrt(x.sub(x.mean()).pow(2).sum() / x.size() - 1); cout << "standard deviation: " << std << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Derleyici tarafından yaratılan ve yine derleyici tarafından yok edilen isimsiz nesnelere "geçici nesneler (temporary objects)" denilmektedir. Geçici nesneler çeşitli durumlarda oluşturulabilmektedir. Örneğin tür dönüştürmeleri, return işlemi geçici nesneler yoluyla yapılmaktadır. İşte sınıflar türünden de geçici nesneler oluşturulabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta T bir tür ismi (genellikle sınıf) olmak üzere T([argüman_listesi]) biçminde ya da T{[argüman listesi]} biçiminde bir ifade "T türünden geçici nesne yarat" anlamına gelmektedir. Bu biçimde geçici nesne yaratıldığında derleyici eğer yaratılan bir sınıf nesnesi ise bu nesne için argüman listesine uygun yapıcı fonksiyonunu çağırır. Sonra bu geçici nesne bu geçici nesnenin yaratıldığı ifade bittiğinde yıkıcı fonksiyon çağrılarak geçici nesne yok edilir. Yani bu biçimde yaratılmış olan geçici nesnelerin ömürleri o nesnenin yaratıldığı ifade kadardır. O nesnenin yaratıldığı ifadenin bitiminde nesne için yıkıcı fonksiyon çağrılarak nesne yok edilmektedir. Eğer bir ifadede birden fazla geçici sınıf nesnesi yaratılmışsa onların yıkıcı fonksiyonları yapıcı fonksiyonlarına göre ters sırada çağrılmaktadır. Örneğin: void foo(Point pt) { //... } //... foo(Point{3, 2}); Burada Point sınıfı türünden geçici bir nesne yaratılmış ve bu geçici nesne foo fonksiyonunun pt parametresine ilkdeğer olarak verilmiştir. Bu durumda pt için kopya yapıcı fonksiyonunun çağrılacağına dikkat ediniz. (C++17 ile birlikte burada "copy elision" uygulaması artık zorunlu hale getirilmiştir. İzleyen paragraflarda bu konu ele alınacaktır.) Burada yaratılmış olan geçici Point nesnesi tüm ifade bittiğinde (yani fonksiyonun çağrısı bittiğinde) yok edilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Point { public: constexpr Point(int x, int y) : m_x(x), m_y(y) {} constexpr int x() { return m_x; } constexpr int y() { return m_y; } void disp() const; private: int m_x; int m_y; }; void Point::disp() const { cout << '(' << m_x << ',' << m_y << ')' << endl; } void foo(Point pt) { pt.disp(); } int main() { Point pt{10, 2}; foo(Point(10, 2)); // geçici nesne yaratılıyor return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 44. Ders 24/01/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte geçici biçimde yaratılan sınıf nesneleri için yapıcı ve yıkıcı fonksiyonların ne zaman çağrıldığı gözlemlenebilir. Bu örneği çalıştırdığınızda ekranda şunları göreceksiniz: one constructor: 10 disp: 10 destructor: 20 two Geçici sınıf nesnesi için yıkıcı fonksiyonun onun yaratıldığı ifadenin sonunda çağrıldığında dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a) : m_a(a) { cout << "constructor: " << m_a << endl; } ~Sample() { cout << "destructor: " << m_a << endl; } void disp() const { cout << "disp: " << m_a << endl; } int m_a; }; void foo(Sample s) { s.disp(); s.m_a = 20; } int main() { cout << "one" << endl; foo(Sample(10)); cout << "two" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aynı ifade içerisinde birden fazla geçici sınıf nesnesi yaratıldığında onların yapıcı ve yıkıcı fonksiyonlarının ters sırada çağrılacağını anımsayınız. Aşağıda bu duruma bir örnek verilmiştir. Bu örneği çalıştırdığınızda ekranda şunları göeceksiniz: one constructor: 10 constructor: 20 s: 20 destructor: 30 destructor: 10 two --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a) : m_a(a) { cout << "constructor: " << m_a << endl; } ~Sample() { cout << "destructor: " << m_a << endl; } void disp() const { cout << "disp: " << m_a << endl; } void foo(Sample s) { cout << "s: " << s.m_a << endl; s.m_a = 30; } int m_a; }; int main() { cout << "one" << endl; Sample(10).foo(Sample(20)); cout << "two" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- T bir sınıf belirtmek üzere T(...) biçiminde yaratılan geçici nesneler sağ taraf değeri (prvalue) belirtmektedir. Dolayısıyla biz onu ancak const bir sol taraf değeri referansına ya da bir sağ taraf değeri referansına bind edebiliriz. Örneğin: Sample &r = Sample(10); // error! geçici nesne prvalue fakat referans const değil! const Sample &k = Sample(10); // geçerli, geçici nesne prvalue ve referans const Sample &&m = Sample(10); // geçerli, geçici nesne prvalue ancak referns rvalue referans --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıda const bir sol taraf değeri referansaına bind edilen bir geçici nesne için yıkıcı fonksiyonunun bu referansın ömrü bittiğinde (yani referans bellekten yok edileceği) çağrılacağını gösteren bir örnek verilmiştir. Bu örnekte ekranda şunları göreceksiniz: one constructor: 10 two destructor: 10 three --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a) : m_a(a) { cout << "constructor: " << m_a << endl; } ~Sample() { cout << "destructor: " << m_a << endl; } int m_a; }; int main() { cout << "one" << endl; { const Sample &r = Sample(10); cout << "two" << endl; } cout << "three" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'a çeşili öğeler eklendikçe standratlarda daha önce olmayan terimler uydurulmuştur. Örneğin C++17 ile birlikte "temporary materialization conversion" denilen bir terim uydurulmuştur. Bu terim "bir sağ taraf değerinden hareketle bir geçici nesnenin oluşturulmasını" belirtmektedir. Örneğin C++17 ve sonrasında bir geçici nesne const bir referansa ya da bir sağ taraf değeri referansına bind edildiğinde "temporary materialization conversion" oluşmaktadır. Yani bu geçici nesne artık bir sağ taraf değeri olmaktan çıkıp bir nesne haline gelmektedir. Bu standartlarda "temporary materialization conversion" işlemi sonucunda elde edilen nesnenin bir "xvalue" olduğu belirtilmektedir. Anımsanacağı gibi C++11 ve sonrasında "xvalue" hayatını kaybetmek üzere olan nesneleri belirtmekteydi. "xvalue" duruma göre hem bir sol taraf değeri gibi hem de bir sağ taraf değeri gibi ele alınmaktaydı. Pekiyi C++17'de neden "temporary materialization conversion" terimi uydurulmuştur? İşte C++17 ile birlikte izleyen paragraflarda ele alacak olduğmuz eskiden zorunlu olmayan bazı "copy elision" durumları zorunlu (mandatory) hale getirilmiştir. Dolayısıyla artık C++17 ile birlikte geçici nesne oluşturan ifadeler aslında gerçek anlamda geçici nesne oluşturmamaktadır. C++17 ile birlikte bu geçici nesne oluşturan ifadelerin gerçekten geçici nesne oluşturması durumu "temporary materialization conversion" terimi ile açıklanmıştır. Buna ilişkin daha sonra anlamlandırabileceğiniz bir örnek verelim: Sample s = Sample(); // C++17 ve sonrasında "temporary materialization conversion" uygulanmıyor const Sample &r = Sample(); // "temporary materialization conversion" uygulanıyor --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- T bir sınıf belirtmek üzere T(...) ifadesi bir prvalue olduğuna göre onun adresini alamayız. Örneğin: void foo(const Sample *ps) { //... } //... foo(&Sample()); // geçersiz! Ancak yukarıda da belirttiğimiz gibi aynı işlem referans yoluyla yapılabilmektedir. Örneğin: void foo(const Sample &r) { //... } //... foo(Sample()); // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıflar türünden geçici nesnelerin T(...) sentaksıyla yaratılması bazı işlemleri programcı açısından kolaylaştırmaktadır. Örneğin nokta kavramo Point sınıfıyla temsil edilmiş olsun. İki nokta arasında doğru çizen aşağıdaki gibi bir draw_line fonksiyonunun bulunduğunu düşünelim: void draw_line(const Point &pt1, const Point &pt2); Eğer geçici sınıf nesnesi yaratma diye bir şey olmasaydı biz fonksiyonu şöyle çağırmak zorunda kalırdık: Point pt1{100, 100}, pt2{200, 200}; drawline(pt1, pt2); Halbuki geçici nesne yaratabilme özelliğini kullanarak fonksiyonu daha pratik bir biçimde şöyle çağırabilmekteyiz: drawline(Point{100, 100}, Point{200, 200}); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında "fonksiyonel tarzda tür dönüştürmesi" ile sınıflar türünden geçici nesne yaratılması benzer sentaksla yapılmaktadır. Örneğin Point(10, 20) gibi bir ifadeyle Point sınıfı türünden bir geçici nesne yaratıldığı gibi int(10) gibi bir ifadeyle ibt türünden de geçici bir nesne yaratılabilmektedir. Hatta C++'ta temel türler söz konusu olduğunda parantezin içi de boş bırakılabilmektedir. Dolayısıyla int() gibi bir ifade içi 0 olan geçici bir int nesnenin yaratılacağı anlamına gelmektedir. Bu sayede şablon (template) işlemlerinde temel türleri de kapsayabilecek bir genellik sağlanabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 45. Ders 29/01/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta bazı durumlarda kopya yapıcı fonksiyonun (copy conctructor) derleyici tarafından çağrılması elimine edilebilmektedir. Yani bu durumlarda derleyici kopya yapıcı fonksiyonları çağırmayabilmektedir. Bu duruma "kopya yapıcı fonksiyonun çağrılmasının elimine edilmesi (copy elision)" denilmektedir.(Bu terimin İngilizce karşılığı "copy elision" biçimindedir. Buradaki "elision" sözcüğü "çıkarmak, atmak, atlamak" gibi anlamlara gelmektedir. Biz kursumuzda bu durumu Türkçe "kopya yapıcı fonksiyonun çağrılmasının elimine edilmesi" ya da doğrudan İngilizce "copy elision" biçiminde belirteceğiz.) C++17'ye kadar kopya yapıcı fonksiyonun çağrılmasının elimine edilmesi "iste bağlı (optional)" durumdaydı. Yani derleyici bu eleminasyonu isteğe bağlı olarak yapabilir ya da yapmayabilirdi. Ancak C++17 ile birlikte bazı durumlardaki eleminasyon artık "zorunlu (mandatory)" hale getirilmiştir. Biz burada C++17 ve sonrasındaki nihai durumdan bahsedeceğiz. C++17 ile birlikte bir sınıf türünden nesne aynı sınıf türünden "saf sağ taraf değerine (prvalue)" ilişkin bir nesne ile ilkdeğer verilerek yaratıldığında (yani tipik olarak bir geçici nesne ile ilkdeğer verilerek yaratıldığında) yeni yaratılan nesne için kopya yapıcı fonksiyonu elimine edilmektedir. Bu durumda doğrudan yeni nesneye saf sağ taraf değeri için çağrılacak yapıcı fonksiyonla ilkdeğer verilmektedir. Bu durumu özetle şöyle ifade edebiliriz: Biz bir sınıf türünden nesneyi aynı sınıf türünden geçici bir nesne ile ilkdeğer vererek tanımladığımızda aslında o nesneyi sanki geçici nesnedeki yapıcı fonksiyon çağrılacak biçimde tanımlamış oluruz. Örneğin: Sample s = Sample(a, b, c); Burada normalde ilk bakıldığında önce geçici nesnenin uygun yapıcı fonksiyon ile yaratılacağıve sonra da bunun kopya yapıcı fonksiyonu ile s'e kopyalanacağı sanılmaktadır. Halbuki C++17 ve sonrasında artık bu geçici nesne hiç yaratılmamakta ve kopya yapıcı fonksiyonu da hiç çağrılmamaktadır. Doğrudan s nesnesi sanki geçici nesnenin yaratımında belirtilen yapıcı fonksiyon ile tanımlanmış gibi bir etki oluşmaktadır. Yani C++17 ve sonrasında yukarıdaki ilkdeğer verme tamamen aşağıdakiyle eşdeğer biçimdedir: Sample s(a, b, c); Tabii buradaki ilkdeğer vermenin '=' ile yapılması (copy initialiation) gerekmemektedir. Aşağıdaki tanımalama da (direct initialization) tamamen yukarıdakilerle eşdeğerdir: Sample s{Sample(a, b, c)}; İşte C++17 öncesinde böyle bir eliminasyonun yapılıp yapılmayacağı derleyicilerin isteğine bırakılmıştır. Halbuki C++17 ile birlikte artık zorunla hale getirilmiştir. Tabii bir geçici sınıf nesnesinin başka bir geçici sınıf nesnesi ile ilkdeğer verilerek yaratıldığı durumda da bu eliminasyonlar yapılmaktadır. Örneğin: Sample s(Sample(Sample(Sample(a, b, c)))); Burada tüm geçici nesnelerin yaratılması ve kopya yapıcı fonksiyonların çalıştırılması elimine edilecektir. Yani bu işlem yine aşağıdakine eşdeğerdir: Sample s(a, b, c); Fonksiyonun geri dönüş değeri bir sınıf türündense ve return ifadesinde aynı sınıf türünden saf sağ taraf değeri (prvalue) varsa (yani tipik olarak aynı türnden geçici bir sınıf nesnesi ile return işlemi yapılıyorsa) bu durumda geri dönüş değeri için oluşturulacak nesne için yine kopya yapıcı fonksiyonu elimine edilecektir. Örneğin: Sample foo() { //... return Sample(a, b, c); } Burada bu fonksiyon çağrıldığında akış return deyimine geldiği zaman önce geçici nesne oluşturulup sonra kopya yapıcı fonksiyonu ile geri dönüş değeri için oluşturulan geçici nesneye kopyalama yapılmayacaktır. Zaten geri dönüş değeri için yaratılacak nesne doğrudan return deyiminde belirtilen yapıcı fonksiyon ile ilkdeğer alacaktır. Tabii fonksiyonların geri dönüş değerleri referans değilse çağrı ifadesinin de saf sağ taraf değeir (prvalue) belirtitğini anımsayınız. Bu biçimde kopya yapıcı fonksiyonun elimine edilmesine C++ programcıları arasında "Unnamed Return Value Optimzation (URVO)" da denilmektedir. Bu durumda örneğin: Sample s(foo()); Böyle bir tanımlama aşağıdakiyle eşdeğer hale gelecektir: Sample s(a, b, c); Fonksiyonun parametre değişkeni bir sanıf türündense biz de fonksiyonu aynı sınıf türünden bir geçici nesne ile çağırırsak yine "zorunlu eliminasyon (mandatory copy elision)" yapılmaktadır. Örneğin: void foo(Sample s) { cout << s.a() << endl; } //... foo(Sample(100)); Burada önce geçici nesne yaratılıp kopya yapıcı fonksiyonu ile parametre değişkenine aktarım yapılmayacaktır. Doğrudan parametre değişkeni geçici nesnenin yaratılmasında belirtilen yapıcı fonksiyon ile ilkdeğer alacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a) : m_a(a) { cout << "int constructor" << endl; } Sample(const Sample &r) : m_a(r.m_a) { cout << "copy constructor" << endl; } ~Sample() { cout << "destructor" << endl; } int a() const { return m_a; } private: int m_a; }; Sample foo() { cout << "foo" << endl; return Sample(10); } int main() { Sample s = Sample(foo()); // Sample s(10); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Ayrıca tüm C++ verisyonları dahil olmak üzere birkaç durumda da "isteğe bağlı (optional)" biçimde bir "copy elision" yapılabilmektedir. Bunlardan en önemli olanı "NRVO (Named Return Value Optimization)" denilen durumdur. Bir fonksiyonun geri dönüş değeri bir sınıf türündense ve fonksiyonun return ifadesi o sınıf türünden yerel bir sınıf nesnesinin isminden oluşuyorsa bu durumda derleyici bu yerel sınıf nesnesi için yapıcı fonksiyonu çağırıp, geri dönüş değeri için kopya yapıcı fonksiyonu çağırmak yerine doğrudan bu yerel nesneyi zaten geri dönüş değeri ile aktarılacak geçici nesne biçiminde oluşturabilir. Dolayısıyla return işlemi sırasında kopya yapıcı fonksiyonu elimine edilebilir. Örneğin: Sample foo() { //... Sample s(a, b, c); //... return s; } Burada aslında s nesnesi zaten geri dönüş değeri için yaratılacak nesne biçiminde oluşturulabilmektedir. Yani burada derleyici isterse geri dönüş değerine ilişkin nesneyi s gibi yaratabilir. Böylece kopya yapıcı fonksiyonu hiç çağrılmaz. Başka bir deyişle aslında fonksiyonun s yerel değişkeni zaten fonksiyonun geri dönüş değeri için oluşturulan nesnedir. Böylece burada aslında kopya yapıcı fonksiyonu yine hiç çağrılmayabilir. Bu eliminasyonun C++17 sonrasında da "isteğe bağlı (optional)" olduğuna dikkat ediniz. Eğer return ifadesindeki isim parametre değişkenine ilişkinse böyle bir optimizasyon yapılmamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a) : m_a(a) { cout << "int constructor" << endl; } Sample(const Sample &r) : m_a(r.m_a) { cout << "copy constructor" << endl; } ~Sample() { cout << "destructor" << endl; } int a() const { return m_a; } private: int m_a; }; Sample foo() { Sample s(10); cout << "foo" << endl; return s; // burada geri dönüş değeri için kopya yapıcı fonksiyonun çağrılıp çağrılmayacağı derleyicileri yazanların isteğine bırakılmıştır } int main() { Sample s{foo()}; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi C++'ın yeni standartlarında yeni özellekler eklendikçe tutarlı bir anlatım oluşturabilmek için bazı terimler de uydurulmaktadır. Örneğin kopya yapıcı fonksiyonun zorunlu elimine edilmesi (mandatory copy elision) C++17 ile dile eklenince standratlara "yemporary materialization" denilen bir terim de sokulmuştur. Bu teriminin sokulmasının nedeni artık C++17 ve sonrasında geçici nesne yaratır gibi yaptığımızda aslında bir nesne yaratılmayabilceği içindir. Örneğin: Sample s = Sample(x, y, z); Burada aslında C++17 ve sonrasında geçici nesne hiç yaratılmamaktadır. İşte bu durum standartlarda bu geçici nesnenin "materialize" olmaması biçiminde ifade edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf başka bir sınıf türünden veri elemanlarına sahip olabilir. Örneğin Student sınıfı öğrencilerin bilgilerini tutup onlar üzerinde işlem yapan bir sınıf olsun. Öğrencinin ismi String sınıfı türünden bir nesne ile tutulabilir. Öğrencinin doğum tarihi Date isimli bir sınıf nenesi yoluyla tutulabilir: class Student { public: //... private: int m_no; String m_name; Date m_bdate; }; Pekiyi böylesi bir durumda elemanlar için yapıcı fonksiyonlar nerede ve nasıl çağrılmaktadır? --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Elemana sahip sınıf türünden bir nesne yaratıldığında eleman olan sınıf nesneleri o sınıfların kendi yapıcı fonksiyonları ile ilkdeğerlerini alır. Yani C++'ta elemana sahip sınıfın yapıcı fonksiyonları elemana ilişkin sınıfların yapıcı fonksiyonlarını çağırarak elemanlara ilkdeğerlerini vermektedir. Pekiyi elemana sahip sınıfın yapıcı fonksiyonları elemana ilişkin sınıfın hangi yapıcı fonksiyonlarını ne zaman çağırmaktadır. İşte elemana sahip sınıfın yapıcı fonksiyonun elemana ilişkin sınıfın hangi yapıcı fonksiyonunu çağıracağı MIL sentaksında belirlenmektedir. MIL sentaksında elemanın ismi ve parantezlerle argüman listesi belirtilirse o elemanlar için ilgili sınıfların uygun uapıcı fonksiyonları çağrılmaktadır. Yani elemanlar çağrılacak yapıcı fonksiyonları aşağıdaki gibi MIL sentaksında belirtebiliriz: T::T(...) : eleman_ismi(argüman_listesi), eleman_ismi(argüman_listesi), eleman_ismi(argüman_listesi), ... { //... } Burada elemanlar için argüman listesine uygun yapıcı fonksiyonlar çağrılmaktadır. Daha önceden de belirttiğimiz gibi buradaki argüman listesinde global değişkenler, elemana sahip sınıfın yapıcı fonksiyonlarının parametreleri kullanılabilir. Yani buradaki argümanlar sanki ğye fonksiyonun içeriisnde yazılmış gibi isim araması uygulanmaktadır. Örneğin: class Student { public: Student(const char *name, int day, int month, int year); //... private: String m_name; Date m_bdate; }; Student::Student(const char *name, int day, int month, int year) : m_name(name), m_bdate(day, month, year) { //... } //... Student s("Ali Ay", 12, 11, 2002); Burada s nesnesi için Student sınıfının dört parametreli yapıcı fonksiyonu çağrılacaktır. MIL sentaksında m_name ve m_bdate elemanları için hafni yapıcı fonksiyonların çağrılacağı belitilmiştir. Eğer sınıfın başka sınıf türünden veri elemanı MIL sentaksında belirtilmezse o veri elemanı için "default yapıcı fonksiyon" çağrılmaktadır. Tabii elemana ilişkin sınıfın default yapıcı fonksiyonu yoksa ya da erişilemez biçimdeyse bu durum derleme aşamasında error oluşturacaktır. C++'ta her zaman elemana ilişkin sınıfların yapıcı fonksiyonları elemana sahip sınıfların yapıcı fonksiyonlarından daha önce çalıştırılır.Yani önce elemanlar için yapıcı fonksiyonlar çağrılır, sonra programın akışı elemana sahip sınıfın ana bloğundan içeri girer. Derleyiciler genellikle elemana ilişkin sınıf nesneleri için onların yapıcı fonksiyonlarını elemana sahip sınıfın yapıcı fonksiyonlarının ana bloğunun başına yerleştirdikleri gizli bir çağırma kodu yoluyla çağırmaktadır. Daha önceden de belirttiğimiz gibi MIL sentaksındaki sıranın hiçbir önemi yoktur. Her zaman sınıf bildirimindeki sıra dikkate alınmaktadır. Elemanlar için yapıcı fonksiyonlar her zaman bildirimdeki sıraya göre (yukarıdan aşağıya, soldan sağa) çağrılmaktadır. Örneğin: Student::Student(const char *name, int day, int month, int year) : m_bdate(day, month, year), m_name(name) { //... } Burada MIL sentaksında önce m_bdate elemanı sonra m_name elemanı belirtilmiştir. Ancak önce m_name elemanı için sonra m_bdate elemanı için yapıcı fonksiyonlar çalıştırılacaktır. Çünkü sınıf bildiriminde önce m_name sonra m_bdate bildirilmiştir. MIL sentaksında eleman ismi hiç belirtilmemiş olsa bile belirtilmeyen elemanlar için default yapı fonksiyonlar yine bildirimdeki sıraya göre çağrılmaktadır. Örneğin: Student::Student(const char *name, int day, int month, int year) : m_bdate(day, month, year) { //... } Burada m_name elemanı MIL sentaksında belirtilmemiştir. Ancak yine önce m_name için String sınıfının default yapıcı fonksiyonu çağrılır, sonra m_bdate için Date sınıfının yapıcı fonksiyonu çağrılır. Sınıfın temel türlere ilişkin veri elemanlarının bulunduğu durumda biz MIL sentaksında ya da yapıcı fonksiyonun ana bloğunda bunlara değer atamamışsak bunların içerisnde çöğ değerlerin bulunacağını anımsayınız. Tabii temel türler için MIL sentaksında ilkdeğer verilirse bunlar da bildirimdeki sıraya göre işlem görecektir. Örneğin: class Student { public: Student(const char *name, int day, int month, int year, int no); //... private: int m_no; String m_name; Date m_bdate; }; //... Student::Student(const char *name, int day, int month, int year, int no) : m_bdate(day, month, year), m_name(name), m_no(no) { //... } Burada önce m_no veri elemanına değer atanacak, sonra m_name için yapıcı fonksiyon çağrılacak, sonra da m_bdate için yapıcı fonksiyon çağrılacaktır. Bu çağrılardan sonra akış Student sınıfının yapıcı fonksiyonunun içine girecektir. Sınıfın başka sınıf türünden veri elemanlarının bulunduğu durumda bu veri elemanlarına eleman sahip sınıfın yapıcı fonksiyonu içerisinde '=' operatörü ile değer atamaya çalışmak kötü bir tekniktir. (Tabii zaten bir sınıf nesnesine atama yapabilmek için sınıfın "atama operatör fonksiyonu" denilen bir üye fonksiyonunun bulunuyor olması gerekmektedir. Bu konu ileride ele alınacaktır.) Çünkü biz ilgili veri elemanını MIL sentaksında belirtmediğimiz zaman zaten onun için önce default yapıcı fonksiyon çağrılmaktadır. Ona elemana sahip sınıfın yapıcı fonksiyonu içerisinde değer atamak ikinci bir işlem gerektirmektedir. O halde şunları söyleyebiliriz: Elemana sahip sınıfın yapıcı fonksiyonunda elemanlar için her zaman (eğer default yapıcı fonksiyonun çağrılmasını istemiyorsak) MIL sentaksıyla o elemanlar için ilkdeğer vermemiz gerekir. Sınıfın temel türlere ilişkin (int, long double gibi) veri elemanları da MIL sentaksında belirtilebilir. Sınıfın temel türlere ilişkin veri elemanları söz konusu olduğunda o elemanlara MIL sentaksıyla değer atamak ile elemana sahip sınıfın yapıcı fonksiyonun içerisinde değer atamak arasında hiçbir etkinlik farkı yoktur. Standartlar MIL sentaksında belirtilmeyen veri elemanları için "default-initialize" işleminin yapılacağını söylemektedir (11.9.3-9). Anımsanacağı default-initialize sınıf nesneleri için onların default yapıcı fonksiyonlarının çağrılması, temel türler için hiçbir initialize işleminin yapılmaması anlamına geliyordu. Örneğin: #include "string.hpp" #include "date.hpp" using namespace std; using namespace CSD; class Student { public: Student(const char *name, int day, int month, int year, int no); void disp() const; private: String m_name; Date m_bdate; int m_no; }; Student::Student(const char *name, int day, int month, int year, int no) : m_name(name), m_bdate(day, month, year), m_no(no) { } void Student::disp() const { m_name.disp(); m_bdate.disp(); } int main() { Student s("Ahmet Ak", 7, 11, 1999, 123); s.disp(); return 0; } Burada daha önce yazmış olduğumuz String ve Date sınıflarını Student sınıfının veri elemanları için kullandık. Bir Student nesnesi yaratıldığında önce m_name için, sonra m_date için MIL sentaksında belirtilen yapıcı fonksiyonları çağrılacaktır. m_no veri elemanına MIL sentaksında belirtilen değer atanacaktır. Aşağıdaki örnekte Sample sınıfının Mample sınıfı tründen bir veri elemanıo vardır. Örneği çalıştırarak ekrana çıkan yazıları anlamdırınız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Mample { public: Mample(int a); void disp() const; private: int m_a; }; class Sample { public: Sample(int a, int b); void disp() const; private: Mample m_m; int m_b; }; Mample::Mample(int a) : m_a(a) { cout << "Mample(int) constructor" << endl; } void Mample::disp() const { cout << m_a << endl; } Sample::Sample(int a, int b) : m_m(a), m_b(b) { cout << "Sample(int, int) constructor" << endl; } void Sample::disp() const { m_m.disp(); cout << m_b << endl; } int main() { Sample s(10, 20); s.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 46. Ders 31/01/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf için kopya yapıcı fonksiyonu yazmadığımızda derleyicinin yazdığı kopya yapıcı fonksiyonun "memberwise copy" yaptığını belirtmiştik. Aynı zamanda bu "memberwise copy" işleminin ilkdeğer verme gibi MIL sentaksı yoluyla yapıldığını belirtmiştir. Bu durumda elemana sahip sınıf için kopya yapıcı fonksiyonu yazılmamışsa aslında derleyicinin yazdığı kopya yapıcı fonksiyon sınıfın başka sınıf türünden veri elemanlarını kendi sınıflarının kopya yapıcı fonksiyonu yoluyla kopyalayacaktır. Örneğin: #include "string.hpp" #include "date.hpp" using namespace std; using namespace CSD; class Student { public: Student(const char *name, int day, int month, int year, int no); void disp() const; private: String m_name; Date m_bdate; int m_no; }; Student::Student(const char *name, int day, int month, int year, int no) : m_name(name), m_bdate(day, month, year), m_no(no) { } void Student::disp() const { m_name.disp(); m_bdate.disp(); } void foo(Student s) { s.disp(); } //... int main() { Student s("Ahmet Ak", 7, 11, 1999, 123); foo(s); return 0; } Burada main fonksiyonundaki yerel s nesnesi foo fonksiyonun parametre değişkenine atanırken Student sınıfının kopya yapıcı fonksiyonu çalıştırıkacaktır. Örneğimizde Student sınıfı için kopya yapıcı fonksiyonu yazılmamıştır. Derleyicinin yazdığı kopya yapıcı fonksiyonu nesnelerin m_name ve m_bdate veri elemanlarını kendi sınıflarının kopya yapıcı fonksiyonları yoluyla kopyalayacaktır. Dolayısıyla biz String sınıfı için kopya yapıcı fonksiyonu yazdığımızdan dolayı burada bir sorun oluşmayacaktır.Yani buradaki Student sınıfı için bizim kopya yapıcı fonksiyonunu yazmamıza gerek yoktur. Çünkü derleyicinin yazdığı kopya yapıcı fonksiyonu zaten bizim işimizi görektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın başka sınıf türünden veri elemanlarının olduğu durumda sınıf için kopya yapıcı fonksiyon yazılırken veri elemanları için kopya yapıcı fonksiyonların çağrılmasını sağlamak programcının sorumluluğundadır. C++'a yeni başlayanlar sezgisel olarak kopya yapıcı fonksiyonun MIL sentaksında belirtilmeyen veri elemanları için o sınıfların kopya yapıcı fonksiyonlarının çağrılması gerektiğini düşünmektedir. Halbuki kopya yapıcı fonksiyonlarında da veri elemanları için MIL sentaksında belirleme yapılmamışsa diğer yapıcı fonksiyonlarda olduğu gibi veri elemanları için kendi sınıflarının default yapıcı fonksiyonları çeğrılmaktadır. Örneğin yukarıda belirttiğimiz Student sınıfı için kopya yapıcı fonksiyonunu aşağıdaki gibi yazmış olalım: Student::Student(const Student &r) { //... } Burada MIL sentaksında bir belirleme yapılmadığı için sınıfın m_name ve m_bdate veri elemanalrı için default yapıcı fonksiyon çağrılacaktır. Tabii bu da programcının istediği bir şey değildir. Bu sınıf için kopya yapıcı fonksiyonun şöyle yazılması gerekirdi: Student::Student(const Student &r) : m_name(r.m_name), m_bdate(r.m_bdate), m_no(r.m_no) { //... } Derleyicinin kendisini yazdığı kopya yapıcı fonksiyonun zaten bu biçimde bir MIL sentaksı kullandığına dikkat ediniz. Student sınıfı için aşağıdaki gibi kopya yapıcı fonksiyon yazılmamlıdır: Student::Student(const Student &r) { m_name = r.m_name; m_bdate = r.m_bdate; m_no = r.m_no; } Daha önceden de belirttiğimiz gibi bu durumda önce veri elemanları için default yapıcı fonksiyon çağrılıp sonra atama yapılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önceden C++'ta her zaman yapıcı fonksiyonlarla yıkıcı fonksiyonların ters sırada çalıştırıldığını söylemiştik. İşte sınıfın başka sınıf türünden veri elemanlarına sahip olduğu durumda da bu ters sırada çağrılma kuralı geçerlidir. Yani elemana sahip sınıf türünden bir nesne yok edilirken "önce elemana sahip" sınıf için yıkıcı fonksiyon çalıştırılır, sonra ters sırada elemanlara ilişkin sınıfların yıkıcı fonksiyonları çalıştırılır". Bu da yapıcı fonksiyonların çalıştırılma sırasının ters sırasıdır. Örneğin: #include "string.hpp" #include "date.hpp" using namespace std; using namespace CSD; class Student { public: Student(const char *name, int day, int month, int year, int no); Student(const Student &r); ~Student(); void disp() const; private: String m_name; Date m_bdate; int m_no; }; Student::Student(const char *name, int day, int month, int year, int no) : m_name(name), m_bdate(day, month, year), m_no(no) { } Student::Student(const Student &r) { m_name = r.m_name; m_bdate = r.m_bdate; m_no = r.m_no; } Student::~Student() { //... } void Student::disp() const { m_name.disp(); m_bdate.disp(); } int main() { Student s("Ahmet Ak", 7, 11, 1999, 123); s.disp(); return 0; } Burada s nesnesi için çağrılacak yıkıcı fonksiyon aşağıdaki gibi yazılmıştır: Student::~Student() { //... } Bu s nesnesi yok edilirken önce bu yıkıcı fonksiyon çalıştırılıp sonra (ters sırada) sırasıyla m_bdate ve m_name veri elemanları için yıkıcı fonksiyonlar çalıştırılacaktır. Tipik olarak derleyiciler bu işlemi aslında elemana sahip sınıfın yıkıcı fonksiyonlarının ana bloğunun sonuna yerleştirdikleri gizli bir çağırma kodu yoluyla gerçekleştirmektedir. Yani sembolik olarak derleyicinin yukarıdaki yıkıcı fonksiyon için ürettiği kod aşağıdaki gibi olacaktır: Student::~Student() { //... m_bdate.~Date(); // derleyicinin yerleştirdiği gizli çağırma kodu m_name.~String(); // derleyicinin yerleştirdiği gizli çağırma kodu } Elemana sahip sınıf için biz yıkıcı fonksiyonu yazmadığımız zaman derleyicinin yazacağı içi boş yıkıcı fonksiyon yine veri elemanları için ters sırada yıkıcı fonksiyonları çağıracaktır. Yani yukarıdaki örnekte biz Student sınıfı için yıkıcı fonksiyonu yazmamış olsak bile sınıfın m_name ve m_bdate veri elemanı için String ve Date sınıflarının yıkıcı fonksiyonları çalıştırılacaktır. Aşağıda elemana ilişkin ve sahip sınıfların yapıcı ve yıkıcı fonksiyonlarının ters sırada çağrıldığı gösterilmektedir. Programı çalıştırıp ekrana çıkan yazıları inceleyiniz. Ekrana çıkan yazılar şöyle olacaktır: Mample(int) constructor Sample(int, int) constructor 10 20 Sample destructor Mample destructor -------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Mample { public: Mample(int a); ~Mample(); void disp() const; private: int m_a; }; class Sample { public: Sample(int a, int b); ~Sample(); void disp() const; private: Mample m_m; int m_b; }; Mample::Mample(int a) : m_a(a) { cout << "Mample(int) constructor" << endl; } Mample::~Mample() { cout << "Mample destructor" << endl; } void Mample::disp() const { cout << m_a << endl; } Sample::Sample(int a, int b) : m_m(a), m_b(b) { cout << "Sample(int, int) constructor" << endl; } Sample::~Sample() { cout << "Sample destructor" << endl; } void Sample::disp() const { m_m.disp(); cout << m_b << endl; } int main() { Sample s(10, 20); s.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standartlarda özel üye fonksiyonların bazı durumlarda "trivial" olduğu belirtilmiştir. Burada "trival" sözcüğü Türkçe "önemsiz, ihmal edilebilir" gibi anlamlara gelmektedir. Standartlara göre bir yapıcı fonksiyon "trivial" ise aslında o yapıcı fonksiyon hiçbir şey yapmamaktadır. Dolayısıyla derleyici tarafından optimizasyon amacıyla aslında hiç çağrılmayabilir. Benzer biçimde bir yıkıcı fonksiyon "trival" ise bu yıkıcı fonksiyon da aslında bir şey yapmamaktadır. Dolayısıyla optimizasyon amacıyla derleyici tarafından çağrılmayabilir. Yapıcı ve yıkıcı fonksiyonların "trivial" olması için onların "user provided" olmaması gerekmektedir. Programcının kendisinin yazdığı yapıcı ve yıkıcı fonksiyonlar için boş olasalar bile "trivial" olamazlar. Standartlarda derleyici tarafından yazılan yapıcı ve yıkıcı fonksiyonların hangi durumlarda "trivial" olduğu maddeler halinde açıklanmıştır. Tabii yapıcı ve yıkıcı fonksiyonlar programcı tarafından inline olarak yazılmışsa ve gerçekten onların çağrılmasıyla bir yan etki oluşmayacaksa derleyici onları yine çağırmayabilir (yani boş bir inline açım yapabilir). ---------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın başka sınıf türünden veri elemanlarını get etmek için kullanılan getter fonksiyonları kopya yapıcı fonksiyonun çalıştırılmasına yol açabilmektedir. Örneğin: using namespace std; using namespace CSD; class Student { public: Student(const char *name, int day, int month, int year, int no); void disp() const; String name() const { return m_name; } Date bdate() const { return m_bdate; } private: String m_name; Date m_bdate; int m_no; }; Student::Student(const char *name, int day, int month, int year, int no) : m_name(name), m_bdate(day, month, year), m_no(no) {} void Student::disp() const { m_name.disp(); m_bdate.disp(); } Burada nesnenin içerisindeki m_name veri elemanını get etmek için name isimli üye fonksiyon, m_bdate veri elemanını get etmek için ise bdate isimli üye fonksiyon sınıfa eklenmiştir. Ancak bu durumda biz üye fonksiyonları çağırdığımızda fonksiyonların geri dönüş değerleri sınıf türünden olduğu için ilgili sınıfların kopya yapıcı fonksiyonları çalıştırılacaktır. Örneğin: Student s("Ahmet Ak", 7, 11, 1999, 123); s.name().disp(); s.bdate().disp(); Burada s.name() çağrısı ile önce geçici bir String nesnesi oluşturulup bu nesne üzerinde disp üye fonksiyonu çağrılmıştır. Bu tür durumlarda kopya yapıcı fonksiyonun çağrılmasını engellemek için veri elemanının referansıyla geri dönen getter fonksiyonlar oluşturulabilir. Örneğin: using namespace std; using namespace CSD; class Student { public: Student(const char *name, int day, int month, int year, int no); void disp() const; const String &name() const { return m_name; } const Date &bdate() const { return m_bdate; } private: String m_name; Date m_bdate; int m_no; }; Buradaki name ve bdate getter fonksiyonları const üye fonksiyonlar olduğu için fonksiyonun geri dönüş değerine ilişkin referansların da const olması gerekmektedir. (const üte fonksiyonlar içerisinde sınıfın veri elemanları const veri elemanları gibi ele alındığını anımsayınoz.) Bu durumda artık name ve bdate getter fonksiyonlar doğrudan veri elemanlarının adreslerini verdiği için kopya yapıcı fonksiyonlar çalıştırılmayacaktır. getter fonksiyonların yukarıdaki biçimde const referans yapılması gereksiz kopya yapıcı fonksiyonların çağrılmasını engelliyor olsa da nesne yönelimli programlama tekniğindeki "veri elemanlarının gizlenmesi (data hiding)" prensibini biraz bozmaktadır. Çünkü ileride sınıfın bu veri elemanlarının tür bakımından değiştirilmesi mümkün olamayacaktır. O halde programcı eğer sınıfın ilgili veri elemanlarını tür bakımından değiştirmeyecekse bu tekniği kullanabilir. Yani sınıfın başka sınıf türünden veri elemanlarını get etmek için yazılan getter fonksiyonların nesnenin değeriyle mi referansıyla mı geri döndürüleceği programcının isteğine ve gerekçelerine bağlı olarak değişebilmektedir. Örneğin Qt kütüphanesindeki sınıfların getter fonksiyonlarında duruma göre her iki yöntem de kullanılmaktadır. Şimdi "sınıfın başka sının türünden veri elemanlarını referans yoluyla get etmek ve set etmek yerine onları doğrudan public bölümde bulundurmak arasında ne fark var" biçiminde bir soru aklınıza gelebilir. Evet aslında bu ikisi arasında büyük bir farklılık olduğu söylenemez. Ama eğer biz sınıfın başka sınıf türünen veri elemanlarınını referans yoluyla get ve set edersek onları bütünsel olarak alıp set edebiliriz. Onların veri elemanlarını doğrudan ya da dolaylı bir biçimde ara noktalarda değiştiremeyiz. Aşağıda Qt kütüpanesindeki QWidget sınıfında buunan bazı getter fonksiyonları örnek olarka veriyoruz: QIcon windowIcon() const; void seetWindowIcon(const QIcon &icon); const QFont &font() const; void setFont(const QFont &font); const QPalette &palette() const; void setPalette(const QPalette &palette); QString windowTitle() const; void setWindowTitle(const QString &str); QString toolTip() const; void setToolTip(const QString &str); const QRect &geometry() const; void setGeometry(const QRect &rect); QSize size() const void resize(const QSize &size); Pekiyi burada tasarımcı neden bazı getter fonksiyonlarda const referans ile gerş dönerken bazılarında nesnenin değeriyle geri dönmüştür? İşte bunun tasarımsal bazı nedenleri vardır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 47. Ders 05/02/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- GUI framework'lerin kütüphanelerin pencere ve çizim işlemleri için genellikle Point, Size ve Rect gibi sınıflar bulundurulmaktadır. Point sınıfları bir pixel'in koordinatını Size sınıfı ise "genişlik ve yükseklik kavramını" temsil etmektedir. Rect sınıfları "bir dikdörtgensel bölgenin" tutulması ve onun üzerinde bazı faydalı işlemlerin yapılması için bulundurulmaktadır. Örneğin bir C++ GUI kütüphanesi olan Qt Framework'ünde bu sınıflar QPoint, QSize ve QRect isimimleriyle bulunmaktadır. Microsoft C++ GUI framework'ü olan MFC'de ise bu sınıflar CPoint, CSize ve CRect isimleriyle bulunmaktadır. .NET Forms kütüphanesinde aynı sınıflar Point, Size ve Rectangle ismiyle bulunurlar. Point gibi bir sınıfın x ve y değerlerini tutan iki int türden veri elemanın bulunması yeterlidir. Sınıfın yapıcı fonksiyonu bizden bu değerleeri alıp veri elemanlarına yerleştirir. Yapıcı fonksiyonları constexpr yapmak gerektiğinde sabit ifadesi oluşturmak için kullanılabilir. Ancak pek çok çok framework zaten constexpr fonksiyon öncesinde yazıldığı için böyle bir işlevselliği desteklememktedir. Size sınıfı da benzer biçimde genişlik ve yüksekliğe ilişkin iki veri veri elemanına sahip olabilir. Point sınıfı gibi Size sınıfı da bizden genişlik ve yüksekliği alıp sınıfgın veri elemanlarında saklayabilir. Biz aşağıdaki örnekte öğretici olsun diye Rect sınıfının veri elemanlarını Point ve Size türünden aldık. Rect sınıfı koordinatları "sol üst köşe" ve "genişlik-yükseklik" biçiminde tutmaktadır. Örneğmizde rect sınıfına faydalı bazı üye fonksiyonlar da ekledik. Yukarıda sözünü ettiğimiz GUI framework'lerin kütüphanelerinde de genellikle bizim sınıfa eklediğimiz üye fonksiyonların benzerleri bulunmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // point.hpp #ifndef POINT_HPP_ #define POINT_HPP_ namespace CSD { class Point { public: Point() = default; constexpr Point(int x, int y) : m_x(x), m_y(y) {} constexpr int x() const { return m_x; } constexpr int y() const { return m_y; } void disp() const; void move(int x, int y) { m_x += x; m_y += y; } void move(const Point &pt) { m_x += pt.m_x; m_y += pt.m_y; } constexpr Point add(int x, int y) const; constexpr Point add(const Point &pt) const; private: int m_x, m_y; }; constexpr Point Point::add(int x, int y) const { return Point(m_x + x, m_y + y); } constexpr Point Point::add(const Point &pt) const { return Point(m_x + pt.m_x, m_y + pt.m_y); } } #endif // point.cpp #include #include "point.hpp" using namespace std; namespace CSD { void Point::disp() const { cout << '(' << m_x << ',' << m_y << ')' << endl; } } // size.hpp #ifndef SIZE_H_ #define SIZE_H_ namespace CSD { class Size { public: Size() = default; constexpr Size(int width, int height) : m_width(width), m_height(height) {} constexpr int width() const { return m_width; } constexpr int height() const { return m_height; } void disp() const; private: int m_width; int m_height; }; } #endif // size.cpp #include #include "size.hpp" using namespace std; namespace CSD { void Size::disp() const { cout << "width: " << m_width << ", height: " << m_height; } } // rect.hpp #ifndef RECT_HPP_ #define RECT_HPP_ #include "point.hpp" #include "size.hpp" namespace CSD { class Rect { public: constexpr Rect(int x, int y, int width, int height) : m_pos(x, y), m_size(width, height) {} constexpr Rect(const Point &pos, const Size &size) : m_pos(pos), m_size(size) {} constexpr Point pos() const { return m_pos; } constexpr Size size() const { return m_size; } constexpr int x() const { return m_pos.x(); } constexpr int y() const { return m_pos.x(); } constexpr int width() const { return m_size.width(); } constexpr int height() const { return m_size.height(); } constexpr Point bottom_right() const { return m_pos.add(m_size.width(), m_size.height()); } bool contains(const Point &pt) const; bool contains(int x, int y) const; bool contains(const Rect &rect) const; void move(int x, int y) { m_pos.move(x, y); } void move(const Point &pt) { m_pos.move(pt); } private: Point m_pos; Size m_size; }; } #endif // rect.cpp #include #include "rect.hpp" using namespace std; namespace CSD { bool Rect::contains(const Point &pt) const { return pt.x() > m_pos.x() && pt.x() < m_pos.x() + m_size.width() && pt.y() > m_pos.y() && pt.y() < m_pos.y() + m_size.height(); } bool Rect::contains(int x, int y) const { return x > m_pos.x() && x < m_pos.x() + m_size.width() && y > m_pos.y() && y < m_pos.y() + m_size.height(); } bool Rect::contains(const Rect &rect) const { return contains(rect.m_pos) && contains(bottom_right()); } } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın başka sınıf türünden veri elemanlarına sahip olması (ileride buna "içerme ilişkisi (composition)" diyeceğiz) aslında dolaylı bir biçimde gösterici yoluyla da yapılabilir. Bunun için elemana sahip sınıf elemana ilişkin sınıf türünden nesnenin kendisini değil o türden bir gösterici tutar. Sonra elemana sahip sınıfın yapıcı fonksiyonunda o gösterici için new operatörüyle tahsisat yapılır. Tabii elemana sahip sınıfın yıkıcı fonksiyonunda da dinamik tahsis edilmiş nesne delete operatörü ile yok edilmelidir. Örneğin: class A { //... }; class B { B(); ~B(); //... private: A *m_a; }; B::B() { m_a = new A(); //... } B::~B() { delete m_a; } Pekiyi bu biçimde bir içerme ilişkisi ile elemanın kendisinin tutulması arasında ne fark vardır? Yani örneğin yukarıdaki işlevsillikle aşağıdakinin arasında ne fark vardır? class A { //... }; class B { B(); ~B(); //... private: A m_a; }; B::B() : m_a() { //... } Biz birinci biçimde elemana sahip sınıf içersinde eleman olan sınıfa ilişkin bir gösterici tuttuk. İkinci biçimde elemana sahip sınıf içerisinde bizzat elemanın kendisini tuttuk. Bu iki biçim arasında semantik farklılıklar olsa da aslında mantıksal bakımdan önemli bir farklılık yoktur. Neticide her iki durumda da sınıf başka sınıf türünden elemana sahip olmuştur. Tabii sınıfın başka sınıf türünden gösterici veri elemanı olduğu durumda new yapılmadıktan sonra eleman olan sınıf için bir nesnenin yaratılmayacağına dikkat ediniz. Örneğin: class B { B(); ~B(); //... private: A *m_a; }; B::B() { //... } Burada m_a göstericisinin gösterdiği yer için otomatik olark bir nesne yaratılmamaktadır. Dolayısıyla elemana sahip sınıfın yapıcı fonksiyonunda bu örnekte A nesnesi için bir yapıcı fonksiyon da çağrılmayacaktır. A nesnesi için yapıcı fonksiyon o nesne new ile yaratılırken çağrılacaktır. Örneğin: B::B() { m_a = new A(); // bu noktata A nesnesi yaratılır ve yapıcı fonksiyon çağrılır } Burada şu noktaya dikkat ediniz: B sınıfının veri elemanı olarak A sınıfı türünden bir nesnenin olması ile A sınıfı türünden bir göstericinin olması NYPT bakımından benzer anlama gelse de C++ semantiği bakımından farklı anlamlara gelmektedir. Göstericilerin türü ne olursa olsun onlar sınıf nesnesi değildir. Temel türlere ilişkin nesnelerdir. Qt gibi bazı framework'lerde Widget nesneleri sınıfın veri elemanı olarak değil gösterici veri elemanı olarak bulundurulmaktadır. Dolayısıyla bunların yaratımları eleman sahip Widget sınıflarının yapıcı fonksiyonları içerisinde new operatörüyle yapılmaktadır. Bu Qt'nin tasarımına ilişkin bir özelliktir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 48. Ders 07/02/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesinin aynı sınıf türünden bir nesne ile ilkdeğer verilerek tanımlanması durumunda tanımlanan nesne için kopya yapıcı fonksiyonun çağrıldığını gördük. eğer biz kopya yapıcı fonksiyonu yazmazsak derleyici onu "sınıfın karşılıklı veri elemanlarını ilkdeğer veriyormuş gibi atayacak biçimde (memberwise copy)" yazıyordu. Özellikle sınıfın bir gösterici veri elemanı varken bizim sınıf için kopya yapıcı fonksiyonu azmamız gerekiyordu. Pekiyi aynı sınıf türünden iki sınıf nesnesi birbirine atandığunda ne olacaktır? Örneğin: class String { //... private: char *m_str; size_t m_size; }; String a{"Ali Ak"}; String b; b = a; Anımsanacağı gibi C'de aynı türden iki yapı nesnesi birbirine atandığında yapının karşılıklı elemanları birbirine atanıyordu. C++'ta sınıflar veri elemanlarının organizasyonu bakımından C'deki yapılara benzerdir. Pekiyi C++'ta aynı türden iki sınıf nesnesi birbirine atanırsa ne olacaktır? İşte C++'ta aynı türden iki sınıf nesnesini birbirine atadığımızda sınıfın ismine "kopya atama operatör fonksiyonu (copy assignment operator)" denilen bir üye fonksiyonu çağrılmaktadır. Atama işlemi bu üye fonksiyon tarafından yapılmaktadır. Aslında operatör fonksiyonları ayrı bir bölüme ele alacağımız geniş ve ayrıntılı bir konudur. Kopya atama operatör fonksiyonunu da biz o bölğmde detaylı olarak ele alacağız. Ancak ne olursa olsun bu bölümde de kopya yapıcı fonksiyonu ele aldıktan sonra kopya atama operatör fonksiyonu hakkında temel bazı bilgiler vereceğiz. Konu detaylı olduğu için detaylarını ileride ele alacağız. C++ standartlarına göre k bir sınıf nesnesi olmak üzere: k = s; işleminin eşdeğeri şöyledir: k.operator =(s); Görüldüğü gibi k nesnesi ile sınıfın "operator =" isimli bir üye fonksiyonu çağrılmıştır. Atanmak istenen değer de bu üye fonsiyona parametre olarak geçirilmiştir. Operatör fonksiyonlarının isimleri "operator" anahtar sözcüğü ile bir operatör sembolünden oluşmaktadır. Operatör fonksiyonlarının bazı kısıtlar dışında diğer bakımlardan diğer üye fonksiyonlardna bir farkı yoktur. Eğer programcı sınıfı için kopya atama operatör fonksiyonunu yazmazsa bu fonksiyon derleyici tarafından yazılır. Derleyicinin yazdığı kopya atama operatör fonksiyonu da sınıfın karşılıklı veri elemanlarını birbirine atar (memwise copy). Yani biz bir sınıf için kopya atama operatör fonksiyonunu yazmayabiliriz. Bu durumda aynı türden iki sınıf nesnesi birbirine atandığında tıpkı C'de olduğu gibi sınıfın karşılıklı veri elemanları biribirine atanacaktır. Pekiyi mademki derleyici bizim için eğer biz yazmazsak kopya atama operatör fonksiyonunu kendisi yazıyor, bu durumda bu operatör fonksiyonunu yazmamız gereken yerler var mıdır? İşte "bir sınıf için ne zaman kopya yapıcı fonksiyonu yazılması gerekse o sınıf için aynı zamanda kopya atama operatör fonksiyonun da aynı gerekçelerle" yazılması gerekmektedir. Yani örneğin sınıfın gösterici veri elemanları olduğu durumda bizim o sınıf için hem kopya yapıcı fonksiyonu hem de kopya atama operatör fonksiyonunu "içerik kopyalaması yapacak biçimde" yazmamız gerekir. Daha önce yazmış olduğumuz String sınıfınının aşağıdaki ver elamanlarına sahip olduğunu varsayalım: class String { //... private: char *m_str; size_t m_size; }; Eğer bu sınıf için biz kopya atama operatör fonksiyonunu yazmazsak önemli sorunlar ortaya çıkar. Örneğin: String s{"ankara"}; { String k{"izmir"}; k = s; k.idsp(); } s.disp(); Burada k = s işleminde iki sorun oluşacaktır. Birincisi s'in m_str veri elemanı k'nın m_str elemanına atanmasıyla k'nın m_str elemanının daha önce gösterdiği dinamik alanın boşaltılma olanağı ortadana kalkacaktır. Dolayısıyla bu durum bir "bellek sızıntısına (memory leak)" yol açacaktır. İkinci sorun daha önce kopya yapıcı fonksiyonda ele aldığımız sorundur. Yani iki nesnenin m_str veri elemanları aynı nesneyi gösterir. Bu durumda birisi çağrılan yıkıcı fonksiyon diğerinin kullandığı alanı da free hale getirecektir. Bu da kodda ileride tanımsız davranış oluşturacaktır. Tabii bir sınıf için kopya yapaıcı fonksiyonun yazılmasına gerek yoksa kopya atama operatör fonksiyonunun da yazılmasına gerek yoktur. Örneğin: class Complex { //... private: double m_real; double m_imag; }; Complex x{3, 2}; Complex y; y = z; Böylesi bir sınıf için kopya yapıcı fonksiyonun ve kopya atama operatör fonksiyonunun programcı tarafındna yazılmasına gerek yoktur. Zaten derleyicinin kendisinin yazacağı kopya yapıcı fonksiyon ve kopya atama operatör fonksiyonu istenilen şeyi yapacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın aşağıdaki parametrik yapılara sahip olan "operator =" isimli operatör fonksiyonlarına "kopya atama operatör fonksiyonlzrı (copy assignment operator)" denilmektedir (sınıfın isminin T olduğunu varsayıyoruz): T T & const T & volatile T & const volatile T & Kopya atama operatör fonkdiyonu tipik olarak const T & parametreli biçimde yazılmaktadır. Zaten bir sınıf için biz kopya atama operatör fonksiyonunu yazmamışsak const T & parametreli kopya atama operatör fonksiyonu yazılmaktadır. (Bu kuralın bazı ayrıntıları vardır.) Kopya atama operatör fonksiyonunun geri dönüş değeri herhangi bir türden olabilirse de "onun kendisi sınıfı türünden bir referansa geri dönmesi" en normal durumdur. Bunun ayrıntıları operatör fonksiyonlarının anlatıldığı bölümde ele alınacaktır. Kopya atama operatör fonksiyonunun yazılmasında dikkat edilecek bazı noktalar vardır. Örneğin programcılar geneillikle fonksiyonun başında aşağıdaki gibi bir kontrol uygularlar: T &T::operator =(const T &r) { if (this == &r) return *this; //... } Bu kontrol bir nesnenin kendisinin kendisine atanması durumunda oluşabilecek anomaliyi ortadan kaldırmak için yapılmaktadır. Kopya atama operatör fonksiyonları *this biçiminde bir ifadeyle geri döndürülmelidir. Örneğin: T &T::operator =(const T &r) { if (this == &r) return *this; //... return *this; } this anahtar sözcüğünü biz henüz görmedik. Dolayısıyla fonksiyonun bu kısımları üzerinde açıklama yapmayacağız. Ancak kopya atama operatör fonksiyonunun geri kısmı içerik kopyalaması yapacak biçimde yazılmalıdır. Örneğin daha önce yazmış olduğumuz kapasiteli String sınıfında kopya atama oprratör fonksiyonu tipik olarak aşağıdaki gibi yazılmalıdır: class String { //... private: char *m_str; size_type m_size; size_type m_capacity; }; //... String &String::operator =(const String &r) { if (this == &r) return *this; delete[] m_str; // 1 m_str = new char[r.m_capacity]; // 2 strcpy(m_str, r.m_str); // 3 m_size = r.m_size; // 4 m_capacity = r.m_capacity; // 4 return *this; } Aşağıdaki gibi bir atama yapmış olalım: String s{"ankara"}; String k{"izmir"}; k = s; Buradaki k = s işleminin eşdeğeri şöyledir: k.operator =(s); O halde kopya atama operatör fonksiyonu içerisinde doğrudan kullandığımız veri elemanları k'nın veri elemanları r referansıyla kullandığımız veri elemanları ise s'nin veri elemanlarıdır. Şimdi yapılanları adım adım açıklayaliım: 1) delete[] m_str; Burada k için daha önceden tahsis edilmiş olan alan "sızıntı oluşmasında diye" free hale getirilmiştir. 2) m_str = new char[r.m_capacity]; r'nin kapsitesi kadar k için alan tahsis edilmiştir. 3) strcpy(m_str, r.m_str); Burada içerik kopyalaması yapılmaktadır. 4) m_size = r.m_size; m_capacity = r.m_capacity k nesnesinin ywni size ve capacity değerleri olması gerektiği gibi güncellenmiştir. Burada yazmış olduğumuz kopya atama operatör fonksiyonunu daha önce yazmış olduğumuz kopya yapıcı fonksiyon ile karşılaştırınız: String::String(const String &r) { m_str = new char[r.m_size + DEF_CAPACITY]; strcpy(m_str, r.m_str); m_size = r.m_size; m_capacity = r.m_size + DEF_CAPACITY; } //... String s("ankara"); String k(s); Burada kopya yapıcı fonksiyon içerisinde doğrudan kullandığımız veri elemanları henüz yaratılmış olan nesnenin veri elemanlarıdır. Dolayısıyla bizim yeni yaratılan nesnenin m_str elemanı için bir delete işlemi yapmamıza gerek yoktur. Kopya atama operatör fonksiyonu hakkında bu noktada bu kadar bilgiyi yeterli görüyoruz. Ayrıntılar operatör fonksiyonlarının anlatıldığı bölümde ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Referanslarla ilgili overload resolution süreci hakkında daha önce görmüş olduğumuz bazı hatırlatmaları yapmak istiyoruz. Örneğin: void foo(int &r) // 1 { cout << "foo: int &" << endl; } void foo(int &&r) // 2 { cout << "foo: int &&" << endl; } Burada iki foo fonksiyonu vardır. Biri sol taraf değeri referansı diğeri sağ taraf değeri referansı parametresine sahipt.r Bu fonksiyonu aşağıdaki gibi çağırmış olalım: foo(10); Burada tereddüt edilecek bir şey yoktur. Zaten sol taraf değeri referans parametreli fonksiyon "uygun fonksiyon (viable function)" deildir. Dolayısıyla burada sağ taraf değeri referans parametreli fonksiyon çağrılacaktır. Şimdi fonksiyonu şöyle çağırmış olalım: int a = 10; foo(a); Burada da tereddüt edilecek bir durum yoktur. Çünkü burada zaten sağ taraf değeri referans parametreli foo fonksiyonu "uygun (viable)" fonksiyn değildir. Dolayısıyla sol taraf değeri referans parametreli foo çağrılacaktır. Şimdi fonksiyonlar şöyle olsun: void foo(const int &r) // 1 { cout << "foo: int &" << endl; } void foo(int &&r) // 2 { cout << "foo: int &&" << endl; } Fonksiyonu şöyle çağırmış olalım: foo(10); Burada her iki fonksiyon da "uygun (viable)" fonksiyonlardır. Ancak overload resolution kuralı gereği bu tür durumlarda sağ taraf değeri referans parametreli fonksiyonun daha iyi dönüşüm sunduğu kabul edilmektedir. Dolayısıyla burada sağ taraf değeri referans parametreli fonksiyon çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sağ taraf değeri referansına (rvalue reference) bir sağ taraf değerinin bind edilmesi gerektiğini anımsayınız. C++11 ile birlikte "value kategori" ismi altında terimlerin aşağıdaki gibi değiştirildiğini daha önce ifade etmişk: glvalue rvalue / \ / \ / \ / \ lvalue xvalue prvalue Sol taraf değeri referansına sol taraf değerinin bind edilmesi gerekir. Yani biz sol taraf değeri referansına "xvalue" bind edemeyiz. Ancak sağ taraf değeri referansına "xvalue" ve "prvalue" bind edebiliriz. Çünkü hem "xvalue" hem de "prvalue" sağ taraf değeri kabul edilmektedir. Pekiyi nedir bu xvalue? xvalue terimi "expiring value" sözcüklerden kısaltılarak uydrulmuştur. "expiring" sözcüğü "vakti dolan, yok olmak üzere olan" gibi anlamlara gelmektedir. Geçici nesneler, fonksiyonların geri dönüş değerleri, sabitler "prvalue" kabul edilmektedir. Nesne belirten ifadeler ise "lvalue" kabul edilmektedir. Pekiyi hangi ifadeler "xvalue" kabul edilmektedir? C++'ta xvalue belirten üç ifade vardır: 1) Bir ifade eğer sağ taraf türünden referansa dönüştürülürse artık ifade xvalue belirtir. Örneğin: int a = 10; Burada a lvalue belirtir. Ancak (int &&)a ya da static_cast(a) ifadeleri xvalue belirtir.(Bir nesneyi sol taraf değeri referansına dönüştürürsek bu xvalue belirtmez lvalue belirtir.) 2) Bir fonksiyonun geri dönüş değeri sağ taraf değeri türünden referans ise böyle bir fonksiyon çağrıldığında bu çağrı xvalue belirtmektedir. Örneğin: int &&foo() { //... } Burada foo() çağrısı xvalue belirtir. Fonksiyonun geri dönüş değeri referans değilse o çağrının prvalue belirttiğini anımsayınız. Örneğin: int foo() { //... } Burada foo() çağrısı rvalue belirtir. Benzer biçimde fonksiyonun geri dönüş değeri sol taraf değeri referansıysa bu fonksiyonun çağrısı lavlue belirtir. Örneğin: int &foo() { //... } Burada foo() çağrısı lvalue belirtmektedir. 3) Bir fonksiyonda return ifadesinde parametre bir yerel değişken ya da parametre değişkeni varsa bu return ifadesindeki değişken artık xvalue belirtmektedir. Bu özel bir durumdur. Örneğin: T foo() { T a; //... return a; } Burada a yerel değişkeni hayatını kaybetmek üzeredri. İşte return ifadesinde hayatını kaybetmek üzere olan bir lavlue kullanılırsa artık bu lavlue belirtmez xvalue belirtir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Elimizde bir sol taraf değeri varsa biz onu nasıl bir sağ taraf değeri referansına bind edebiliriz? İşte akla gelen en makul yol onu sağ taraf değeri referansına dönüştürmektir. Örneğin: void foo(int &&r) { r = 20; } //... int a = 10; foo(static_cast(a)); // geçerli Burada static_cast(a) ifadeis ile artık a nesnesi bir lvalue olmaktan çıkıp bir rvalue haline getirilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int &&r) { r = 20; } int main() { int a = 10; foo(static_cast(a)); cout << a << endl; // 20 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte lvalue ifadesini sağ taraf değeri referansına dönüştüren move isimli standart bir fonksiyon kütüphaneye eklenmiştir. Bu fonksiyonun prototipi isimli başlık dosyasındadır. move fonksiyonu bir taşıma yapmamaktadır. Bu fonksiyonun tek yaptığı şey lvalue ifadesini sağ taraf değeri referansına dönüştürerek ondan bir xvalue edilmesini sağlamaktır. Dolayısıyla elimizde bir lvalue varsa biz onu sağ taraf değeri referansına bind edeceksek tür dönüştürmesi yerine zaten bu işlemi yapan move fonksiyonundan faydalanabiliriz. Örneğin. #include void foo(int &&r) { r = 20; } //... int a = 10; foo(move(a)); // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bu noktada bir anımsatma da yapmak istiyoruz. İster sol taraf değeir referansı olsun isterse sağ taraf değeri referansı olsun bir referansı ilkdeğer verdikten sonra kullandığımızda artık o ifade bir sol taraf değeri (lvalue) anlamına gelmektedir. Örneğin: int &&r = 10; // geçerli, r'nin içerisinde geçici olarak yaratılan int nesnenin adresi var int &k = r; // geçerli, r kullanılırken artık sol taraf değeri belirtmektedir. Örneğin: void foo(int &r) { //... } void foo(int &&r) { //... } //... int &&r = 10; foo(r); // int & parametreli foo fonksiyonu çağrılır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 49. Ders 12/02/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi move fonksiyonu "bir şeyi taşımamaktadır" yalnızca lvalue ifadesini xvalue (yani rvalue) haline getirmektedir. Bir lvalue ifadesinin sağ taraf değeri referansına dönüştürülmesi ile move fonksiyonuna sokulması arasında bazı küçük farklılıklar vardır. Bu konu şablonların ele alındığı bölümde açıklanacaktır. Biz bir kodda move fonksiyonunun çağrıldığını gördüğümüzde "move fonksiyonuna parametre olarak geçilen nesnenin yaşamını kaybetmek üzere olduğunu dolayısıyla kaynakların onun içerisinden alınarak başka nesneye aktarılmasının istendiğini" anlamalıyız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önce aşağıdaki veri elemanlarına sahip bir String sınıfı yazmıştık: class String { //... private: char *m_str; size_type m_size; size_type m_capacity; }; Bir String nesnesi yaratıldığında onun için m_capacity kadar yer tahsis edilip string'in karakterleri o alan kopyalanıyordu. Biz bu String sınıfı için kopya yapıcı fonksiyonunu da yazmıştık. Şimdi bir öğrencinin bilgilerini tutan bir Student sınıfını aşağıdaki gibi yazmak isteyelim: class Student { public: Student(const String &name, int no) : m_name(name), m_no(no) {} void disp() const; //... private: String m_name; int m_no; }; Şimdi de aşağıdaki gibi Student sınıfı türünden bir nesne yaratalım: Student student{String("Ali Serce", 123)}; student.disp(); Student sınıfının yapıcı fonksiyonu bizden bir String referansı istediği için biz de pratik bir biçimde geçici nesne yaratıp oonu referansa bind ettik. Bu kodda hiçbir problem yoktur. Her nae kadar bu kodda problem yoksa da burada programcıları rahatsız eden bir durum vardır. Geçici String nesnesinin yaratılıp hemen kopya yapıcı fonksiyonu ile Student nesnesinin m_name veri elemanın kopyalnması gereksiz bir işlem gibidir. Çünkü burada hem String türünden geçici nesnenin yaratılması sırasında tahsisat yapılmış, hem de onun kopya yapıcı fonksiyonu ile m_name veri elemanına kopyalnması sırasında tahsisat yapılmıştır. Üstelik de bu geçici nesne bu kopyalamadan sonra hemen yok edilmektedir. İşte C++11'e kadar yukarıdaki etkinlik problemi için bir şey yapılamıyordu. C++11 ile birlikte sağ taraf değeri referansları ve "taşıma semantiği (move semantic)" denilen mekanizma ile buradaki rahatsız edici durum giderilmiştir. Taşıma semantiği hayatını kaybetmek üzere olan (xvalue) nesnenin kaaynaklarını çalarak başka nesnede kullanma sürecini belirtmektedir. Yukarıdaki örnekte Önce giçi string nesnei yaratılacak ve aşağıdaki gibi bir durum oluşacaktır: m_str ---------> xxxxxxxxxx\0________________ m_size (yazının uzunluğu) m_capacity (toplam tahsis edilen alan) Bu nesne Student sınıfının m_name elemanına kopya yapıcı fonksiyonu yoluyla kopyalnırken geçici nesnenin m_str göstericisinin gösterdiği yerin gereksiz bir kopyasında oluşturulmaktadır. Burada gereksiz dememizin nedeni zaten bu geçici nesnenin biraz sonra yok edilecek olmasındadır. İşte taşıma semantiği sayesinde bu kopyalama (artık buna taşıma diyeceğiz) elimine edilebilmektedir. Student nesnesinin m_name elemanının m_str göstericisi geçici yaratılan nesnenin m_str veri elemanı için tahsis edilen alanı doğrudan kendi bünyesine geçirerek kullanabilir. Çünkü zaten bu geçici nesne biraz hayatını kaybedecek dolayısıyla o alana gereksimi olmayacaktır. Bu süreci beyin ölümü gerçekleşen kişinin organlarınının alınarak başka kişiye aktarılmasına benzetebiliriz. Taşıma semantiği "taşıma yapıcı fonksiyonu (move constructor)" ve "taşıma atama operatör fonksiyonu (move assignment operator)" denilen fonksiyonlar yoluyla yapılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın sınıf ismi T olmak üzere aşağıdaki parametre yapısına sahip yapıcı fonksiyonlarına "taşıma yapıcı fonksiyonları (move constructors)" denilmektedir: - T && - const T && - volatile T && - const volatile T && Taşıma yapıcı fonksiyonlarının const parametreli olmasının anlamı yoktur. Dolayısıyla taşıma yapıcı fonksiyonları tipik olarak T && parametreli biçimde karşııza çıkmaktadır. Taşıma yapıcı fonksiyonları C++11 ile birlikte dile eklenmiştir. Zaten C++11 ile birlikte "sağ taraf değeri referanslarının" dile eklenmesinin asıl nedeni "taşıma semantiği (move semantics)" denilen bu durumun sağlanması içindir. Örneğin: class Sample { public: Sample() = default; // default constructor Sample(const Sample &r); // copy constructor Sample(Sample &&r); // move constructor (C++11 ile birlikte eklendi) //... }; Pekiyi sınıfların taşıma yapıcı fonksiyonları ne zaman çağrılmaktadır? İşte taşıma yapıcı fonksiyonları bir sınıf türünden nesneyi aynı sınıf türünden bir sağ taraf değeri referansı ile yaratmak istediğimizde çağrılır. Örneğin: Sample s; Sample k{s}; // k için kopya yapıcı fonksiyonu çağrılır Burada s bir lvalue ifadesidir. Dolayısıyla zaten overload resolution işleminde tek uygun fonksiyon const Sample & parametreli kopya yapıcı fonksiyonudur. Dolayısıyla burada k için sınıfın kopya yapıcı fonksiyonu çağrılacaktır. Örneğin: Sample k{Sample()}; // Sample() ifadesi prvalue olduğundan burada k için taşıma yapıcı fonksiyonu çağrılır Burada Sample() ifadesi ile bir geçici nesne oluşturulmuştur. Geçici nesneler "prvalue" kabul edilmektedir. Dolayısıyla hem const Sample & parametreli hem de Sample && parametreli yapıcı fonksiyonlar uygun fonksiyonlardır. Ancak daha önce de belirttiğimiz gibi bu durumda sağ taraf değeri referansı parametresine sahip olan fonksiyon const sol taraf değeri referansına sahip olan fonksiyondan daha iyi bir dönüştürme sunmaktadır. Dolayısıyla burada k nesnesi için taşıma yapıcı fonksiyonu çağrılacaktır. Buradan özetle şunu söyleyebiliriz: "Sınıfın hem kopya yapıcı fonksiyonu hem de taşıma yapıcı fonksiyonu varsa eğer nesne aynı sınıf türünden sol taraf değeri ile yaratılıyorsa kopya yapıcı fonksiyonu, sağ taraf değeri ile yaratılıyorsa taşıma yapıcı fonksiyonu" çağrılır. Taşıma yapıcı fonksiyonunun geçmişe doğru uyumu bozmadan eklendiğine dikkat ediniz. Yani yukarıdaki örnekte Sample sınıfının taşıma yapıcı fonksiyonu olmasaydı yine her şey C++11 öncesinde olduğu gibi işleyecekti. Örneğin: class Sample { public: Sample() = default; // default constructor Sample(const Sample &r); // copy constructor //... }; Sample s; Sample k{s}; // k için kopya yapıcı fonksiyonu çağrılır Sample r{Sample()}; // r için kopya yapıcı fonksiyonu çağrılır Yukarıdaki Student sınıfına aşağıdaki gibi bri yapıcı fonksiyon daha ekleyelim: class Student { public: Student(const String &name, int no) : m_name(name), m_no(no) {} Student(String &&name, int no) : m_name(move(name)), m_no(no) {} void disp() const; //... private: String m_name; int m_no; }; Şöyle bir kod söz konusu olsun: String name{"Ali Serce"}; int no = 123; //... Student student{name, no}; //... Burada student nesnesi için "const String &, int" parametreli yapıcı fonksiyon çağrılacaktır. Dolayısıyla butadaki name Student sınıfının m_name veri elemanına kopya yapıcı fonksiyonu yoluyla aktarılacaktır. Şimdi kod aşağıdaki gibi olsun: String name{"Ali Serce"}; int no = 123; //... Student student{move(name), no}; //... Burada artık student nesnesi için sınıfın "String &&, int" parametreli yapıcı fonksiyonu çalıştırılacaktır. Bu fonksiyon da sınıfın m_name veri elemanını taşıma yapıcı fonksiyonu yoluyla oluşturacaktır. Yani bu kodu yazan kişi name isimli nesneyi kullanmış ancak daha sonra tekrar kullanmayacağı için onun kaynaklarını transfer etmek istemiştir. Tabii buradaki name yerel değişkeninin kaynakları transfer edildiğinde artık bu yerel değişkenin kod içerisinde kullanılmaması uygun olur. Başka bir deyişle bizim "kaynaklarını çaldığımız nesneyi artık kullanmamamız" gerekir. Aşağıdaki gibi bir fonksiyon olsun: Student foo() { String name{"Ali Serce"}; int no = 123; //... return Student(name, no); } Burada C++17 ve sonrasında "copy elision" zorunlu olduğu için return ifadesindeki geçici nesne aslında geri dönüş değeri için oluşturulacak nesne biçiminde yaratılacaktır. Ancak buarada yine yaratılacak Student nesnesi için onun m_name elemanı name yerel değişkeninden kopyalanarak oluşturulacaktır. Halbuki fonksiyon aşağıdaki gibi yazılsaydı name yerel değişkeni içerisindeki kaynaklar çalınıp kullanılabilirdi: Student foo() { String name{"Ali Serce"}; int no = 123; //... return Student(name, no); } Yukarıdaki Student sınıfının aşağıdaki yapıcı fonksiyonuna dikkat ediniz: Student(String &&name, int no) : m_name(move(name)), m_no(no) {} Burada MIL sentaksında biz move fonksiyonunu kullanmasaydık "sağ taraf değeri referansını ilkdeğer verdikten sonra kullandığımızda o sol taraf değeri belirtirtecekti" dolayısıyla burada move fonksiyonu çağrılmasaydı yine String sınıfının kopya yapıcı fonksiyonu çalıştırılacaktı. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi biz sınıfımız için taşıma yapıcı fonksiyonunu nasıl yazmalıyız? Yukarıda da belirttiğimiz gibi bizim taşıma yapıcı fonksiyonunda yaşamını yitirmek üzere olan nesnenin kaaynaklarını çalmamız ancak onu da "geçerli (valid/consistent)" bırakmamız gerekir. İşte yaşamını kaybetmek üzere olan nesnenin kaynaklarını çaldıktan sonra onu geçerli halde bırakabilmek için onun veri elemanlarının değiştirilmesi gerekmektedir. Zaten sırf bunun için C++11 ile "sağ taraf değeri referansları" konusu eklenmiştir. Burada "hem geçici nesneyi kullanmak hem de onu güncelemmek"" gerekmektedir. Yazmış olduğumuz String sınıfını yeniden anımsayalım: class String { //... String(String &&r) noexcept; // move constructor //... private: char *m_str; size_type m_size; size_type m_capacity; }; Bu sınıf için taşıma yapıcı fonksiyonu şöyle yazılabilir: String::String(String &&r) { m_str = r.m_str; m_size = r.m_size; m_capacity = r.m_capacity; r.m_str = nullptr; } Taşıma yapıcı fonksiyonu şöyle bir bağlamda kullanılmış olsun: String s{"ankara"}; //... String k{move(s)}; // k için taşıma yapıcı fonksiyonu çağrılacak Buarada taşıma yapıcı fonksiynu içerisindeki doğrudan kullandığımız veri elemanları yaratılmakta oaln k nesnesinin veri elemanlarıdır. r referansı ile kullandığımız veri elemanları ise s nesnesinin veri elemanlarıdır. Taşıma yapıcı fonksiyonun son satırına dikkat ediniz: r.m_str = nullptr; Burada biz s nesnesinin m_str veri elemanına NULL adres yerleştiriyoruz. İçerisinde null adres bulunan bir gösterici delete edildiğinde bu işlem bir etkiye yol açmamaktadır. (Yani String sınıfının yıkıcı fonksiyonu içerisinde delete işlemi yaparken NULL adres kontrolünü yapmamıza gerek yoktur.) Aşağıdaki Student sınıfı örneğini çalıştırarak deneyiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // app.cppp #include #include "String.hpp" using namespace std; using namespace CSD; class Student { public: Student(const String &name, int no) : m_name(name), m_no(no) {} Student(String &&name, int no) : m_name(move(name)), m_no(no) {} String name() const { return m_name; } int no() const { return m_no; } void disp() const; private: String m_name; int m_no; }; void Student::disp() const { cout << m_name.c_str() << ", " << m_no << endl; } int main() { String name{"Ali Serce"}; int no = 123; name.disp(); cout << no << endl; Student student(move(name), no); // name nesnesinin kaynakları çalınarak student nesnesinin m_name elemanına aktarılmıştır student.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 50. Ders 14/02/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Taşıma yapıcı fonksiyonu genellikle "nexcept" exception belirleyicisi ile bildirilmktedir. noexcept belirleyicisi "exception" konusu ile ilgilidir. Exception konusu ileride ayrı bir başlık altında ele alınacaktır. noexcept belirleyicisi ilgili fonksiyonun "bir exception fırlatmayacağını" belirtmektedir. Bu belirleyici sayesinde ilgili sınıfı kullanan başka sınıflar "yüksek exception garantisi (strong exception guarantee)" verebilmektedir. Taşıma semantiği çoğu kez zaten bir exception oluşmadan gerçekleştirilmektedir. Dolayısıyla taşıma yapıcı fonksiyonlarında noexcept belirleyicisinin kullanılması genellikle olağan bir durumdur. Ancak bazı özel durumlarda taşıma işlemleri de exception oluşmasına yol açabilmektedir. Bu durumda taşıma yapıcı fonksiyonunda noexcept belirleyicisi kullanılmamalıdır. Biz bu konu görülene kadar taşıma yapıcı fonksiynlarında "noexcept" belirleyicisini kullanacağız. Örneğin: class String { //... String(String &&r) noexcept; // move constructor //... private: char *m_str; size_type m_size; size_type m_capacity; }; String::String(String &&r) noexcept { m_str = r.m_str; m_size = r.m_size; m_capacity = r.m_capacity; r.m_str = nullptr; } noexcept belirleyicisi hem prototipte hem de tanımlamada bulundurulmak zorundadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz daha önce "copy elision" kavramından bahsetmiştik. Ancak o zaman henüz taşıma yapıcı fonksiyonunu görmemiştik. C++11 ile birlikte taşıma yapıcı fonksiyonu ve taşıma semantiği dile eklenince "copy elision" kavramının da ismi "copy/move elisin" haline getirildi. Yani C++11 ve sonrasında yalnızca kopya yapıcı fonksiyonunun elimine edilmesi değil eğer varsa taşıma yapıcı fonksiyonun da elimine edilmesi söz konusu olmaktadır. Bundan sonra biz de "copy elision" terimi terine "copy/move elision" terimini kullanacağız. Bazen copy/move elision işlemi ve taşıma semantiği konusu yeni başlayanlara karışık gelebilmektedir. Örneğin T bir sınıf belirtmek üzere aşağıdaki fonksiyonu inceleyinic: T foo() { T a; //... return a; } Anımsanacağı gibi bu return işleminde copy/move elision işlemi "istağe bağlı (optional)" durumdadır. Bu özel duruma NRVO (Named Return Value Optimization) dendiğini anımsayınız. Eğer burada derleyici copy/move elision yaparsa aslında a yerel nesnesini doğrudan geri dönüş değeri için kullanacağı nesne biçiminde yaratır. Dolayısıyla return işlemi sırasında kopya da taşıma yapıcı fonksiyonları çağrılmaz. Şimdi derleyicinin böyle bir optimizasyonu yapmadığını dolayısıyşa copy/move elision işlemini uygulamadığını düşünelim. Bu durumda return işlemi sırasında yaratılacak geçici nesne için T sınıfının hangi yapıcı fonksiyonu çağrılacaktır? Biz daha önce return ifadesinde bir yerel değişken ya da parametre değişkeninin ismi varsa bu değişkenin xvalue belirttiğini söylemiştik. O halde return işlemi ile yaratılacak geçici T nesnesi için "eğer varsa T sınıfının taşıma yapıcı fonksiyonu" çağrılacaktır. Tabii eğer T sınıfının taşıma yapıcı fonksiyonu yoksa bu geçici T nesnesi için yine T sınıfının kopya yapıcı fonksiyonu çağrılacaktır. Aslında return ifadesindeki nesnenin xvlaue belirtmesi C++14 ile birlikte dile eklendi. C++11'de buradaki nesne lvalue belirtmekteydi. Dolayısıyla programcının geri dönüş değeri olarak yaratılacak nesne için taşıma yapıcı fonksiyonunun çağrılmasını sağlaması için move fonksiyonu kullanması gerekiyordu. Örneğin: T foo() { T a; //... return move(a); // C++14 ve sonrasında move işlemine gerek yok, çünkü artık a zaten xvalue belirtiyor. } C++14 ve sonrasında move işlemine gerek yok, çünkü artık a zaten xvalue belirtmektedir. Şimdi foo fonksiyonunun aşağıdaki gibi kullanıldığını varsayalım: T foo() { T a; //... return a; } T t{foo()}; Burada C++17 ve sonrasında kesinlikle t nesnesi için copy/move elision uygulanacaktır. Dolayısıyla aslında foo fonksiyonunun geri dönüş değeri doğrudan t üzerinde oluşturulacaktır. Tabii C++17 öncesinde copy/move elisin bu durumda isteğe bağlı olduğu için eğer derleyici bu eleminasyonu yapmazsa t nesnesi için T sınıfının varsa taşıma yapıcı fonksiyonunu çağıracaktı. Fakat mevcut satndartlarda yukarıdaki gibi kod aşağıdaki seçeneklerden biri biçiminde el alınmak zorundadır: 1) Eğer derleyici NRVO uygulamazsa bu durumda a yerel nesnesi için default yapıcı fonksiyon çağrılacaktır. return ifadesiyle doğrudan t için taşıma yapıcı fonksiyonu çağrılacaktır. Tabii yerel a nesnesi için yıkıcı fonksiyon da çağrılacaktır. 2) Eğer derleyici NRVO uygularsa burada a yerel değişkeni doğrudan t üzerinde yaratılacaktır. Yani aslında foo içerisinde kullanılan a t nesnesi olacaktır. Dolayısıyla burada yalnızca a nesnesi için default yapıcı foknksiyon çalıştırılmış olacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- NRVO ile geri dönüş değerindeki copy/move elision işleminin derleyiciler tarafından nasıl gerçekleştirildiğini merak edebilirsiniz. Örneğin: T foo() { T a; //... return a; } T t{foo()}; Burada a yerel nesnesi doğrudan t üzerinde oluşturulacaktır. Pekiyi derleyici foo fonksiyonunu bağımsız bir biçimde derlerken buradaki a'nın aslında t olduğunu nereden bilecektir? İşte pek çok derleyici aslında bu tür durumlarda fonksiyona gizlice bu t nesnesinin adresini parametre olarak aktarmaktadır. Yani aslında derleyici yerel değişken a yerine ona parametre olarak geçirilen t'yi kullanmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Taşıma semantiğinde taşınan nesnede değişiklik yapıldığını anımsayınız. O halde const sınıf nesneleri taşınamazlar. Örneğin: void foo(String k) { //... } //... const String s{"ankara"}; //... foo(move(s)); Burada s nesnesinin artık kullanılmayacağından dolayı foo fonksiyonun parametre değişkenine taşınmak istedndiğini düşünelim. Bu durumda amaçladığımız şey k nesnesi için String sınıfının taşıma yapıcı fonksiyonunun çağrılmasıdır. Ancak bu durumda taşıma yapıcı fonksiyonu s üzerinde değişiklik yapmak isteyeceğinden zaten bunu yapamayacaktır. Pekiyi bu durum error mu oluşturacaktır? Aslında bu durumda move fonksiyonunun çağrılması geçerlidir. Ancak taşıma semantiği yerine kopyalama semantiği devreye girecektir. Yani burada move çağrısının bir etkisi olmayacaktır. move fonksiyonu bir lvalue değerini T && türüne dönüştürerek ondan bir xvalue oluşturmayı hedeflamektedir. Ancak bunu yaprken de const'luğu korumaktadır. Dolayısıyla yukarıdaki foo çağrısının eşdeğeri şöyşedir: foo(static_cast(s)); Böyle bir çağrıda artık String sınııfnın taşıma yapıcı fonksiyonu "uygun (viable)" fonksiyon olmaz. Dolayısıyla k nesnesi için String sınıfının kopya yapıcı fonksiyonu çağrılır. Başka bir deyişle burada move çağrısının aslında bir etkisi yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfların kopya yapıcı fonksiyonları ve taşıma yapıcı fonksiyonları açıkça defaulted ya da deleted yapıabilir. Örneğin: class Sample { public: Sample() = default; Sample(const Sample &r) = delete; //... }; Burada biz Sample sınıfı için kopya yapıcı fonksiyonunu yazmasaydık onu derleyici "memberwise copy" yapacak biçimde yazacaktı. Ancak biz bunu açıkça "deleted" hale getirdik. Dolayısıyla derleyic artık kopya yapıcı fonksiyonu bizim için yazmayacaktır. Örneğin: Sample s; Sample k(s); // geçersiz! kopya yapıcı fonksiyonu yok! Açıkça silinmiş fonksiyonlar yine "overload resolution" işlemine sokulmaktadır. Ancak overload resolution işleminde eğer seçilirlerse "error" oluşturacaklardır. Örneğin: class Sample { public: Sample() = default; Sample(const Sample &r); Sample(const Sample &&r) = delete; //... }; //... Sample s; Sample k(move(s)); // geçersiz! burada move constructor seçilir ancak o da deleted yapılmıştır --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi biz yazmadığımız zaman taşıma yapıcı fonksiyonu otomatik olarak derleyici tarafından yazılmakta mıdır? İşte aslınd akopya yapıcı fonksiyonunun ve taşıma yapıcı fonksiyonunun derleyici tarafından yazılmasına ilişkin bazı ayrıntılar vardır. 1) Mevcut standartlara göre "eğer programcı kendi sınıfı için taşıma yapıcı fonksiyonu ya da taşıma atama operatör fonksiyonu (bunu henüz görmedik)" yazmışsa (tanımlamışsa)bu durumda derleyici kopya atama operatör fonksiyonunu kendisi yazmamaktadır. (Standart terminolojisi ile ifade edilirse bu durumda kopya yapıcı fonksiyonu "deleted" olmaktadır.) Eğer programcı kendi sınıfı için taşıma yapıcı fonksiyonu ya da taşıma atama operatör fonksiyonu yazmamışsa bu durumda derleyici kopya yapıcı fonksiyonunu "memberwise copy" yapacak biçimde kendisi yazmaktadır. (Standart terminolojisi ile ifade edilirse bu durumda kopya yapıcı fonksiyonu "defaulted" olmaktadır.) C++20 ile birlikte "eğer sınıfın kopya atama operatör fonksiyonu ya da yıkıcı fonksiyonu programcı tarafından yazılmışsa ilerideki C++ versiyonlarında kopya yapıcı fonksiyonunun derleyici tarafından yazılmayabileceği belirtilmiştir. Yani şimdilik biz sınıfımız için kopya atama operatör fonksiyonu ya da yıkıcı fonksiyonu yazmış olsak bile derleyici kopya yapıcı fonksiyonu kendisi yazmaktadır. Ancak sonraki versiyonlarda bu özel durum söz konusu olduğunda derleyicinin kopya yapıcı fonksiyonu yazmayacağı standartlara eklenebilir. Bu tür durumlara standartlarda bir özelliğin "deprecated" yapılması denilmektedir. 2) Mevcut standartlara göre eğer programcı sınıfı için kopya yapıcı fonksiyonu, kopya atama operatör fonksiyonunu, taşıma yapıcı fonksiyonunu ve yıkıcı fonksiyonu yazmamışsa bu durumda derleyici sınıf için taşıma yapıcı fonksiyonunu kendisi yazmaktadır. (Standart terminolojisi ile ifade edilirse bu durumda taşıma yapıcı fonksiyonu "defaulted" olmaktadır. Eğer programcı sınıfı için bu fonksiyonlardan herhangi birini yazarsa bu durumda taşıma yapıcı fonksiyonu "deleted" değil "bildirilmemiş gibi (undeclared)" olmaktadır. Yukarıdaki kurallar ne anlama gelmektedir? - Biz bir sınıf için taşıma yapıcı fonksiyonunu yazmışsak muhtemelen sınıfımızın kopya yapıcı fonksiyonuna da ihtiyacı olacaktır. Bu durumda derleyicinin kopya yapıcı fonksiyonunu kendisinin yazması yanlışlıklara yol açabilecektir. (Derleyicinin "defaulted" yaptığı kopya yapıcı fonksiyonu memberwise copy yaptığı için böcekler oluşabilecektir.) Aynı durum taşıma atama operatör fonksiyonu için de geçerlidir. - Biz sınıfımız için kopya yapıcı fonksiyonu ya da kopya atama opreatör fonksiyonunu yazmışsak muhtemelen taşıma yapıcı fonksiyonuna da gereksinimimz olacaktır. Benzer sebeple bu durumda da derleyicinin taşıma yapıcı fonksiyonunu bizim için yazması böcek oluşmasına yol açabilecektir. - Biz bir sınıf için yıkıcı fonksiyon yazmışsak muhtemelen sınıfımızda yok edilecek bazı kaynaklar bulunmaktadır. Bu kaynakların bulunması da taşıma ve kopyalamanın da gerekmesi anlamına gelmektedir. Bu nedenle yıkıcı fonksiyonun yazılmış olması derleyici tarafından kopya yapıcı fonksiyonun (bu durum henüz deprecated) ve taşıma yapıcı fonksiyonunun yazılmamasına yol açmaktadır. (eğer derleyici bu durumda bunları yazsaydı derleyicinin yaptığı memberwise-copy işlemi yine böcek oluşmasına yol açabilirdi.) Derleyicinin kendisinin yazdığı taşıma yapıcı fonksiyonu T && parametresine sahiptir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi taşıma yapıcı fonksiyonu için yukarıda belirtitğimiz koşulların sağlanmış olduğunu düşünelim. Derleyicinin kendisinin yadzığı taşıma yapıcı fonksiyonu ne yapmaktadır? İşte derleyicinin kendisinin yazdığı taşıma yapıcı fonksiyonu tıpkı kopya yapıcı fonksiyonu gibi sınıfın karşılıklı veri elemanlarını ilkdeğer veriliyormuş gibi birbirine kopyalamaktadır. Ancak derleyicinin yazdığı taşıma yapıcı fonksiyonu sanki bu işlemi move fonksiyonu ile yapıyormuş gibi bir etki oluşturmaktadır. Özetle derleyicinin kendisinin yazdığı taşıma yapıcı fonksiyonu default olarak sınıfın karşılıklı veri elemanlarını taşımaktadır. Örneğin: class Student { public: Student(const String &name, int no) : m_name(name), m_no(no) {} String name() const { return m_name; } int no() const { return m_no; } void disp() const; private: String m_name; int m_no; }; Buradaki Student sınıfı için derleyici hem kopya yapıcı fonksiyonunu hem de taşıma yapıcı fonksiyonunu kendisi yazacaktır. Derleyicinin kendisini yazdığı bu fonksiyonlar tamamen aşağıdaki gibi olacaktır: Student::Student(const Student &r) : m_name(r.name), m_no(r.m_no) {} Student::Student(Student &&r) : m_name(move(r.name)), m_no(move(r.m_no)) {} /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 51. Ders 19/02/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz kopya yapıcı fonkdiyonunu gördükten sonra kopya atama operatör fonksiyonu hakkında da temel bilgiler vermiştik. Aynı sınıf türünden iki nesne biribirine atandığında sınıfın kopya atama operatör fonksiyonu çağrılıyordu. İşte nasıl kopya yapıcı fonksiyonu ile ilişkili kopya atama operatör fonksiyonu varsa benzer biçimde taşıma yapıcı fonksiyonu şle ilişkili olan "taşıma atama operatör fonksiyonu (move assignment operator" denilen bir fonksiyon da bulunmaktadır. T bir sınıf belirtmek üzere sınıfın T &&, const T &&, volatile T && ve const volatile T && parametreli "operator =" isimli fonksiyonlarına" taşıma atama operatör fonksiyonları denilmektedir. Pratikte en fazla kullanılan taşıma atama operatör fonksiyonu T && parametreli olandır. Biz taşıma atama operatör fonksiyonunu hemen her zaman bu parametrik yapıya sahip bir biçimde görürüz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesine aynı sınıf türünden bir sağ taraf değeri (rvalue) atanmak istendiğinde sınıfın taşıma atama operatör fonksiyonu çağrılmaktadır. Örneğin: T a; //... a = T(); Burada T() bir "prvalue" durumundadır. Yani bir sağ taraf değeridir. İşte bu durumda T sınıfının taşıma atama operatör fonksiyonu çağrılacaktır. Tabii sınıfın hem kopya atama operatör fonksiyonu hem de taşıma atama operatör fonksiyonu bir arada bulunabilir. (Bütüm fonksiyonlarda olduğu gibi atama operatör fonksiyonları da "overload" edilmektedir.) Bu durumda "overload resolution" kuralları devreye girer ve uygun atama operatör fonksiyonu seçilir. Örneğin: class T { public: T &operator =(const T &); T &operator =(T &&) noexcept; //... }; //... T a; T b; a = b; // kopya atama operatör fonksiyonu çağrılacak a = T(); // taşıma atama operatör fonksiyonu çağrılacak Yukarıdaki örnekte a = b işleminin eşdeğeri şöyledir: a.operator =(b); Burada b bir lvalue belirttiğine göre zaten yalnızca kopya atama operatör fonksiyonu uygun fonksiyon durumundadır. Dolayısıyla kopya atama operatör fonksiyonu çağrılacaktır.a = T() işleminin ise eşdeğeri şöyledir: a.operator=(T()); Burada her iki atama operatör fonksiyonu da uygun fonksiyonlardır. Ancak anımsanacağı gibi T && parametreli fonksiyon daha iyi dönüştürme sağlamaktadır. Dolayısıyla burada taşıma atama operatör fonksiyonu çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Taşıma atama operatör fonksiyonuna taşıma yapıcı fonksiyonu ile aynı nedenden dolayı gereksinim duyulmaktadır. Zaten sınıfın taşıma yapıcı fonksiyonu varsa taşıma atama operatör fonksiyonun da olmasını bekleriz. Bir nesneye aynı sınıf türünden bir sağ taraf değeri atandığında söz konusu sağ taraf değeri zaten atama sonrasında yaşımını yitireceğinden dolayı onun kaynakları atanan nesne tarafından çalılanbilir. Böylece gereksiz bir kopyalamanın önüme geçilmiş olur. Tabii tıpkı taşıma yapıcı fonksiyonunda olduğu gibi taşıma atama operatör fonksiyonu da C++'a C++11 ile sokulmuştur. C++11 öncesinde böylesi bir iyileştirme yapılamıyordu. Dolayısıyla bu tür durumlarda kopyalama yapılıyordu. Pekiyi taşıma atama operatör fonksiyonu nasıl yazılmalıdır? Aslında daha önceden de belirttiğimiz gibi operatör fonksiyonları ayrı bir başlık altında değerlendireceğimiz bir konudur. Biz burada bu fonksiyonun yazımına ilişkin reçete vereceğiz. Taşıma atama operatör fonksiyonun da geri dönüş değeri kendi sınıfı türünden referans parametreli olmalıdır. Bu fonksiyonları da *this ifadesiyle geri döndürmeliyiz. Bunun dışında kaynak çalma süreci taşıma yapıcı fonksiyonunda olduğu gibidir. Ancak bir atama işlemi söz konusu olduğu için atamadaki hedef nesnenin atama öncesindeki bazı kaynaklarının boşaltılması da gerekebilecektir. Örneğin String sınıfı için taşıma atama operatör fonksiyonu şöyle yazılabilir: class String { public: //... String &operator =(String &&r) noexcept; //... private: char *m_str; size_type m_size; size_type m_capacity; }; String &String::operator =(String &&r) noexcept { if (this == &r) return *this; delete[] m_str; // hedefin eskidne gösterdiği yer delete ediliyor m_str = r.m_str; // kaynak nesnenin kaynakları çalınıyor m_size = r.m_size; m_capacity = r.m_capacity; r.m_str = nullptr; // kaynak nesnenin geçerli bir durumda bırakılması gerekir return *this; } Burada önce atamanın yapıldığı hedef nesnesin daha önce gösterdiği alan delete edilmiş sonra kaynak nesnenin gösterdiği alan ondan çalınmıştır. Tabii kaynak nesnenin m_str elemanına nullptr yerleştirilerek onun geçerli bir durumda kalması sağlanmıştır. Aslında bu işlem nesnelerin m_str veri elemanlarının karşılıklı biçimde yer değiştirilmesiyle de sağalanabilirdi. İki nesneyi karşılıklı yer değiştirmek için C++'ın standart kütüphanesinde swap isimli bir fonksiyon şablonu bulunmaktadır. Yukarıdaki taşıma atama operatör fonksiyonu aşağıdaki gibi de yazılabilirdi: String &String::operator =(String &&r) noexcept { if (this == &r) return *this; swap(m_str, r.m_str); m_size = r.m_size; m_capacity = r.m_capacity; return *this; } Burada biz kaynak nesne ile hedef nesnenin m_str elemanlarını yer değiştirdik. Dolayısıyla bizim artık hedef nesnenin m_str elemanını delete etmemize gerek kalmamıştır. Nasıl olsa kayna nesne yok edilirken bu alanı delete edecektir. Burada bir noktaya dikkatinizi çekmek istiyoruz. Biz yukarıdaki kodda yalnızca nesnelerin m_str elemanlarını yer değiştirdik. Bu durumda kaynak nesnenin m_size ve m_capacity elemanları olması gereken değerde olmayacaktır. Pekiyi bu durum bir sorun oluşturur mu? Aslında buradaki kaynak nesnenin yalnızca "destruct edilebilmesi" yeterlidir. Ancak nesnenin bu haliyle "destruct" edilebildiği halde "geçerli" bir durumda olmadığına dikkat ediniz. Kaynak nesne geçici bir nesne ise zaten bu işlemden sonra yok edilecektir. Ancak kaynak nesne move ile taşınmışsa hala yaşamaya devam ediyor olabilir. Biz daha önce taşınan nesnenin bir daha kullanılmaması gerektiğini belirtmiştik. İşte eğer burada kaynak nesne move ile taşınmışsa onun daha sonra kullanılması soruna yol açabilir. Tabii bu tür durumlarda nesneyi tam geçerli durumda bırakmak için diğer elemanlar da swap edilebilir. Örneğin: String &String::operator =(String &&r) noexcept { if (this == &r) return *this; swap(m_str, r.m_str); swap(m_size, r.m_size); swap(m_capacity, r.m_capacity); return *this; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz daha önce "kendi sınıfımız için kopya atama operatör fonksiyonunu yazmazsak bu operatör fonksiyonunun sınıfın karşılıklı veri elemanlarını birbirine atayacak biçimde derleyici tarafından yazıldığını" belirtmiştik. Pekiyi ya taşıma atama operatör fonksiyonunu yazmazsak ne olur? İşte kopya atama operatör fonksiyonunun ve taşıma atama operatör fonksiyonun derleyici tarafından yazılıp yazılmayacağına ilişkin bazı ince kurallar bulunmaktadır: 1) Eğer sınıfımız için biz "taşıma yapıcı fonksiyonu ya da taşıma atama operatör yazmışsak bu durumda kopya atama operatör fonksiyonu "deleted" yapılmaktadır. Ancak bu iki fonksiyonu da sınıfımızda yazmamışsak kopya atama operatör fonksiyonu "defaulted" yapılmaktadır. Yani derleyici tarafından sınıfın karşılıklı veri elemanlarının birbirine atanmasını sağlamaktadır. Ancak bu defaulted durum mevcut standartlarda "deprecated" durumdadır. Yani sonraki standartlarda sınıfın kopya yapıcı fonksiyonun ya da yıkıcı fonksiyonunun olması durumunda kopya atama operatör fonksiyonunun "deleted" olabielceği belirtilmiştir. 2) Eğer sınıfımız için biz "kopya yapıcı fonksiyonunu" ya da "kopya atama operatör fonksiyonunu" ya da "taşıma yapıcı fonksiyonunu" ya da yıkıcı fonksiyonu yazmışsak taşıma atama operatör fonksiyonu derleyici tarafından yazılmamaktadır. (Derleyici bu fonksiyonu "deleted" yapmamaktadır. "undeclared" yapmaktadır.) Eğer bu fonksiyonların hiçbiri yazılmadıysa bu durumda taşıma atama operatör fonksiyonu derleyici tarafından sınıfın karşılıklı veri elemanlarını move ile atayacak biçimde (yani taşıyacak biçimde) yazılmaktadır. Örneğin: class Student { public: Student(const String &name, int no) : m_name(name), m_no(no) {} String name() const { return m_name; } int no() const { return m_no; } void disp() const; private: String m_name; int m_no; }; Buradaki Student sınıfı için derleyici aşağıdaki özel fonksiyonlrın hepsini kendisi default işlem yapacak biçimde yazacaktır: - Kopya yapıcı fonksiyon - Taşıma yapıcı fonksiyon - Kopya tama operatör fonksiyonu - Taşıma atama operatör fonksiyonu Yani bu sunı için bizim fonksiyonları yazmamıza hiç gerek yoktur. Örneğin: Student x{"Ali Serce",123}; //... x = Student{"NecatiErgin, 456"}; Burada derleyicinin yazdığı taşıma atama operatör fonksiyonu çağrılacak ve zaten sınıfın m_name ve m_no elemanları taşınacaktır. Yani burada m_name için kaynaklar kaynak nesneden çalınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte hangi "özel üye fonksiyonların (special member function)" derleyici tarafından "ne zaman" yazılıp yazılmayacağı konusu iyice karmaşık hale gelmiştir. Programcıların yukarıdaki kuralları akılda tutması zor olabilmektedir. Bu nedenle bazı kitaplarda yer alan aşağıdaki tabloyu vermek istiyoruz. Bir özel üye fonksiyonun "deleted" yapılmasıyla "hiç yazılmaması (not declared)" arasında küçük bir farklılık vardır. Bir özel üye fonksiyon "deleted" ise "isim aramasında o fonksiyon seçilirse" error oluşmaktadır. Halbuki bir özel fonksiyon "not declared" ise isim aramsına zaten o fonksiyon girmeyecektir. Dolayısıyla ona rakip olan fonksiyon seçilecektir. Örneğin taşıma atama operatör fonksiyonun "deleted" olduğunu varsayalım: T a; a = T(b); // geçersiz! derleme sırasında error oluşur Şimdi de taşıma atama operatör fonksiyonun "not declared" olduğunu varsayalım: T a; a = T(b); // geçerli, kopya atama operatör fonksiyonu en uygun fonksiyon olarak seçilir. default constructor destructor copy constructor copy assignment move constructor move assignment nothing default default default default default default any constructor not declared default default default default default default constructor user declared default default default default default destructor default user declared default default not declared not declared (deprecated) (deprecated) copy constructor not declared default user declared default not declared not decalred (deprecated) copy assignment default default default user declared not declared not declared (deprecated) move constructor not declared default deleted deleted user declared not declared move assignment not declared default deleted deleted not declared user declared --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Özel üye fonksiyonlar ister "defaulted" ister "deleted" isterse "not declared" olsun. Biz onları istediğimiz zaman ""= default", "= delete"sentaksıyla defaulted ya da deleted hale getirebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 52. Ders 21/02/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Kopya yapıcı fonksiyonu, taşıma yapıcı fonksiyonu, kopya atama operatör fonksiyonu ve yaşıma atama operatör fonksiyonu konularını pekiştirebilmek için bir Matrix sınıfı örneği vermek istiyoruz. Örneğimizdkei Matrix sınıfının bildirimi şöyledir: class Matrix { public: Matrix(); Matrix(size_t rowsize, size_t colsize); Matrix(const Matrix &r); Matrix(Matrix &&r) noexcept; ~Matrix(); void disp() const; Matrix add(const Matrix &r) const; Matrix sub(const Matrix &r) const; Matrix mul(const Matrix &r) const; Matrix matmul(const Matrix &r) const; Matrix div(const Matrix &r) const; double &at(size_t row, size_t col); const double &at(size_t row, size_t col) const; Matrix &operator =(const Matrix &r); Matrix &operator =(Matrix &&r) noexcept; private: double *m_matrix; size_t m_rowsize; size_t m_colsize; }; Buradaki Matrix sınıfı double türden satır ve sütun uzunluğu belirli olan bir matrisin bilgilerini tutup onun üzerinde işlemler yapmaktadır. Matrisin elemanları tek boyutlu bir dizi biçiminde tutulmaktadır. SInıfın add, sub, mul ve div fonksiyonları matrisin karşılıklı elemaları üzerinde işlem yapmaktadır. matmul ise matris çarpımını gerçekleştirmektedir. Aşağıdaki koda dikkat ediniz: Matrix x{3, 2}, y{3, 2}, z; z = x.add(y); Burada x.add(y) işleminden bir rvalue elde edilecektir. Dolayısıyla atama işlemi "taşıma atama operatör fonksiyonuyla" gerçekleştirilecektir. Örneğin: Matrix z{x.add(y)}; Burada C++17 ve sonrasında "copy/move elision" zorunlu hale getirildiğinden add fonksiyonunun geri dönüş değeri doğrudan z nesnesi üzerinde oluşturulacaktır. Sınıfın add üye fonksiyonun nasıl yazıldığına dikkat ediniz: Matrix Matrix::add(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] + r.m_matrix[i]; return result; } Burada return ifadesindeki result nesnesi "xvalue" biçimindedir. Dolayısıyla geri dönüş değeri için sınıfın taşıma yapıcı fonksiyonu çağrılacaktır. Bu durumda sınıfta taşıma yapıcı fonksiyonu olmasaydı kopya yapıcı fonksiyonu çağrılacaktı. Tabii aslında bu durumda derleyicimiz NRVO ile "copy/move" elision işlemini de isteğe bağlı bir biçimde yapabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // matrix.hpp #ifndef MATRIX_HPP_ #define MATRIX_HPP_ #include namespace CSD { class Matrix { public: Matrix(); Matrix(size_t rowsize, size_t colsize); Matrix(const Matrix &r); Matrix(Matrix &&r) noexcept; ~Matrix(); void disp() const; Matrix add(const Matrix &r) const; Matrix sub(const Matrix &r) const; Matrix mul(const Matrix &r) const; Matrix matmul(const Matrix &r) const; Matrix div(const Matrix &r) const; double &at(size_t row, size_t col); const double &at(size_t row, size_t col) const; Matrix &operator =(const Matrix &r); Matrix &operator =(Matrix &&r) noexcept; private: double *m_matrix; size_t m_rowsize; size_t m_colsize; }; } #endif // matrix.cpp #include #include #include "matrix.hpp" using namespace std; namespace CSD { Matrix::Matrix() : m_matrix(nullptr), m_rowsize(0), m_colsize(0) {} Matrix::Matrix(size_t rowsize, size_t colsize) { m_matrix = new double[rowsize * colsize]; m_rowsize = rowsize; m_colsize = colsize; } Matrix::Matrix(const Matrix &r) { m_matrix = new double[r.m_rowsize * r.m_colsize]; memcpy(m_matrix, r.m_matrix, r.m_rowsize * r.m_colsize * sizeof(double)); m_rowsize = r.m_rowsize; m_colsize = r.m_colsize; } Matrix::Matrix(Matrix &&r) noexcept { m_matrix = r.m_matrix; m_rowsize = r.m_rowsize; m_colsize = r.m_colsize; r.m_matrix = nullptr; } Matrix::~Matrix() { delete[] m_matrix; } void Matrix::disp() const { for (size_t row = 0; row < m_rowsize; ++row) { for (size_t col = 0; col < m_colsize; ++col) cout << m_matrix[row * m_colsize + col] << ' '; cout << endl; } } Matrix Matrix::add(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] + r.m_matrix[i]; return result; } Matrix Matrix::sub(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] - r.m_matrix[i]; return result; } Matrix Matrix::mul(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] * r.m_matrix[i]; return result; } Matrix Matrix::matmul(const Matrix &r) const { if (m_colsize != r.m_rowsize) throw invalid_argument("matrix size mismatch"); Matrix result{m_rowsize, r.m_colsize}; double total; for (size_t i = 0; i < m_rowsize; ++i) for (size_t k = 0; k < r.m_colsize; ++k) { total = 0; for (size_t j = 0; j < m_colsize; ++j) total += m_matrix[i * m_colsize + j] * r.m_matrix[j * r.m_colsize + k]; result.m_matrix[i * result.m_colsize + k] = total; } return result; } Matrix Matrix::div(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] / r.m_matrix[i]; return result; } double &Matrix::at(size_t row, size_t col) { if (row >= m_rowsize || col >= m_colsize) throw invalid_argument("invalid index"); return m_matrix[row * m_colsize + col]; } const double &Matrix::at(size_t row, size_t col) const { if (row >= m_rowsize || col >= m_colsize) throw invalid_argument("invalid index"); return m_matrix[row * m_colsize + col]; } Matrix &Matrix::operator =(const Matrix &r) { if (this == &r) return *this; if ((m_rowsize != 0 && m_colsize != 0) && (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize)) throw invalid_argument("invalid matirx dimensions"); double *new_matrix = new double[r.m_rowsize * r.m_colsize]; delete[] m_matrix; memcpy(new_matrix, r.m_matrix, r.m_rowsize * r.m_colsize * sizeof(double)); m_matrix = new_matrix; m_rowsize = r.m_rowsize; m_colsize = r.m_colsize; return *this; } Matrix &Matrix::operator =(Matrix &&r) noexcept { if (this == &r) return *this; if ((m_rowsize != 0 && m_colsize != 0) && (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize)) throw invalid_argument("invalid matirx dimensions"); delete[] m_matrix; m_matrix = r.m_matrix; m_rowsize = r.m_rowsize; m_colsize = r.m_colsize; r.m_matrix = nullptr; return *this; } } // app.cpp #include #include "matrix.hpp" using namespace std; using namespace CSD; int main() { Matrix x{3, 2}, y{2, 4}; x.at(0, 0) = 1; x.at(0, 1) = 2; x.at(1, 0) = 3; x.at(1, 1) = 4; x.at(2, 0) = 5; x.at(2, 1) = 6; y.at(0, 0) = 1; y.at(0, 1) = 2; y.at(0, 2) = 3; y.at(0, 3) = 4; y.at(1, 0) = 5; y.at(1, 1) = 6; y.at(1, 2) = 7; y.at(1, 3) = 8; x.disp(); cout << "-------------" << endl; y.disp(); cout << "-------------" << endl; Matrix result; result = x.matmul(y); result.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 53. Ders 26/02/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte bazı programlama dillerinde var olan sınıf nesnelerine küme parantezi ile ilkdeğer verme imkanı da dile eklenmiştir. Örneğin: vector v{1, 2, 3, 4, 5}; Burada dinamik büyütülen diziyi temsil eden vector isimli bir şablon sınıf türünden bir nesne yaratılmıştır. Ancak nesnenin aynı zamanda küme parantezleri içerisindeki ddeğerleri tutması da sağlanmıştır. C++ standartlarında nesnelere bu biçimde küme parantezleriyle ilkdeğer verilmesine genel olarak "list initialization" denilmektedir. Bu özelliğin uygulanabilmesi için initializer_list isimli bir sınıf şablonu da C++11 ile standartlara eklenmiştir. initializer_lits sınıfı bir şablon sınıf olduğu için açısal parantezler içerisinde tür belirten bir ifadenin bulundurulması gerekmektedir. Örneğin: initialzier_list il; initializer_list nesnesi yaratılırken açısal parantezler içerisinde belirtilen tür bu nesnenin "hangi türden elemanları" tutacağını belirtmektedir. BuYukarıdaki örnekte il nesnesi "int" türden elemanlara ilişkindir. initialzier_list sınıfı isimli bir başlık dosyasında bildirilmiştir. Dolayısıyla bu sınıfı kullanabilmek için bu başlık dosyasının include edilmesi gerekir. initializer_list sınıfının üç public üye fonksiyonu vardır: size, begin ve end. size üye fonksiyonu bize dizinin uzunluğunu belirten size_t türünden bir değer geri döndürür. begin üye fonksiyonu dizinin ilk elemanın adresini, end üye fonksiyonu ise "dizinin son elemanından sonraki" adresi geri döndürmektedir. Bir initializer_list nesnesi default yapıcı fonksiyonla yaratılabilir. Örneğin: initialzier_list il; Bu durumda nesne boş diziyi belirtmektedir. Dolayısıyla size üye fonksiyonu 0 değerini verecektir. begin ve end üye fonksiyonları da tipik olarak nullptr değerlerini verir. Ama aslında initializer_list nesneleri küme parantezleriyle ilkdeğer verilerek yaratılır. Örneğin: initialzier_list il = {10, 20, 30, 40, 50}; Burada ilkdeğer verilirken '=' atmonun bulundurulup bulundurulmaması arasında bir farklılık yoktur. Yani yukarıdaki ilkdeğer verme aşağıdaki ile tamamen eşdeğerdir: initialzier_list il{10, 20, 30, 40, 50}; Pekiyi burada ne olmaktadır? Bu durumda derleyici "ilgili türden const bir dizi oluşturur ve küme parantezi içerisinde belirtilen elemanları tek tek bu diziye yerleştirir. Sonra da initializer_list nesnesinin veri elemanlarına bu diziyi belirtecek biçimde ilkdeğerlerini verir. Yani artık buradaki il nesnesi ile size üye fonksiyonunu çağırırsak küme parantezleri içerisinde yazdığımız elemanların sayısını elde ederiz. begin üye fonksiyonunu çağırırsak oluşturulan dizinin başlangıç adresini (yani oluşturulan dizinin ilk elemanının adresini), end üye fonksiyonu çağırırsak oluşturulan dizinin son elemanından sonraki adresi elde ederiz. Burada nesnenin elemanlarına değerlerini derleyicinin yerleştirdiğine dikkat ediniz. Tabii bize arayüz olarak size, begin ve end üye fonksiyonları verilmiştir. Standartlar sınıfın private bölümünde nelerin olduğunu açıklamamaktadır. Ancak tipik olarak sınıfta iki private veri elemanı bulundurulmaktadır. Bu iki eleman ilgili dizinin başını ve sonunu tutan iki gösterici olabileceği gibi, ilgili dizinin başına tutan bir gösterici ile dizinin uzunluğunu tutan size_t tütünden bir nesne de olabilir. initializer_list sınıfının begin ve end üye fonksiyonları const bir adres vermektedir. Bu fonksiyonlar aynı zamanda constexpr fonksiyonlardır. size üye fonksiyonu da constexpr bir fonksiyondur. Bir initializer_list nesnesi küme parantezleriyle ilkdeğer verilerek oluşturulduğunda derleyicinin küme parantezleri içerisindeki değerleri ilgili türden const bir dizinin içerisine yerleştirdiğini belirtmiştik. Bu dizi derleyici tarafından yaratılmaktadır. Bu nedenle "geçici bir nesne (temporaray object)" niteliğindedir. Standartlara göre küme parantezleri ile initializer_list nesnesine ilkdeğer veridliğinde derleyici tarafından yaratılan sont türden bu geçici dizi initializer_list nesnesi yaşadığı sürece yaşamakta bu nesne yaşamını kaybettiğinde bu dizi de yaşamını kaybatmektedir. Başka bir deyişle bu dizinin ömrü inializer_list nesnesinin ömrü kadardır. Örneğin: initializer_list il{10, 20, 30, 40, 50}; Burada 10, 20, 30, 40, 50 değerlerini tutan dizi il nesnesi yaşadığı sürece yaşamını devam ettirecektir. Anımsanacağı gibi C++11 ve sonrasında bütün küme parantezleri ile ilkdeğer verme işleminde "narrowing conversion" kuralı uygulanmaktadır. Örneğin: initializer_list il = {10, 20, 30.2, 40, 50}; // geçersiz! Ancak örneğin: initializer_list il = {10, 20, 30.2, 40, 50}; // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Mademki biz initializer_list sınıfının begin ve end fonksiyonları ile derleyici tarafından yaratılan dizinin başlangıç ve bitiş adreslerini elde edebiliyoruz, o halde bu dizinin elemanlarına da erişebiliriz. Örneğin: initializer_list il = {10, 20, 30, 40, 50}; const int *pi1, *pi2; pi1 = il.begin(); pi2 = il.end(); while (pi1 != pi2) { cout << *pi1 << " "; ++pi1; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { initializer_list il = {10, 20, 30, 40, 50}; const int *pi1, *pi2; pi1 = il.begin(); pi2 = il.end(); while (pi1 != pi2) { cout << *pi1 << " "; ++pi1; } cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- initializer_list sınıfının begin ve end üye fonksiyonlarının olması nedeniyle bu sınıf iteratör desteğine sahiptir. Dolayısıyla aralık tabanlı for döngülerinde kullanılabilmektedir. Örneğin: initializer_list il = {10, 20, 30, 40, 50}; for (int x : il) cout << x << " "; cout << endl; Burada değişken bir referans olabilir ancak begin üye fonksiyonu const bir adres geri döndürüğü için referansın da const olması gerekir. Örneğin: for (const int &x : il) // referansın const olması gerekir cout << x << " "; cout << endl; Benzer biçimde yukarıdaki örnekte auto belirleyicisi kullanılırsa referans olmadığı durumda x int türden referans olduğu durumda const int & türünden olacaktır. Örneğin: for (auto &x : il) // burada x const int & türünden cout << x << " "; cout << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyonun parametre değişkeni initializer_list türünden olabilir. Bu durumda fonksiyon doğrudan küme parantezleri ile çağrılabilir. Örneğin: void foo(initializer_list il) { for (auto x : il) cout << x << " "; cout << endl; } int main() { foo({10, 20, 30, 40, 50}); return 0; } Burada yapılan işlemin aşağıdakinden bir farkı yoktur: initializer_list il = {10, 20, 30, 40, 50}; Benzer biçimde fonksiyonun geri dönüş değeri de initializer_list türünden olabilir. Örneğin: iinitializer_list foo() { //... } Standartlara göre initializer_list nesnesine küme parantezleriyle ilkdeğer verildiğinde bu işlem sanki bir referansa geçici bir nesneyle ilkdeğer veriliyormuş gibi ele alınmaktadır. Yani bu durumda yaratılan geçici dizi yukarıda da belirttiğimiz gibi initializer_list nesnesi yaşadığı sürece yaşamaktadır. Aşağıdaki kodu inceleyiniz: const int &foo() { return 10; } int x; x = foo(); C++'ta fonksiyonun içerisinde yaratılmış olan geçici nesneler fonksiyon çağrısı bittikten sonra "referansa bind edilseler bile" yok edilmektedir. Dolayısıyla yukarıda kod tanımsız davranışa yol açacaktır. Şimdi aşağıdaki kodu inceleyiniz: initializer_list foo() { return {10, 20, 30, 40, 50}; } initializer_list nesnesine küme paranteziyle değer atamaının bir referansa geçici nesneyle değer atamaktan bir farkı yoktur. Burada da küme parantezleri içerisindeki değerlerin yerleştirildiği geçici dizi fonksiyon bittiğinde yok edilecektir. Dolayısıyla burada da aynı biçimde bir hata yapılmıştır. Örneğin: initializer_list foo(initializer_list il) { return il; } auto result = foo({10, 20, 30, 40, 50}); Bu kod da benzer bir soruna yol açacaktır. Fonksiyonun parametre değişkeni yok edildiğinde derleyici tarafından yaratılan geçici dizi de yok edilecektir. Dolayısıyla benzer bir durum oluşacaktır. initializer_list türünden referanslar tanımlanabilir. Ancak initializer_list nesneleri zaten çok az veri elemanına sahiptir. Dolayısıyla onların adres yoluyla fonksiyonlara aktarılması ile değer yoluyla fonksiyonlara aktarılması arasında bir performans kazancının sağlanması beklenmemelidir. Örneğin: void foo(const initializer_list &il) { //... } //... foo({10, 20, 30, 40, 50}); Bu çağrı geçerlidir. Burada yine derleyici bir geçici dizi ve bir de geçici initializer_list nesnesi oluşturup onun adresini parametre değişkenine aktaracaktır. Dolayısıyla burada bir performans kazancı söz konusu olmayacaktır. Çünkü burada zaten derleyici yine bir initializer_list nesnesini kendisi oluşturmaktadır. Yani burada yine aslında bir nesnin yaratılması söz konusu olmaktadır. Yani yukarıdaki kodrun eşdeğeri aslında şöyle olmaktadır: foo(initializer_list({10, 20, 30, 40, 50})); Bu nedenle pratikte programcılar initializer_list türünden referanslar yerine doğrudan initializer_list nesnelerin kendisini kullanmayı tercih etmektedir. Yukarıda da belirtitğimiz gibi genel initializer_list nesneleri zaten az yer kapladığı için onların fonksiyonlara adres yoluyla aktarılmasıyla değer yoluyla aktarılması arasında önemli bir performans farkı yoktur. Örneğin: void foo(initializer_list &il) { //... } //... initializer_list il = {10, 20, 30, 40, 50}; //... foo(il); Burada foo fonksiyonunun parametresinin referans yapılması önemli bir performans kazancı oluşturmayacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi biz iç içe küme parantezleriyle bir nesneye ilkdeğer vermek istersek buradaki initilizer_list türü ne olacaktır? Örneğin: initializer_list ils = {{10, 20, 30}, {40, 50, 60}, {70, 80, 90}}; Barada ? yerine biz int yerleştiremeyiz. İşte bu tür durumlarda initializer_list türünün kendisi de initializer_list türünden olmalıdır. Örneğin: initializer_list<> ils = {{10, 20, 30}, {40, 50, 60}, {70, 80, 90}}; // geçerli Burada ils nesnesi aslında initializer_list nesnelerinin bulunduğu bir diziyi belirtmektedir. Dolayısıyla biz burada ils nesnesini aralık tabanlı for döngüsüyle dolaşırsak initializer_list nesnelerini elde ederiz. Onu da dolaşırsak int nesnelerini elde ederiz. Örneğin: initializer_list> ils = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; for (initializer_list il : ils) { for (int x : il) cout << x << " "; cout << endl; } Aşağıda bu duruma ilişkin bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include int main() { initializer_list> ils = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; for (initializer_list il : ils) { for (int x : il) cout << x << " "; cout << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aynı isimli birden fazla initializer_list parametresine sahip fonksiyon olabilir. Bu durumda overload resolution işleminde küme parantezleri içerisindeki değerlerin türlerine bakılır. Bu türle tam uyuşum sağlayan fonksiyon varsa doğal olarak o seçilecektir. Örneğin: void foo(initializer_list il) { cout << "initializer_list" << endl; } void foo(initializer_list il) { cout << "initializer_list" << endl; } Burada foo fonksiyonunu şöyle çağırmış olalım: foo({10, 20, 30, 40, 50}); // initilaizer_list parametreli olan fonksiyon çağrılır initializer_list parametresine sahip olan fonksiyon çağrılacaktır. Fonksiyonu şöyle çağırmış olalım: foo({10.1, 20.2, 30, 40, 50}); // ambiguity error Burada verilen ilkdeğerlerin türlerinin bir kısmı int bir kısmı double biçimdedir. Dolayısıyla ambiguity oluşacaktır. Çünkü burada derleyici initializer_list ve initializer_list arasında bir seçim yapamamktadır. Buradaki overload resolution sürecinde şöyle bir kural vardır: Küme parantezi içerisindeki her değer initializer_list parametresindeki T türüne otomatik dönüştürülmeye çalışılır. Buradaki en kötü kalitedeki dönüştürme overload resolution işleminde otomatik dönüştürme kategorisi olarak ele alınmaktadır. Burada önemli olan noktalardan biri de şudur: Normalde küme parantezleri "daraltıcı dönüştürmelere (narrowing conversions)" izin vermemektedir. Ancak overload resolution işleminde daraltıcı dönüştürme uygulayan fonsiyonlar da sanki "uygun (viable)" fonksiyon gibi en uygun fonksiyonun seçimindeki yarışa sokulmaktadır. Eğer bu yarışta daraltıcı dönüştürme uygulayan fonksiyon seçilirse bu durumda error oluşur. Başka bir deyişle "daraltıcı dönüştümenin uygulanıp uygulanmadığına en uygun fonksiyon seçildikten sonra" bakılmaktadır. Örneğin: void foo(initializer_list il); void foo(initializer_list il); //... foo({1, 2, 3.14, 4, 5}); // ambiguity error Burada her fonksiyon da aday ve uygun kabul edilmektedir. Birinci fonksiyon için elemanlardan int türüne en kötü dönüştürme "standart dönüştürme (standart conversion)", ikinnci fonksiyon için de elemanlardan double türüne en kötü dönüştürme standart dönüştürmedir. Bu durumda her iki fonksiyon da eşit iyilikte ya da kötülüktedir. Örneğin: foo({1.1, 2.2, 3.3f, 4.4, 5.5}); // initializer_list Burada birinci fonksiyon için en kötü dönüştürme "standart dönüştürme", ikinci fonksiyon için en kötü dönüştürme "double türüne yükseltme (floating point promotion)" biçimindedir. Dolayısıyla burada initializer_list parametresine sahip olan fonksiyon seçilecektir. -------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tabii initializer_list nesnesine boş küme parantezleri ile de ilkdeğer verilebilir. Bu durumda nesnenin size üye fonksiyonu 0 değerini verecektir. Standartlar bu durumda begin ve end üye fonksiyonlarının hangi değeri vereceği konusunda bir şey söylememiştir. Ancak tipik olarak bu fonksiyonlar nullptr ile geri dönerler. Tabii bizim size değeri 0 iken begin fonksiyonunun geri döndürdüğü adrese erişmememiz gerekir. Örneğin: initialzier_list il = {}; // geçerli cout << il.size() << endl; // 0 Aralık tabanlı for düngüsünde doğrudan küme parantezi içerisinde değerler kullanılabilir. Bu durumda bu küme aparantezi içerisindeki değerler derleyici tarafından initilizer_list nesnesine dönüştürülmektedir. Örneğin: for (int x : {10, 20, 30, 40, 50}) cout << x << " "; cout << endl; Burada derleyici küme parantezi içerisindeki değerlerden initilizer_list nesnesi oluşturmaktadır. Böylesi bir durumda yukarıdaki overload resolution işlemindeki gibi küme parantezleri içerisindeki değerlerin hepsinin uygun türden olması gerekmektedir. Örneğin: for (double x : {10.2, 20.2, 30.3, 40.4, 50.4}) // geçerli cout << x << " "; cout << endl; Burada derğerlerin hepsi double türdendir. Ancak örneğin: for (double x : {10, 20, 30.3, 40, 50}) // geçersiz! cout << x << " "; cout << endl; Burada küme parantezi içerisindeki bazı değerler int türden bazı değerler double türündendir. Dolayısıyla burada yaratılacak nesnesin initilizer_list türünde mi olacağı yoksa initializer_list türünden mi olacağı belirsizdir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın initializer_list ya da const/volatile initializer_list & parametreli yapıcı fonksiyonuna "initializer_list yapıcı fonksiyonu" denilmektedir. Bu biçimdeki sınıf nesneleri yaratılırken bunlara küme parantezleri içerisinde ilkdeğer verilebilir. Örneğin: Sample s{10, 20, 30, 40, 50}; Burada Sample sınıfının eğer uyguna initializer_list parametreli yapıcı fonksiyonu çağrılabilir. Örneğin: class Sample { public: Sample(initializer_list il); //... }; Sample s{10, 20, 30, 40, 50}; Burada s nesnesi için Sample sınıfının initilizer_list parametreli yapıcı fonksiyonu çağrılacaktır. initializer_list nesnesine küme parantezleri içerisinde ilkdeğer verilirken yaratılan geçici dizinin ömrünün initializer_list nesnesi kadar olduğunu anımsayınız. Bu durumda yukarıdaki örnekteki Sample sınıfının initializer_list parametreli yapıcı fonksiyonu bu dizideki değerleri uygun bir yere kopyalamalıdır. Aksi takdirde yapıcı fonksiyondan çıkıldığında söz konusu bu geçici dizi de yok edilecektir. Aşağıdaki örnekte initializer_list nesnesinin belirttiği geçici dizideki değerler dinamik olarak tahsis edilmiş başka bie alana kopyalanmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class Sample { public: Sample(initializer_list il); ~Sample(); void disp() const; private: int *m_pi; size_t m_size; }; Sample::Sample(initializer_list il) { m_pi = new int[il.size()]; const int *pi = il.begin(); for (int i = 0; i < il.size(); ++i) m_pi[i] = pi[i]; m_size = il.size(); } Sample::~Sample() { delete[] m_pi; } void Sample::disp() const { for (size_t i = 0; i < m_size; ++i) cout << m_pi[i] << " "; cout << endl; } int main() { Sample s{10, 20, 30, 40, 50}; s.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz daha önce sınıf türünden nesneleri yaratırken yapıcı fonksiyona geçirilecek değerleri unifiorm initializer sentaks ismiyle küme parantezleri içerisinde vermiştik. Pekiyi bu durumda sınıfın hem initializer_list parametreli hem de diğer tür parametreli yapıcı fonksiyonları aynı anda bulunuyorsa ne olacaktır? Örneğin: class Sample { public: Sample(int a, int b); Sample(initializer_list il); //... }; Sample s{10, 20}; Burada her iki yapıcı fonksiyon da aday ve uygun fonksiyonlardır. Ancak "overload resolution" kuralına göre burada initilzier_list parametreli yapıcı fonksiyonun daha iyi bir dönüştürme sunduğu kabul edilmektedir. DOlayısıyla burada initializer_list parametreli yapıcı fonksiyon çağrılacaktır. Ancak standartlar küme parantezlerinin içi boşsa nesnenin "value-initialize" edileceğini belirtmektedir. Yani bu özel durumda sınıfın hem default yapıcı fonksiyonu hem de initilizer_list parametreli yapıcı fonksiyonu varsa default yapıcı fonksiyon çağrılacaktır. Örneğin: class Sample { public: Sample(); Sample(initializer_list il); //... }; Sample s{}; // burada default yapıcı fonksiyon çağrılır Burada default yapıcı fonksiyon çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 54. Ders 28/02/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir bir sınıf türünden nesneye doğrudan küme parantezleri ile değer atamak isteyebiliriz. Örneğin: VArray va{1, 2, 3, 4, 5}; //... va = {10, 20, 30}; Burada nesne initializer_list parametreli yapıcı fonksiyon ile yaratılmı sonra da ona küme parantezleri ile yeni değer atanmıştır. Her ne kadar biz henüz operatör fonksiyonlarını görmediysek de konuda bir bütünlük sağlamak amacıyla burada initializer_list parametreli atama operatör fonksiyonlarından da bahsedeceğiz. Yukarıdaki atamaya dikkat ediniz: va = {10, 20, 30}; Bunun eşdeğeri aşağıdaki gibidir: va.operator =({10, 20, 30}); O halde burada operator = fonksiyonunun paramtresi initializer_list sınıfı türünden olmalıdır. İşte eğer sınıfımızın initializer_list parametreli atama operatör fonksiyonu varsa biz o sınıf türündne nesneye küme parantezleriyle değer atayabiliriz. Daha önce yazmış olduğumuz VArray sınıfını anımsayınız. Aşağıda bu sınıf için kopya, taşıma atama operatör fonksiyonlarını ve initializer_list parametreli atama operatör fonksiyonlarını veriyoruz: class VArray { public: //... VArray &operator =(const VArray &va); VArray &operator =(VArray &&va) noexcept; VArray &operator =(std::initializer_list il); //... private: double *m_v; size_t m_size; }; VArray &VArray::operator =(const VArray &va) { if (&va == this) return *this; delete[] m_v; m_v = new double[va.m_size]; memcpy(m_v, va.m_v, sizeof(double) * va.m_size); m_size = va.m_size; return *this; } VArray &VArray::operator =(VArray &&va) noexcept { if (&va == this) return *this; delete[] m_v; m_v = va.m_v; m_size = va.m_size; va.m_v = nullptr; return *this; } VArray &VArray::operator =(initializer_list il) { delete[] m_v; m_size = il.size(); m_v = new double[m_size]; memcpy(m_v, il.begin(), m_size * sizeof(double)); return *this; } Aşağıda VArray sınıfının güncellenmiş halini veriyoruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // varray.hpp #ifndef VARRAY_HPP_ #define VARRAY_HPP_ #include #include namespace CSD { class VArray { public: VArray(); VArray(size_t size); VArray(const double *v, size_t size); VArray(std::initializer_list il); VArray(const VArray &va); VArray(VArray &&r) noexcept; ~VArray(); VArray add(const VArray &va) const; VArray add(double d) const; VArray sub(const VArray &va) const; VArray sub(double d) const; VArray mul(const VArray &va) const; VArray mul(double d) const; VArray div(const VArray &va) const; VArray div(double d) const; VArray pow(double d) const; double sum() const; double mean() const; VArray &operator =(const VArray &va); VArray &operator =(VArray &&va) noexcept; VArray &operator =(std::initializer_list il); size_t size() const { return m_size; } void disp() const; private: double *m_v; size_t m_size; }; } #endif // varray.cpp #include #include #include #include #include "varray.hpp" using namespace std; namespace CSD { VArray::VArray() : m_v(nullptr), m_size(0) {} VArray::VArray(size_t size) { m_v = new double[size]; m_size = size; } VArray::VArray(const double *v, size_t size) : VArray(size) { ::memcpy(m_v, v, sizeof(double) * size); } VArray::~VArray() { delete[] m_v; } VArray::VArray(initializer_list il) { m_v = new double[il.size()]; m_size = il.size(); size_t i = 0; for (auto val : il) m_v[i++] = val; } VArray::VArray(const VArray &va) { m_v = new double[va.m_size]; m_size = va.m_size; ::memcpy(m_v, va.m_v, sizeof(double) * m_size); } VArray::VArray(VArray &&r) noexcept { m_v = r.m_v; m_size = r.m_size; r.m_v = nullptr; } VArray VArray::add(const VArray &va) const { if (m_size != va.m_size) throw invalid_argument("Varrays must be the same size"); VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] + va.m_v[i]; return result; } VArray VArray::add(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] + d; return result; } VArray VArray::sub(const VArray &va) const { if (m_size != va.m_size) throw invalid_argument("Varrays must be the same size"); VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] - va.m_v[i]; return result; } VArray VArray::sub(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] - d; return result; } VArray VArray::mul(const VArray &va) const { if (m_size != va.m_size) throw invalid_argument("Varrays must be the same size"); VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] * va.m_v[i]; return result; } VArray VArray::mul(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] * d; return result; } VArray VArray::div(const VArray &va) const { if (m_size != va.m_size) throw invalid_argument("Varrays must be the same size"); VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] / va.m_v[i]; return result; } VArray VArray::div(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = m_v[i] / d; return result; } VArray VArray::pow(double d) const { VArray result(m_size); for (size_t i = 0; i < m_size; ++i) result.m_v[i] = ::pow(m_v[i], d); return result; } double VArray::sum() const { double total = 0; for (size_t i = 0; i < m_size; ++i) total += m_v[i]; return total; } double VArray::mean() const { return sum() / m_size; } VArray &VArray::operator =(const VArray &va) { if (&va == this) return *this; delete[] m_v; m_v = new double[va.m_size]; memcpy(m_v, va.m_v, sizeof(double) * va.m_size); m_size = va.m_size; return *this; } VArray &VArray::operator =(VArray &&va) noexcept { if (&va == this) return *this; delete[] m_v; m_v = va.m_v; m_size = va.m_size; va.m_v = nullptr; return *this; } VArray &VArray::operator =(initializer_list il) { delete[] m_v; m_size = il.size(); m_v = new double[m_size]; memcpy(m_v, il.begin(), m_size * sizeof(double)); return *this; } void VArray::disp() const { cout << '['; for (size_t i = 0; i < m_size; ++i) { cout << m_v[i]; if (i != m_size - 1) cout << " "; } cout << ']' << endl; } } // app.cpp #include #include "varray.hpp" using namespace std; using namespace CSD; int main() { VArray x{1, 2, 3, 4}; x.disp(); x = {10, 20, 30, 40, 50, 60}; // x.oerator =({10, 20, 30, 40, 50}); x.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de daha önce yazmış olduğumuz Matrix sınıfına initialzier_list parametreli yapıcı fonksiyonu ve atama operatör fonksiyonunu ekleyelim: class Matrix { public: //... Matrix(std::initializer_list> ils); Matrix &operator =(std::initializer_list> ils); //... private: double *m_matrix; size_t m_rowsize; size_t m_colsize; }; Matrix::Matrix(initializer_list> ils) { size_t colsize = 0; for (initializer_list il : ils) { if (colsize == 0) colsize = il.size(); if (colsize != il.size()) throw invalid_argument("invalid matrix dimensions"); } m_rowsize = ils.size(); m_colsize = colsize; m_matrix = new double[m_rowsize * m_colsize]; if (colsize == 0) return; for (size_t index = 0; initializer_list il : ils) { memcpy(m_matrix + index, il.begin(), m_colsize * sizeof(double)); index += m_colsize; } } Matrix &Matrix::operator =(initializer_list> ils) { if (ils.size() != m_rowsize) throw invalid_argument("matrix size mismatch"); for (initializer_list il : ils) if (il.size() != m_colsize) throw invalid_argument("matrix size mismatch"); for (size_t index = 0; initializer_list il : ils) { memcpy(m_matrix + index, il.begin(), m_colsize * sizeof(double)); index += m_colsize; } return *this; } initializer_list parametreli yapıcı fonksiyonu yazarken küme parantezleri içerisindeki küme parantezlerinin eşit sayıda elemana sahip olup olmadığını kontrol ettik. Atama işleminde atanan küme parantezli matrisin hedef matrisle aynı boyutlarda olup olmadığını da benzer biçimde kontrol ettik. Sınıfın tüm kodlarını aşağıda veriyoruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // matrix.hpp #ifndef MATRIX_HPP_ #define MATRIX_HPP_ #include #include namespace CSD { class Matrix { public: Matrix(); Matrix(size_t rowsize, size_t colsize); Matrix(const Matrix &r); Matrix(Matrix &&r) noexcept; Matrix(std::initializer_list> ils); ~Matrix(); void disp() const; Matrix add(const Matrix &r) const; Matrix sub(const Matrix &r) const; Matrix mul(const Matrix &r) const; Matrix matmul(const Matrix &r) const; Matrix div(const Matrix &r) const; double &at(size_t row, size_t col); const double &at(size_t row, size_t col) const; Matrix &operator =(const Matrix &r); Matrix &operator =(Matrix &&r); Matrix &operator =(std::initializer_list> ils); private: double *m_matrix; size_t m_rowsize; size_t m_colsize; }; } #endif // matrix.cpp #include #include #include "matrix.hpp" using namespace std; namespace CSD { Matrix::Matrix() : m_matrix(nullptr), m_rowsize(0), m_colsize(0) {} Matrix::Matrix(size_t rowsize, size_t colsize) { m_matrix = new double[rowsize * colsize]; m_rowsize = rowsize; m_colsize = colsize; } Matrix::Matrix(const Matrix &r) { m_matrix = new double[r.m_rowsize * r.m_colsize]; memcpy(m_matrix, r.m_matrix, r.m_rowsize * r.m_colsize * sizeof(double)); m_rowsize = r.m_rowsize; m_colsize = r.m_colsize; } Matrix::Matrix(Matrix &&r) noexcept { m_matrix = r.m_matrix; m_rowsize = r.m_rowsize; m_colsize = r.m_colsize; r.m_matrix = nullptr; } Matrix::Matrix(initializer_list> ils) { size_t colsize = 0; for (initializer_list il : ils) { if (colsize == 0) colsize = il.size(); if (colsize != il.size()) throw invalid_argument("invalid matrix dimensions"); } m_rowsize = ils.size(); m_colsize = colsize; m_matrix = new double[m_rowsize * m_colsize]; if (colsize == 0) return; for (size_t index = 0; initializer_list il : ils) { memcpy(m_matrix + index, il.begin(), m_colsize * sizeof(double)); index += m_colsize; } } Matrix::~Matrix() { delete[] m_matrix; } void Matrix::disp() const { for (size_t row = 0; row < m_rowsize; ++row) { for (size_t col = 0; col < m_colsize; ++col) cout << m_matrix[row * m_colsize + col] << ' '; cout << endl; } } Matrix Matrix::add(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] + r.m_matrix[i]; return result; } Matrix Matrix::sub(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] - r.m_matrix[i]; return result; } Matrix Matrix::mul(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] * r.m_matrix[i]; return result; } Matrix Matrix::matmul(const Matrix &r) const { if (m_colsize != r.m_rowsize) throw invalid_argument("matrix size mismatch"); Matrix result{m_rowsize, r.m_colsize}; double total; for (size_t i = 0; i < m_rowsize; ++i) for (size_t k = 0; k < r.m_colsize; ++k) { total = 0; for (size_t j = 0; j < m_colsize; ++j) total += m_matrix[i * m_colsize + j] * r.m_matrix[j * r.m_colsize + k]; result.m_matrix[i * result.m_colsize + k] = total; } return result; } Matrix Matrix::div(const Matrix &r) const { if (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize) throw invalid_argument("invalid matrix dimensions"); Matrix result{r.m_rowsize, r.m_colsize}; for (size_t i = 0; i < m_rowsize * m_colsize; ++i) result.m_matrix[i] = m_matrix[i] / r.m_matrix[i]; return result; } double &Matrix::at(size_t row, size_t col) { if (row >= m_rowsize || col >= m_colsize) throw invalid_argument("invalid index"); return m_matrix[row * m_colsize + col]; } const double &Matrix::at(size_t row, size_t col) const { if (row >= m_rowsize || col >= m_colsize) throw invalid_argument("invalid index"); return m_matrix[row * m_colsize + col]; } Matrix &Matrix::operator =(const Matrix &r) { if (this == &r) return *this; if ((m_rowsize != 0 && m_colsize != 0) && (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize)) throw invalid_argument("invalid matrix dimensions"); double *new_matrix = new double[r.m_rowsize * r.m_colsize]; delete[] m_matrix; memcpy(new_matrix, r.m_matrix, r.m_rowsize * r.m_colsize * sizeof(double)); m_matrix = new_matrix; m_rowsize = r.m_rowsize; m_colsize = r.m_colsize; return *this; } Matrix &Matrix::operator =(Matrix &&r) { if (this == &r) return *this; if ((m_rowsize != 0 && m_colsize != 0) && (m_rowsize != r.m_rowsize || m_colsize != r.m_colsize)) throw invalid_argument("invalid matrix dimensions"); delete[] m_matrix; m_matrix = r.m_matrix; m_rowsize = r.m_rowsize; m_colsize = r.m_colsize; r.m_matrix = nullptr; return *this; } Matrix &Matrix::operator =(initializer_list> ils) { if (ils.size() != m_rowsize) throw invalid_argument("matrix size mismatch"); for (initializer_list il : ils) if (il.size() != m_colsize) throw invalid_argument("matrix size mismatch"); for (size_t index = 0; initializer_list il : ils) { memcpy(m_matrix + index, il.begin(), m_colsize * sizeof(double)); index += m_colsize; } return *this; } } // app.cpp #include #include "matrix.hpp" using namespace std; using namespace CSD; int main() { Matrix m = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}}; m.disp(); m = {{10, 20, 30}, {40, 50, 60}, {70, 80, 90}}; m.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte initializer_list konusu dile eklenince standart kütüphanedeki bazı sınıflara bu kullanımı destekleyecek üye fonksiyonlar yerleştirildi. Örneğin string sınıfının initialzier_list parametreli yapıcı fonksiyonu ve atama operatör fonksiyonu vardır: string s{'a', 'b', 'c', 'd', 'e', 'f'}; cout << s << endl; // abcdef s = {'x', 'y', 'z'}; cout << s << endl; // xyz Küme parantezleriyle değer verilmesi durumunda initializer_list parametreli fonksiyonların diğer fonksiyonlardan daha iyi dönüştürme sağladığını anımsayınız. Örneğin: string s{10, 'a'}; Burada 10 tane 'a' karakterinden oluşan string elde edilmeyecektir. Çünkü burada initializer_list parametreli yapıcı fonksyon çağrılacaktır. Dolayısıyla buradaki stringin iki elemanı olacak birinci eleman 10 numaralı karakterden ('\n' karakteri) ikinci eleman 'a' karakterinden oluşacaktır. Tabii biz bu tür durumlarda küme parantezleri yerine normal parantezleri kullanırsak bu durumda initializer_list parametreli fonksiyonlar zaten uygun fonksiyonlar olmayacaktır.Örneğin: string s(10, 'a'); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { string s{'a', 'b', 'c', 'd', 'e', 'f'}; cout << s << endl; s = {'x', 'y', 'z'}; cout << s << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Veri yapıları dünyasında dinamik bir biçimde büyütülen dizilere "dinamik diziler (dynamic array)" denilmektedir. Dinamik dizilerin gerçekleştirimi tipik olarak şöyle yapılmaktadır: Önce dizi için makul birt uzunlukta dinamik tahsisat yapılır. Dizi için tahsis edilen alanın uzunluğuna "kapasite (capacity)" denilmektedir. Dinamik dizi içerisindeki eleman sayısı da yine tutulmaktadır. Buna İngilizce "size" ya da "count" denilebilmektedir. Dinamik diziye eleman eklenirken eleman sona eklenir. size değeri 1 artırılır. size değeri kapasite değerine eriştiğinde yeniden tahsisat (reallocation) yapılarak dizi büyütülür. Büyütme genellikle "öncekinin iki katı" olacak biçimde yapılmaktadır. Bu durum eleman ekleme sırasında yeniden tahsisat gibi zaman alıcı bir işlemin logaritmik düzeye indirgenmesine yol açmaktadır. Bu tür sistemleri eleman ekelemerin karmaşıklığına İngilizce "amortized constant time" denilmektedir. Dinamik dizilerde elemanlar yine ardışıl olarak saklanır. Dolayısıyla elemana erişim indeks yoluyla çok hızlı (random access) biçimde yapılır. Dinamik dizilerin sayısı baştan bilinmeyen, duruma göre değişebilen elemanların ardışıl bir biçimde saklanmasının gerektiği durumlarda kullanılmaktadır. Örneğin bir dizindeki (directory) dosyaları bir diziye yerleştirecek olalım. Biz dizinde kaç tane dosya olduğunu baştan bilmeyiz. Dolayısıyla onları saklamak için dinamik dizi kullanabiliriz. Örneğin bir text dosyada belirli bir sözcüğün geçtiği satırların numaralarını bir dizide saklayacak olalım. Biz baştan o sözcüğün kaç satırda geçtiğini bilmemekteyiz. Bu tür durumlarda durumda dinamik dizilerden faydalanabiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Dinamik diziler C++ standart kütüphanesinde vector isimli bir sınıf şablonuyla temsil edilmiştir. Dolayısıyla programcının dinamik dizileri kendisinin oluşturmasına gerek yoktur. Zaten vector sınıfı tamamen bu gereksinimi etkin bir biçimde karşılamak amacıyla bulunduurlmuştur. vector sınıfı şablon bir sınıf olduğu için vector nesnesinin tutacı elemanların türü nesne yaratılırken açısal parantezler içerisinde belirtilmelidir. Örneğin: vector x; vector y; Burada x nesnesi int değerleri, y nesnesi ise string değerlerini tutabilecek biçimdedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 55. Ders 04/03/2024 - Pazartesi /*------------------------------------------------------------------------------------------------------------------------------------------------------------- /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir vector nesnesi vector sınıfının çeşitli yapıcı fonksiyonlarıyla yaratılabilmektedir. - Sınıfın default yapıcı fonksiyonu boş bir vector nesnesi yaratmaktadır. Örneğin: vector v; - Sınıfın diğer yapıcı fonksiyonu belli bir miktarda nesneden vector oluşturmaktadır. Bu yapıcı fonksiyonun birinci parametresi vector'e başlangıçta kaç elemanın yerleştirileceğini, ikinci parametresi de bu elemanların hangi değere sahip olacağını belirtmektedir. Örneğin: vector v(10, 100); Burada her biri 100 değerini içeren 10 tane elemandan oluşan bir vector yaratılmıştır. Burada eğer vector elemanları bir sınıf türündense yaratılacak vector elemanları kopya yapıcı fonksiyonu ile yaratılmaktadır. Örneğin: vector v(10, Sample()); Fonksiyonun ikinci parametresi default değer almıştır. Bu parametre girilmezse ilgili türden default değerlerle elemanlar yaratılır. (Yani temel türler için, 0 değerleriyle, sınıf türleri için ilgili sınıfın ydefault yapıcı fonksiyonlarıyla) Burada 10 tane vector elemanı için Sample sınıfının kopya yapıcı fonksiyonu çalıştırılacaktır. - Sınıfın belli sayıda elemanını default değerlerle (value-initialize biçiminde) yaratan bir yapıcı fonksiyon da vardır. Örneğin: vector v(10); Burada 10 tane elemandna oluşan bir vector nesnesi yaratılmıştır. - vector sınıfının initializer_list parametreli yapıcı fonksiyonu da vardır. Bu sayede biz vector nesnesine doğrudan küme parantezleri ile ilkdeğer verebilriz. Bu ilkdeğerler vector elemanlarının değerlerini temsil etmektedir. Örneğin: vector v = {10, 20, 30, 40, 50}; Burada vector int türden olduğu için ilgili yapıcı fonksiyoun parametresi de initializer_list türünden olmaktadır. - vector sınıfın kopya yapıcı fonksiyonu taşıma yapıcı fonksiyonu da bulunmaktadır. Örneğin: vector foo() { vector v{1, 2, 3, 4, 5}; return v; } //... vector v; //... v = foo(); Burada foo fonksiyonu çağrıldığında "copy elision" derleyeciye isteğine bağlı olarak (optional) yaılabilir de yapılmayabilir de. Ancak ""copy elision" yapılmayacaksa geri dönüş değeri için vector sınıfının taşıma yapıcı fonksiyonu çağrılacaktır. vector sınıfının taşıma atama operatör fonksiyonu da olduğu için geri dönüş değerinin atanmasında da önemli bir performas kaybı oluşmayacaktır. vector sınıfının initializer_list yapıcı fonksiyonun olması nedeniyle vector nesnesi yaratırken normal parantez ya da küme parantezi kullanımına dikkat ediniz. Örneğin: vector v(10, 100); Burada her elemanı 100 değerini içeren 10 elemanlı bir vector nesnesi yaratılmıştır. Oysa örneğin: vector v{10, 100}; Burada 10 ve 100 değerine sahip iki elemanlı vector nesnesi yaratılacaktır. Overload resolution işleminde initilizer_list parametreli fonksiyonun daha iyi dönüşüm sağlayacağına dikkat ediniz. vector nesnesi sınıf türünden nesneleri tutuyorsa vector nesnesinin ömrü bittiğinde vector seınıfının yıkıcı fonksiyonu tüm vector elemanları için yıkıcı fonksiyonları çağıracaktır. Aşağıda vector sınıfının yapıcı fonksiyonlarının kullanımına ilişkin bir örnek verilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Sample { public: Sample() : m_val(0) {} Sample(int val) : m_val(val) { cout << "Sample constructor" << endl; } Sample(const Sample &r) : m_val(r.m_val) { cout << "copy constructor" << endl; } ~Sample() { cout << "destructor" << endl; } int val() const { return m_val; } private: int m_val; }; int main() { vector a(10, 100); for (int x : a) cout << x << " "; cout << endl; vector b = {10, 20, 30, 40, 50}; for (int x : b) cout << x << " "; cout << endl; Sample s{10}; vector c(10, s); for (Sample &x : c) cout << x.val() << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector nesnesi içerisindeki elemanların sayısı size üye fonksiyonu ile vector nesnesi için ayrılan kapasitenin o anki değeri de capacity üye fonksiyonu ile elde edilmekltedir. Bu üye fonksiyonlar vector::size_type (yani vector sınıfının içerisinde bildirilmiş olan size_type türü) türündne değer vermektedir. C++ standartları bu size_type türünün "işaretsiz bir tamsayı türü olmak koşulu ile herhangi bir tür olarak typedef edilebileceğini" söylemektedir. Tipik olarak bu tür size_t türü biçiminde typedef edilmektedir. Örneğin: vector v = {10, 20, 30, 40, 50}; cout << v.size() << endl; cout << v.capacity() << endl; C++ standartları capacity değeri hakkında bir koşul belirtmemektedir. Yani yukarıdaki kodda capacity değeri 5 olmak zorunda değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector'ün sonuna eleman eklemek için push_back fonksiyonu kullanılmaktadır. Eskidne beri var olan push_back fonksiyonu const T & parametreli fonksiyondu. Ancak C++11 ile birlikte T && parametreli push_back fonksiyonu da eklenmiştir. Yani eklenmek istenen nesne bir sağ taraf değeri ise o nesne taşınarak eklenmektedir. Başka bir deyişle eklenecek değer eğer bir sınıf türünden sol taraf değeri belirten bir nesne ise vector'e eklenen nesne kopya yapıcı fonksiyonu yoluyla, sağ taraf türünden bir nesneyse taşıma yapıcı fonksiyonu yoluyla yaratılmaktadır. Aşağıdaki örnekte ne kastedildiği daha iyi anlaşılabilir. vector sınıfı kapasiteyi büyütürken eski tahsis ettiği yerdeki nesneleri yeni tahsis ettiği alana da yine sınıfın kopya yapıcı fonksiyonu ile kopyalamaktadır. Bu nedenle aşağıdaki kodda fazladan kopya yapıcı fonksiyonların ve yıkıcı fonksiyonların çalıştırıldığını görürseniz şaşırmayınız. Örneğin Microsoft derleyicilerinde aşağıdaki gibi bir çıktı elde edilmiştir: Sample int constructor copy constructor move constructor copy constructor destructor destructor destructor destructor --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Sample { public: Sample() : m_val(0) { cout << "Sample default constructor" << endl; } Sample(int val) : m_val(val) { cout << "Sample int constructor" << endl; } Sample(const Sample &r) : m_val(r.m_val) { cout << "copy constructor" << endl; } Sample(const Sample &&r) noexcept : m_val(r.m_val) { cout << "move constructor" << endl; } ~Sample() { cout << "destructor" << endl; } int val() const { return m_val; } private: int m_val; }; int main() { vector v; Sample s{10}; v.push_back(s); // ekleme kopya yapıcı fonksiyonu yoluyla yaılmaktadır v.push_back(move(s)); // ekleme taşıma yapıcı fonksiyonu yoluyla yazılmaktadır return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte vector sınıfına vektöre ekleme yapan emplace_back isimli yeni bir üye fonksiyon daha eklenmiştir. Bu fonksiyon eğer vector'ün şablon parametresi bir sınıf türündense doğrudan o sınıfın yapıcı fonksiyona geçirilecek argümanlarını alır ve eklenecek elemanı tek bir yapıcı fonksiyon çağırarak hedefte o argümanlarla yaratır. Örneğin: vector v; Sample s(10, 20); v.push_back(s); Burada ekleme işlemi iki yapıcı fonksiyonun çağrılması ile gerçekleşecektir. Birinci yapıcı fonksiyon s nesnesinin yaratılması sırasında çağrılan yapıcı fonksiyondur. İkinci yapıcı fonksiyon eklenecek vector elemanı için çağrılan kopya yapıcı fonksiyonudur. Eklemenin aşağıdaki gibi yapılırsa kopya yapıcı fonksiyonu yerine taşıma yapıcı fonksiyonu çağrılır. Ancak yine toplamda iki yapıcı fonksiyon çağrılmış olur: vector v; v.push_back(Sample(10, 20)); Ancak bu işlemi emplace_back i,le yaparsak yalnızca tek bir yapıcı fonksiyon çağrılacaktır: vector v; v.emplace_back(10, 20); emplace_back fonksiyonuna doğrudan Sample sınıfının yapıcı fonksiyonun argümanlarının aktarıldığına dikkat ediniz. emplace_back bu argümanları alarak tek seferde eklenecek vector elemanını bu değerlerle constryct edecektir. Tabii vector'ün şablon parametresi sınıf türünden değilse push_back ile emplace_back arasında bir farklılık kalmamaktadır. Pekiyi emplace_back fonksiyonu neden C++11 ile eklenmiştir? İşte bu fonksiyonun gerçekleştirilebilmesi için dilde "variadic template" ve "forwarding reference" denilen özelliklerin bulunuyor olması gerekmektedir. Bu özellikler de C++11 ile dile eklenmiştir. Aşağıdaki örnekte push bakc ile emplace_back arasındaki farklılık açıkça görülmektedir. Bu örneği çalıştırdığınızda aşağıdaki gibi bir çıktı elde edeceksiniz: Sample int constructor move constructor ------------ Sample int constructor --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) { cout << "Sample int constructor" << endl; } Sample(const Sample &r) : m_a(r.m_a), m_b(r.m_b) { cout << "copy constructor" << endl; } Sample(const Sample &&r) noexcept : m_a(r.m_a), m_b(r.m_b) { cout << "move constructor" << endl; } private: int m_a; int m_b; }; int main() { vector x; x.push_back(Sample(10, 20)); cout << "------------" << endl; vector y; y.emplace_back(10, 20); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector nesnesinin arasında eleman eklemek için insert üye fonksiyonları kullanılmaktadır. Ancak bu fonksiyonlar insert pozisyonu olarak "iterator" almaktadır. vector sınıfının iterator'leri "rastgele erişimli (random access)" iterator'ler olduğu için insert pozisyonu olarak "v.begin() + n" ifadesini geçirmelisiniz. Iterator kavramı ileri ele alınacaktır. Dolayısıyla burada daha fazla açıklama yapmayacağız. Örneğin: vector v = {10, 20, 30, 40, 50}; Burada biz 30 ie 40 arasına 100 değerini insert etmek isteyelim. Bunu şöyle yapabiliriz: v.insert(v.begin() + 2, 100); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v = {10, 20, 30, 40, 50}; for (int x : v) cout << x << " "; cout << endl; v.insert(v.begin() + 3, 100); for (int x : v) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Eğer vector'ün şablon parametresi bir sınıf türündense budurumda insert işleminde kaydırma yapılırken ilgili sınıfın taşıma atama operatör fonksiyonu (yoksa kopya atama operatör fonksiyonu) çağrılacaktır. Yani bu tür durumlarda yeniden tahsisat yapılırken ya da kaydırma yapılırken aslında sınıfların yapıcı ve atama operatör fonksiyonlarının çalıştırılmaktadır. Tabii programcılar genellikle bu durumla ilgilenmezler. Çünkü sınıfları zaten düzgün yazılmışsa kopyalama ve atama işlemleri herhangi bir soruna yol açmayacaktır. Aşağıda bu durumu betimlemek için bir örnek veriyoruz. Bu örnekten amacımız insert işlemi sırasında nesnelerin yer değiştirmesi dolayısıyla ilgili sınıfların yapıcı ya da atama operatör fonksiyonlarının çalıştırılabileceğinin gösterilmesidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Sample { public: Sample(int a) : m_a(a) { cout << "Sample int constructor" << endl; } Sample(const Sample &r) : m_a(r.m_a) { cout << "copy constructor" << endl; } Sample(const Sample &&r) noexcept : m_a(r.m_a) { cout << "move constructor" << endl; } Sample & operator =(const Sample &r) { m_a = r.m_a; cout << "copy assignment operator" << endl; return *this; } Sample & operator =(const Sample &&r) { m_a = r.m_a; cout << "move assignment operator" << endl; return *this; } int a() const { return m_a; } private: int m_a; }; int main() { vector v = {Sample(10), Sample(20), Sample(30), Sample(40), Sample(50)}; for (Sample &s : v) cout << s.a() << " "; cout << endl; cout << "------------------------" << endl; v.insert(v.begin() + 2, Sample(100)); for (Sample &s : v) cout << s.a() << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfına C++11 ile birlikte initializer_list parametreli bir insert fonksiyonu da eklenmiştir. Bu fonksiyon birden fazla değerin insert edilmesi için kullanılabilir. Örneğin: vector v = {10, 20, 30, 40, 50}; v.insert(v.begin() + 2, {100, 200, 300, 400, 500}); Burada tek hamlede küme parantezlerinin içerisindeki tüm değerler insert edilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v = {10, 20, 30, 40, 50}; for (int x : v) cout << x << " "; cout << endl; cout << "--------------" << endl; v.insert(v.begin() + 2, {100, 200, 300, 400, 500}); for (int x : v) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte insert işleminin hedefte gerçekleştirilmesi için emplace fonksiyonu da sınıfa eklenmiştir. Örneğin: vector v; //... v.emplace(v.begin(), 100); Burada emplace fonksiyonunun ikinci ve sonraki parametreleri yine yapıcı fonksiyona geçirilecek olan argümanları almaktadır. Fonksiyon doğrudan inset edilecek noktada burada belirtilen parametreik yapıya sahip yapıcı fonksiyonu çağırır. Aşağıdaki programla test işlemi yapabilirsiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Sample { public: Sample(int a) : m_a(a) { cout << "Sample int constructor" << endl; } Sample(const Sample &r) : m_a(r.m_a) { cout << "copy constructor" << endl; } Sample(const Sample &&r) noexcept : m_a(r.m_a) { cout << "move constructor" << endl; } Sample & operator =(const Sample &r) { m_a = r.m_a; cout << "copy assignment operator" << endl; return *this; } Sample & operator =(const Sample &&r) { m_a = r.m_a; cout << "move assignment operator" << endl; return *this; } int a() const { return m_a; } private: int m_a; }; int main() { vector v = {Sample(10), Sample(20), Sample(30), Sample(40), Sample(50)}; for (Sample &s : v) cout << s.a() << " "; cout << endl; cout << "------------------------" << endl; v.emplace(v.begin() + 2, 100); for (Sample &s : v) cout << s.a() << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 56. Ders 06/03/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfının clear üye fonksiyonu vector elemanlarını tamamen silmek için kullanılmaktadır. Yani clear fonksiyonu çağrıldığında artık size() fonksiyonu 0 ile geri dönecektir. clear ile vector elemanları silindiğinde kapasitede herhangi bir değişiklik yapılmamaktadır. Aşağıdaki örnekte vektör nesnesi üzerinde clear fonksiyon u uygulanmıştır. Bu işlem sonrasında caapcity değerinin değişmediğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ using namespace std; int main() { vector v{10, 20, 30, 40, 50}; for (auto x : v) cout << x << " "; cout << endl; cout << "size: " << v.size() << " capacity: " << v.capacity() << endl; v.clear(); cout << "size: " << v.size() << " capacity: " << v.capacity() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi vector'lerde elemana erişim O(1) karmaşıklıktadır. Yani dizi elemanlarına erişim gibidir. Vector elemanlarına erişmek için [] operatör fonksiyonu kullanılabilir. [] operatörü ile vector elemanlarına erişilirken herhangi bir sınır kontrolü yapılmamaktadır. Yani bu bakımdan dizi elemanlarına erişimden bir farkı yoktur. Tabii [] operatörü ile biz vektör elemanlarını da değiştirebiliriz. [] operatör fonksiyonunun const versiyonu da vardır. const vector nesneleriyle [] operatörü kullanıldığında artık elemanları değiştiremeyiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{10, 20, 30, 40, 50}; for (decltype(v)::size_type i = 0; i < v.size(); ++i) cout << v[i] << " "; cout << endl; for (decltype(v)::size_type i = 0; i < v.size(); ++i) v[i] = i * i; for (decltype(v)::size_type i = 0; i < v.size(); ++i) cout << v[i] << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- at üye fonksiyonu tamamen [] operatör fonksiyonu gibidir. Tek farkı sınır kontrolü uygulamasıdır. Yani fonksiyon erişilen indeksin 0 ile size() arasında olup olmadığına bakar. Bu aralıkta olmayan erişimlerde out_of_range exception'ı fırlatılır. Tabii at üye fonksiyonunun da sont olan ve const olmayan versiyonu vardır. const olmayan at üye fonksiyonu vector içerisindeki ilgili indekste bulunan değerin referansı ile const olan at üye fonksiyonu ise ilgili indekste bulunan değerin const referansı ile geri dönmekteri. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{10, 20, 30, 40, 50}; for (decltype(v)::size_type i = 0; i < v.size(); ++i) cout << v.at(i) << " "; cout << endl; for (decltype(v)::size_type i = 0; i < v.size(); ++i) v.at(i) = i * i; for (decltype(v)::size_type i = 0; i < v.size(); ++i) cout << v.at(i) << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfının front ve at isimli üye fonksiyonları ilk elemanın ve son elemanın referanslarına geir dönmektedir. Yani eğer bir özel olarak ilk ve son elemanlar üzerinde işlem yapacaksak doğrudan bu üye fonksiyonları kullanabiliriz. Tabii bunların const versiyonları da vardır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{10, 20, 30, 40, 50}; cout << v.back() << endl; v.back() = 100; for (auto x : v) cout << x << " "; cout << endl; cout << v.front() << endl; v.front() = 100; for (auto x : v) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfının data isiml üye fonksiyonu vektörün içerisindeki dinmaik dizinin başlangıç adresini vermektedir. Böylece elimizde bir vector varsa onu C tarzı dizi gibi de kullanabiliriz. Örneğin: int getmax(const int *pi, size_t size); //... vector v{34, 23, 12, 34, 10, 29, 12, 89, 34, 11}; int result; retsult = getmax(v.data(), v.size()); data üye fonksiyonunun const olan bir biçimi de vardır. const vector nesneleriyle data fonksiyonu çağrıldığında biz vektörün içerisindeki dizinin adresini const bir adred biçiminde elde ederiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int getmax(const int *pi, size_t size) { int max = pi[0]; for (size_t i = 1; i < size; ++i) if (max < pi[i]) max = pi[i]; return max; } int main() { vector v{34, 23, 12, 34, 10, 29, 12, 89, 34, 11}; auto result = getmax(v.data(), v.size()); cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfının empty isimli üye fonksiyonu "vector nesnesi boş mu" kontrolünü yapmaktadır. Fonksiyon bool bir değere geri dönmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{34, 23, 12, 34, 10, 29, 12, 89, 34, 11}; cout << (v.empty() ? "empty" : "not empty") << endl; // not empty v.clear(); cout << (v.empty() ? "empty" : "not empty") << endl; // empty return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfının reserve isimli üye fonksiyonu kapasite değerini büyütmek kullanılmaktadır. Eğer fonksiyona argüman olarak girilen yeni kapasite değeri vector nesnesinin o anki kapasitesinden büyük değilse fonksiyon hiçbir şey yapmaz. Bazen programcının vector'e yterleştirilecek elemanların minimum sayısı hakkında bir öngürüsü olabilir. Bu durumda capacity'nin çeşitli zamanlarda otomatik artırılması göreli bir zaman kaybı oluşturabilmektedir. İşte bu tür durumlarda kapasite başta belli bir büyüklükte alınabilir. Örneğin: vector v; v.reserve(100); //... --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v; v.reserve(100); cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfının shrink_to_fit isimli üye fonksiyonu capacity değerini size değerine çekerek tahsis edilmiş olan fazladan alanı geri bırakmak için kullanılmaktadır. Ancak standartlara göre bu istek bir "emir" değil "rica" niteliğindedir. Yani bu fonksiyon çağrıldıktan sonra capacity değeri size değerine eşit olmak zorund değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{10, 20, 30, 40, 50}; v.reserve(100); cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl; v.shrink_to_fit(); cout << "size: " << v.size() << ", capacity: " << v.capacity() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfının pop_back fonksiyonu vektor'ün son elemanını atmakta kullanılmaktadır. Tabii bu işlem O(1) karmaşıklıkta yapılmaktadır. Aşağıdaki örnekte vector'ün son elemanı tek tek alınmış ve yazdırılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{10, 20, 30, 40, 50}; while (!v.empty()) { auto val = v.back(); cout << val << endl; v.pop_back(); } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- resize üye fonksiyonu vektor'de bulunan eleman sayısını artırmak için ya da azaltmak için kullanılmaktadır. Eleman azaltılırken sondaki elemanlar atılmaktadır. Eleman artırılırken default durumda artırılmış elemanlar default değerlerle (şablon parametresi temel türdense 0, sınıf türündense default yapıcı fonksiyon ile) doldurulur. Fonksiyonun iki overload biçimi vardır. İki parametreli overload biçimi doldurma değerini de parametre olarak almaktadır. Fonksiyonun capacity değeri ile bir ilgisi yoktur. size artırılırken capacity değeri de en az size kadar artırılmaktadır. size düşürülürken capacity değeri düşürülmemektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{10, 20, 30, 40, 50}; cout << "size: " << v.size() << " capacity: " << v.capacity() << endl;; for (auto x : v) cout << x << " "; cout << endl; v.resize(10); cout << "size: " << v.size() << " capacity: " << v.capacity() << endl; for (auto x : v) cout << x << " "; cout << endl; v.resize(3); cout << "size: " << v.size() << " capacity: " << v.capacity() << endl; for (auto x : v) cout << x << " "; cout << endl; v.resize(10, 100); cout << "size: " << v.size() << " capacity: " << v.capacity() << endl; for (auto x : v) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın swap üye fonksiyonu iki nesnenin veri elemanlarını yer değiştirir. Bu yer değiştirme vector içerisindeki tahsis edilmiş olan dizi elemanlarının yer değiştirmesi anlamına gelmemektedir. Yalnızca tahsis edilmiş olan dizilerin adreslerinin turulduğu göstericiler ve size ile capacity değerleri yer değiştmektedir. Aynı işlem başlık dosyasındaki global swap fonksiyonu ile de yapılabilmektedir. Aşağıdaki örnekte bu durum açıkça gözürlebilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{10, 20, 30, 40, 50}; vector k{100, 200, 300}; cout << "v.size(): " << v.size() << " v.capacity(): " << v.capacity() << " v.data(): " << v.data() << endl; cout << "k.size(): " << k.size() << " k.capacity(): " << k.capacity() << " k.data(): " << k.data() << endl; v.swap(k); cout << "v.size(): " << v.size() << " v.capacity(): " << v.capacity() << " v.data(): " << v.data() << endl; cout << "k.size(): " << k.size() << " k.capacity(): " << k.capacity() << " k.data(): " << k.data() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- vector sınıfının 6 karşılaştırma operatörü için de karşılaştırma operatör fonksiyonu bulunuyordu. Ancak C++20 ile birlikte <, <=, >, <= ve != operatörleri kaldırılmış (== operatörü kaldırılmamıştır) bunların yerine "üç yönlü karşılaştırma operatörü" eklenmiştir. Karşılaştırmalar "leksikografik" biçimde yapılmaktadır. Yani elamanlar eşit olduğu sürece devam edilir. İlk eşit olmayan elemanların durumuna bakılır. İki vector nesnesinin karşılaştırılabilmesi için şablon parametrelerinin aynı olması gerekir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { vector v{10, 20, 100}; vector k{10, 20, 100, 40, 50}; if ((v <=> k) > 0) cout << "v > k" << endl; else if ((v <=> k) < 0) cout << "v < k" << endl; else cout << "v == k" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tabii bu operatör fonksiyonlarının kullanılabilmesi için vector elemanlarının karşılaştırılabilir olması gerekmektedir. Örneğin string nesneleri karşılaştırılabilir olduğuna göre bir vector nesnelerini karşılaştırabiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { vector v{"ali", "veli", "selami", "ayse", "fatma"}; vector k{"ali", "veli", "selami", "aysel", "fatma"}; if ((v <=> k) > 0) cout << "v > k" << endl; else if ((v <=> k) < 0) cout << "v < k" << endl; else cout << "v == k" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 57. Ders 11/03/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Dilin sektaksıyla desteklenen "built-in" dizilerin dilden kaynaklanan bazı sıkıntıları vardır. Bunları şöyle sıralayabiliriz: - Dizilere tanımlamadan sonra küme parantezleri ile değer atanamamaktadır. Örneğin: int a[5]; //... a = {10, 20, 30, 40, 50}; // geçersiz! - Dizi uzunlukları diziye dayalı biçimde otomatik elde edilememektedir. Örneğin: int a[5]; Burada a dizisinin uzunluğunu programcı kendisi sabit ifadeleriyle belirtmiştir. Bu uzunluğu kullanacaksa yine sabit ifadesiyle bunu belirtir. Örneğin: foo(a, 5); Burada programcının dizi uzunluğunu aklında tutması gerekmektedir. Tabii aslında diziler için uzunluğun elde edilmesi a dizi ismi olmak üzere sizeof(a) / sizeof(*a) ifadesiyle elde edilebilir. Ancak bu kullanım biraz zahmetlidir. Örneğin: foo(a, sizeof(a) / sizeof(*a)); - Dizi elemanlarına erişirken herhangi bir sınır kontrolü yapılmamaktadır. Tabii aslında bu bir kusur değildir. Çünkü C/C++ gibi daha düşük seviyeli dillerde dizi taşmalarının derleyici tarafından kontrol edilmesi derleyicinin ek birtakım kontrol kodlarının koda yerleştirilmesiyle mümkün olabilmektedir. Bu da bu seviyedeki programlama dillerinde istenen bir durum değildir. Ancak yine de programcı istediği zaman bu kontrolü yapabilseydi daha esnek bir durum oluşurdu. İşte C++11 ile birlikte C++'ın standart kütüphanesine array isimli şablon bir sınıf eklenmiştir. Bu array sınıfı built-in dizilerin etkinliğine sahip olmakla beraber programcıya yukarıda belirttiğimiz bazı kolaylıkları da sunmaktadır. array sınıfının bildirimi başlık dosyası içerisindedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir array nesnesi iki şablon parametresi belirtilerek tanımlanmak zorundadır. İlk şablon parametresi dizinin türünü belirtir. İkinci şablon parametresi ise dizinin uzunluğunu belirtmektedir. Bu uzunluğun sabit ifadesi olması zorunludur. Örneğin: array a; array b; Array nesnesi sınıfın default yapıcı fonksiyonu ile (yukarıdaki gibi) yaratılabileceği gibi küme parantezleriyle ilkdeğer verilerek de yaratılabilir. Örneğin: array a = {10, 20, 30, 40, 50}; array b{10, 20, 30}; Yine bu biçimdeki dizi nesnelerinin az sayıda elemanına ilkdeğer verilebilir. Bu durumda diğer elemanlar dizi temel türlere ilişkinse sıfırlanmakta, sınıf türlerine ilişkinse onların default yapıcı fonksiyonu varsa onlar çağrılmaktadır (value-initiallize işlemi). Eğer array nesnesine ilkdeğer verilmezse bu durumda nesne temel türlere ilişkin bir dizi tutuyorsa o dizinin elemanlarında çöp değerler, sınıf türünden bir dizi tutuyorsa o sınıfın default yapıcı fonksiyonun çağrılması sonucunda oluşan değerler bulunacaktır. Örneğin: array a; for (int x : a) // dikkat çöp değerler! cout << x << " "; cout << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- array nesnesinin elemanlarına yine [] operatörü ile erişilebilir. Sınıfın [] operatör fonksiyonu constexpr bir fonksiyondur. Yani aynı zamanda inline bir fonksiyondur. Derleyiciler bu durumda inline açım yapabilecekleri için erişim sırasında muhtemelen bir zaman kaybı oluşmayacaktır. Tabii [] erişiminde bir sınır kontrolü yapılmamaktadır. [] operatör fonksiyonu constexpr bir fonksiyon olduğu için eğer array nesnesine sabit ifadeleriyle ilkdeğer verilmişse bu operatör fonksiyonu bize sabit ifadesi verecektir. (Tabii bu durumda sınıf nesnesinin de constexpr olması gerekir.) Örneğin: constexpr array a = {1, 2, 3, 4, 5}; int x = a[3]; cout << x << endl; // cout << a.operator =(3) << endl; int b[a[2]] = {10, 20, 30}; // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tıpkı vector sınıfında olduğu gibi elemana erişmek için [] operatör fonskiyonunun yanı sıra aynı zamanda at isimli bir üye fonksiyon a bulunmaktadır. Yine bu sınııfn at üye fonksiyonu da erişimde sınır kontrolü uygulamaktadır. Eğer sınıf taşması söz konusu olursa out_of_range sınıfı ile throw edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- array sınıfının iteratör desteği olduğu için sınıf aralık tabanlı for döngülerinde kullanılabilir. Örneğin: array a = {10, 20, 30, 40, 50}; for (int x : a) cout << x << " "; cout << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { array a = {10, 20, 30, 40, 50}; for (int x : a) cout << x << " "; cout << endl; for (int &r : a) r *= r; for (int x : a) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- array nesneneleri ile belirtilen dizilerin uzunlukları size üye fonksiyonu ile elde edilebilir. size üye fonksiyonu const ve constexpr bir fonksiyondur. Örneğin: array a = {10, 20, 30, 40, 50}; for (size_t i = 0; i < a.size(); ++i) cout << a[i] << " "; cout << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { array a = {10, 20, 30, 40, 50}; for (size_t i = 0; i < a.size(); ++i) cout << a[i] << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın data isimli üye fonksiyonu nesnenin içerisindeki dizinin adresini bize verir. Yani biz array nesnesini C tarzı bir fonksiyona geçirmek istersek onun tuttuğu dizinin adresini elde edebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int getmax(int *pi, size_t size) { int max = pi[0]; for (size_t i = 1; i < size; ++i) if (pi[i] > max) max = pi[i]; return max; } int main() { array a = {10, 20, 30, 40, 50}; int max; max = getmax(a.data(), a.size()); cout << max << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tıpkı vector sınıfında olduğu gibi iki array nesnesini lexicogrpahic olarak karşılaştıran karşılaştırma operatör fonksiyonları bulunmaktadır. Ancak <, >, <= ve >= operatör fonksiyonları C++20'de kaldırılmış yerine üç yönlü karşılaştırma operatör fonksiyonu eklenmiştir. == operatör fonksiyonu kaldırılmamıştır. Dolayısıyla C++20 sonrasında da sınıfta bulunmaya devam etmektedir. Karşılaştırmanın yapılabilmesi içib array nesnelerinin aynı türden olması gerekmektedir. Burada aynı türden demekle ""hem dizi türlerinin hem de uzunluklarının aynı olmasını" katsediyoruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { array a = {10, 20, 30, 40, 50}; array b = {10, 20, 29, 100, 200}; if ((a <=> b) > 0) cout << "a > b" << endl; else if ((a <=> b) < 0) cout << "a < b" << endl; else cout << "a == b" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir array nesnesine daha sonra küme parantezleriyle atama yapılabilir. (Built-in dizilerde böyle bir şey yapamadığımızı anımsayınız.) Örneğin;: array a = {10, 20, 30, 40, 50}; for (auto x : a) cout << x << " "; cout << endl; a = {1, 2, 3}; Burada a = {1, 2, 3} atamasında dizinin yalnızca ilk üç elemanına atama yapılmamaktadır. Tüm elemanlarına atama yapılmaktadır. Dolayıısyla geri kalan elemanlar 0 biçiminde atanacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { array a = {10, 20, 30, 40, 50}; for (auto x : a) cout << x << " "; cout << endl; a = {1, 2, 3}; for (auto x : a) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Burada bir noktaya dikkatinizi çekmek istiyoruz. array türünden bir nesneye küme parantezleriyle ilkdeğer verebilmemizin ve küme parantezleriyle atama yapabilmemizin nedeni initilizer_list parametreli yapıcı fonksiyon ve atama operatör fonksiyonunun bulunmasından değildir. Aslında array sınıfının hiç yapıcı fonksiyonu ve atama operatör fonksiyonu yoktutr. Dolaısıyla kopya yapıcı fonksiyon ve kopya atama operatör fonksiyonu derleyici tarafından yazılmaktadır. Bu ilkeğer verme ve atama "aggregate class" özelliğinden dolayı yapılabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tabii array sınıfının tuttuğu dizi temel türlerden olmak zorunda değildir. Herhangi bir sınıf türünden de olabilir. Örneğin: array names = {"ali", "veli", "selami", "ayse", "fatma"}; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { array names = {"ali", "veli", "selami", "ayse", "fatma"}; for (string &name : names) cout << name << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın fill isimli üye fonksiyonu sınıfın tuttuğu diziyi belli bir değerle doldurmak için kullanılmaktadır. Örneğin: array a; a.fill(10); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { array a; a.fill(10); for (auto x : a) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- başlık dosyasında ayrıca to_array isimli bir şablon fonksiyon da bulunmaktadır. Bu fonksiyon built-in dizileri array türüne dönüştürmektedir. Yani biz fonksiyona buil-in dizi veririz, fonksiyon da bize ilgili türdne ve uzunlukta bir array nesnesi verir. Tabii bu dizi ile yeni yaratılan nesnenin bir ilgisi yoktur. Yani kopyalaa yluyla yaratım söz konusudur. Örneğin: int a[] = {10, 20, 30, 40, 50}; array b = to_array(a); Tabii burada auto tür belirleyicisini yazımı kolaylaştırmak için kullanabilirdik: auto b = to_array(a); Tabii aynı türden iki array nesnesi birbirine atanabileceğine göre bu işlemi şöyle de yapabilirdik: int a[] = {10, 20, 30, 40, 50}; array b; //... b = to_array(a); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int a[] = {10, 20, 30, 40, 50}; auto b = to_array(a); a[0] = 100; for (size_t i = 0; i < b.size(); ++i) cout << b[i] << " "; // 10 20 30 40 50 cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önce de belirttiğimiz gibi bir proje nesne yönelimli olarak modellenecekse önce projedeki kavramlar sınıflarla temsil edilir. Sonra bu kavramlar türünden gerçek nesneler yaratılır ve program bu nesneler kullanılarak yazılır. Örneğin bir hastane otomasyonunda "hastane", "doktor", "hemşire", "hasta" vs. gibi kavramlar birer sınıfla temsil edilmelidir. Sonra bu sınıflar türünden nesneler oluşturulmalıdır. Örneğin hastanemizde 10 doktor varsa biz 10 tane Doktor sınıfı türünden nesne yaratırız. Tüm hastane de yine Hastane isimli bir sınıfla temsil edilebilir. Aslında projede kullanılan kavramlar dolayısıyla da sınıflar birbirlerinden kopuk değildir. Sınıflar arasında birtakım ilişkiler söz konusudur. Örneğin Hastane optmasyonunda "Doktor" sınıfı ile "Hasta" sınıfı arasında bir ilişki vardır. Hastanın bir doktoru bulunmaktadır. Bir doktorun birden fazla hastası olabilir. Biz bu bölümde bir projedeki sınıflar arasındaki ilişkiler üzerinde duracağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıflar arasında tipik olarak dört tür ilişki bulunmaktadır: 1) İçerme ilişkisi (composition) 2) Birleşme ilişkisi (aggregation) 3) Türetme ilişkisi (inheritance) 4) Çağrışım ilişkisi (association) Tabii ilişki olmaması da bir ilişki olarak değerlendirilebilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf türünden bir nesne başka bir sınıf türünden bir nesnenin bir parçasını oluşturuyorsa bu iki sınıf arasında "içerme ilişkisi (composition)" vardır. İçerme ilişkisi bire-bir olabileceği gibi bire-çok biçimde de olabilir. İçerme ilişkisi UML "sınıf diyagramlarında (class diagrams) içeren sınıf tarafında içi dolu bir baklavacık (diamond) ile gösterilmektedir. İçerme ilişkisinin sağlanması gereken iki temel özelliği vardır: 1) İçerilen nesne tek bir nesne tarafından içerilir. 2) İçeren nesne ile içerilen nesnenin ömürleri yaklaşık aynıdır. Bu durumda örneğin insan ile böbrek sınıfı arasında içerme ilişkisi vardır. Böbrekle insan aynı zamanda hayata başlar ve bunların aynı zamanda yaşamları biter. Bir böbrek tek bir insanın böbreğidir. Aynı zamanda başka bir insanın böbreği değildir. Ya da bir insanın böbreği normal kullanımda ondan çıkartılıp diğerine takılmaz. Tabii bu tür modellemelerde tipik durumlar dikkate alınmalıdır. Yani örneğin böbrek nakli tipik bir durum değildir. Böbrek naklinin yapılıyor olması bir böbreğin başkaları tarafından da "tipik olarak" kullanıldığı anlamına gelmez. Örneğin Araba sınıfı ile Motor sınıfı arasında da içerme ilişkisi vardır. Motor arabanın bir parçasını oluşturmaktadır. Arama yaşamına motor ile birlikte başlar. Araba kullanım dışı kaldığında (örneğin pert olduğunda) motorla birlikte kullanım dışı kalmaktadır. Bir motor tek bir arabanın motorudur. Tabii burada da istisnai durumlar söz konusu olabilir Yani örneğin arabanın motıry değiştirilebilir. Ancak bu da istisnai bir durum olarak değerlendirilebilir. Satranç tahtası Tahta sınıfyla temsil ediliyor olsun. Üzerindeki karaler de Kare sınıfıyla temsil ediliyor olsun. Satranç tahtası üretildiğinde karelerle birlikte üretilmektedir. Satranç tahtası kırıldığında kareler de kullanılamaz duruma gelir. O halde Tahta sınıfı ile Kare sınıfı arasında içerme ilişkisi vardır. Yukarıda da belirtitğimiz gibi içerme ilişkisi bire bir olmak zorunda değildir. Örneğin 1 Tahta 64 kareyi içermektedir. Bir insan iki böbreği içermektedir. UML sınıf diyagramlarında bu içerme sayıları çizgilerin sınıfla birleştiği yerlerde belirtilebilmektedir. Örneğin Oda sınıfı ile Duvar sınıfı arasındaki ilişki içerme ilişkisi değildir. Her ne kadar oda ile duvarın yaşamları aynıysa da duvar aynı zamanda yandaki odanın da duvarıdır. İçerme ilişkisine İngilizce "has a" ilişkisi de denilmektedir. İçerme ilişkisine benzeyen ancak içerme ilişkisi olmayan ilişkilerin büyük bölümü sonraki paragraflarda ele alacağımız "birleşme (aggregation)" ilişkisidir. Örneğin Hastane ile DOktor sınıfları arasında içerme ilişkisi yoktur. Bilgisayar ile Fare sınıfarı arasında da içerme ilişkisi yoktur. Bu sınıflar arasındaki ilişkilere "birleşme ilişkisi (aggregation)" denilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta içerme ilişkisi iki biçimde oluşturulabilir: 1) İçeren sınıfın private bölümünde içerilen sınıf türünden bir veri elemanı bulundurulur. İçeren sınıf nesnesi yaratıldığında içerilen nesne de yaratılmış olacaktır. İçeren sınıf nesnesi yok edildiğinde içerilen nesne de yok edilecektir. Böylece içeren nesne ile içerilen nesnenin yaşamları aynı olacaktır. İçerilen nesne içeren nesnenin private bölmünde olduğuna göre onu dışarıdan başka sınıflar kullanamaz. Örneğin: class Motor { //... }; class Araba { public: //... private: Motor m_motor; }; //... Araba araba; Burada Araba nesnesi yaratıldığında Motor nesnesi de onun bir parçası olarak yaratılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Motor { //... }; class Araba { //... private: Motor m_motor; //... }; int main() { Araba araba; //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 58. Ders 13/03/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 2) İçeren sınıfın private bölümünde içerilen sınıf türünden bir gösterici veri elemanı bulundurulur. İçeren sınıfın yapıcı fonksiyonunda da new operatörüyle bu veri elemanı için dinamik tahsisat yapılır. Tabii bu tahsisat içeren sınıfın yıkıcı fonksiyonunda delete operatörü ile yok edilmelidir. Örneğin: class Araba { public: Araba(); ~Araba(); //... private: Motor *m_motor; }; Araba::Araba() { m_motor = new Motor(); //... } Araba::~Araba() { delete[] m_motor; //... } Aşağıda da içerme ilişkisinin ikinci biçimine ilişkin örnek görüyorsunuz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Motor { //... }; class Araba { public: Araba(); ~Araba(); private: Motor *m_motor; //... }; Araba::Araba() { m_motor = new Motor(); //... } Araba::~Araba() { //... delete m_motor; } int main() { Araba araba; //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- GUI uygulamalarında ekranda bağımızsız olarak kontrol edilebilen dikdörtgensel bölgelere "pencere (window)" ya da "widget (window gadget)" denilmektedir. GUI kütüphanelerinde genellikle her GUI eleman bir sınıfla temsil edilir. Bir pencere açıldığında eğer onun içerisinde birtakım GUI elemanlar (widget'lar) varsa ve pencere kapatıldığında onlar da ana pencereyle birlikte yok ediliyorsa tipik bir içerme ilişkisi söz konusu olur. Örneğin Qt Kütüphanesinde her Widget bir sınıfla temsil edilmiştir. Tipik olarak içerme ilişkisi gösterici yoluyla gerçekleştirilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Örneğin bir satranç tahtası Board isimli bir sınıfla, tahta üzerindeki kareler de Square isimli bir sınıfla temsil ediliyor olsun. Burada 1'e 64'lük bir içerme ilişkisi vardır: class Square { public: Square() { cout << "Square constructor" << endl; } }; class Board { public: //... private: Square m_squares[8][8]; }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Square { public: Square() { cout << "Square constructor" << endl; } }; class Board { public: //... private: Square m_squares[8][8]; }; int main() { Board board; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Birleşme (aggregation) ilişkisinde bir sınıf türünden nesne başka bir sınıf türünden nesneyi kullanmaktadır. Ancak kullanılan nesne başka nesneler tarafından da kullanılıyor olabilir. Kullanan nesne ile kullanılanılan nesnenin yaşamları aynı da olmayabilir. Genel olarak içerme ilişkisine uymayan kullanma ilişkisi birleşme ilişkisi biçimindedir. Örneğin Hastana ile Doktor sınıfları arasında, Bilgisayar ile Fare sınıfları arasında birleşme ilişkisi vardır. Birleşme ilişkisi UML sınıf diyagramlarında ""kullanan sınıf tarafında içi boş bir baklavacık (diamond)" ile gösterilmektedir. Birleşme ilişkisi de bire-bir olabileceği gibi bire-çok olabilir. Birleşme ilişkisine İngilizce'de aynı zamanda "holds a" ilşkisi de denilmektedi. Birleşme ilişkisi C++'ta tipik olarak kullanan sınıf içerisinde kulalnılan sınıfa ilişkin bir gösterici tutularak gerçekleştirilir. Tabii bu gösterici dışarıda yaratılmış olan bir nesneyi gösterecektir. Bu sayede birden fazla nesne gösterici yoluyla aynı nesnesiyi kullanabilmektedir. Örneğin: class Mouse { //... }; class Computer { public: Computer(Mouse *mouse = nullptr) : m_mouse(mouse) { //... } void attach_mouse(Mouse *mouse) { m_mouse = mouse; } Mouse *detach_mouse() { Mouse *mouse = m_mouse; m_mouse = nullptr; return mouse; } private: Mouse *m_mouse; //... }; Aşağıdaki örnekte Computer ile Mouse sınıfları arasındaki birleşme ilişkisi görülmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Mouse { //... }; class Computer { public: Computer() : m_mouse(nullptr) { //... } void attach_mouse(Mouse *mouse) { m_mouse = mouse; } Mouse *detach_mouse() { Mouse *mouse = m_mouse; m_mouse = nullptr; return mouse; } private: Mouse *m_mouse; //... }; int main() { Computer computer1; Computer computer2; //... Mouse mouse1; bomputer1.attach_mouse(&mouse1); Mouse *mouse = computer1.detach_mouse(); computer2.attach_mouse(mouse); Mouse mouse2; computer1.attach_mouse(&mouse2); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de birleşme ilişkisiyle Hastane Doktor örneğini oluşturalım. Bir Hastane birden falz Doktor'u kullanabilmektedir. Bu durumda Doktor'ların Hastane içerisinde bir vector nesnesi ile tutulması uygun olabilir. Tabii bu vector nesnesi Doctor nesnelerinin adreslerini tutacaktır. Örneğin: vector m_doctors; Aşağıda Hastane Doktor birleşme ilişkisine bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Doctor { //... }; class Hospital { public: Hospital(); void add_doctor(Doctor *doctor); void remove_doctor(Doctor *doctor); private: vector m_doctors; }; Hospital::Hospital() { //... } void Hospital::add_doctor(Doctor *doctor) { m_doctors.push_back(doctor); } void Hospital::remove_doctor(Doctor *doctor) { for (size_t i = 0; i < m_doctors.size(); ++i) if (m_doctors[i] == doctor) { m_doctors.erase(m_doctors.begin() + i); break; } } int main() { Hospital hospital1; Hospital hospital2; Doctor doctorx; hospital1.add_doctor(&doctorx); hospital2.add_doctor(&doctorx); //... hospital1.remove_doctor(&doctorx); //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 59. Ders 20/03/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki bir satranç tahtasının NYPT ile modellenmesi örneği verilmiştir. Burada Board sınıfı tahtayı, Square sınıfı tahtanının karelerini ve Figure sınıfı ise satranç taşlarını temsil etmektedir. Board sınıfı ile Square sınıfı arasında "içerme (composition)", Square sınıfı ile Figure sınıfı arasında ise "birleşme (aggregation)" ilişkisi vardır. Buradaki satranç tahtası örneği size NYPT hakkında iyi bir fikir verecektir. Biz gerçek hayatta birtakım işlemleri nasıl yapıyorsak NYPT'de de benzer biçimde yaparız. Örneğin gerçek bir satranç tahtasında bir taşı hareket ettirmek için taşı kareden çekeriz, sonra hedef kareye koyarız. Aşağıdaki programda da biz aynı biçimde taşı hareket ettirdik. Bu örnekteki sınıfların üye fonksiyonlarını dikkatlice inceleyiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // chess.hpp #ifndef CHESS_HPP_ #define CHESS_HPP_ enum class Color { Black, White }; enum class FigureType { King, Queen, Rook, Bishop, Knight, Pawn }; #endif // board.hpp #ifndef BOARD_HPP_ #define BOARD_HPP_ #include #include "square.hpp" class Board { public: Board(); Square &at(char col, char row); void move(const std::string &m); void disp() const; private: Square m_squares[8][8]; }; #endif // board.cpp #include #include #include "board.hpp" #include "figure.hpp" using namespace std; Board::Board() { for (int row = 0; row < 8; ++row) for (int col = 0; col < 8; ++col) m_squares[row][col].color((row + col) % 2 ? Color::White : Color::Black); for (int i = 0; i < 8; ++i) { m_squares[1][i].figure(new Figure(FigureType::Pawn, Color::White)); m_squares[6][i].figure(new Figure(FigureType::Pawn, Color::Black)); } m_squares[0][0].figure(new Figure(FigureType::Rook, Color::White)); m_squares[7][0].figure(new Figure(FigureType::Rook, Color::Black)); m_squares[0][1].figure(new Figure(FigureType::Knight, Color::White)); m_squares[7][1].figure(new Figure(FigureType::Knight, Color::Black)); m_squares[0][2].figure(new Figure(FigureType::Bishop, Color::White)); m_squares[7][2].figure(new Figure(FigureType::Bishop, Color::Black)); m_squares[0][3].figure(new Figure(FigureType::Queen, Color::White)); m_squares[7][3].figure(new Figure(FigureType::Queen, Color::Black)); m_squares[0][4].figure(new Figure(FigureType::King, Color::White)); m_squares[7][4].figure(new Figure(FigureType::King, Color::Black)); m_squares[0][5].figure(new Figure(FigureType::Bishop, Color::White)); m_squares[7][5].figure(new Figure(FigureType::Bishop, Color::Black)); m_squares[0][6].figure(new Figure(FigureType::Knight, Color::White)); m_squares[7][6].figure(new Figure(FigureType::Knight, Color::Black)); m_squares[0][7].figure(new Figure(FigureType::Rook, Color::White)); m_squares[7][7].figure(new Figure(FigureType::Rook, Color::Black)); } Square &Board::at(char col, char row) { return m_squares[tolower(row) - '1'][tolower(col) - 'a']; } void Board::move(const std::string &m) { string::size_type pos = m.find('-'); if (pos == string::npos) throw invalid_argument("invalid move format"); Square &source = at(m[0], m[1]); Square &target = at(m[3], m[4]); target.put(source.take()); } void Board::disp() const { const char *fg_black = "\x1b[34m"; const char *fg_white = "\x1b[33m"; const char *bg_black = "\x1b[40m"; const char *bg_white = "\x1b[47m"; const char *reset = "\x1b[0m"; for (int row = 7; row >= 0; --row) { for (int col = 0; col < 8; ++col) { cout << (m_squares[row][col].color() == Color::Black ? bg_black : bg_white); cout << ' '; auto figure = m_squares[row][col].figure(); if (figure != nullptr) { cout << (figure->color() == Color::Black ? fg_black : fg_white); cout << m_squares[row][col].figure()->fsym(); } else cout << ' '; cout << ' '; } cout << reset << endl; } cout << reset << endl; } // square.hpp #ifndef SQUARE_HPP_ #define SQUARE_HPP_ #include "chess.hpp" #include "figure.hpp" class Square { public: Square(); Color color() const { return m_color; } void color(Color color){ m_color = color; } void figure(Figure *figure) { m_figure = figure; }; Figure *figure() const { return m_figure; } Figure *take(); void put(Figure *figure); private: Color m_color; Figure *m_figure; }; #endif // square.cpp #include #include "square.hpp" using namespace std; Square::Square() : m_figure(nullptr) {} Figure *Square::take() { Figure *figure; figure = m_figure; m_figure = nullptr; return figure; } void Square::put(Figure *figure) { if (m_figure != nullptr) delete m_figure; m_figure = figure; } // figure.hpp #ifndef FIGURE_HPP_ #define FIGURE_HPP_ #include "chess.hpp" class Figure { public: Figure(FigureType ftype, Color color); FigureType ftype() const { return m_ftype; } Color color() const { return m_color; } void disp() const; char fsym() const; private: FigureType m_ftype; Color m_color; }; #endif // figure.cpp #include #include "figure.hpp" using namespace std; Figure::Figure(FigureType ftype, Color color) : m_ftype(ftype), m_color(color) {} void Figure::disp() const { static const char *colors[] = {"Siyah", "Beyaz"}; static const char *ftypes[] = {"Sah", "Vezir", "Kale", "Fil", "At", "Piyon"}; cout << colors[static_cast(m_color)] << ' ' << ftypes[static_cast(m_ftype)] << endl; } char Figure::fsym() const { static const char *ftypes = "svkfap"; return ftypes[static_cast(m_ftype)]; } // app.cpp #include #include #include "board.hpp" using namespace std; int main() { Board board; board.disp(); board.move("e2-e4"); board.disp(); board.move("e7-e5"); board.disp(); board.move("g1-f3"); board.disp(); board.move("g8-c6"); board.disp(); board.move("f1-b5"); board.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 60. Ders 25/03/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- NYPT'de "türetme ilişkisi" mevcut bir sınıfa "ekleme yapma" anlamına gelmektedir. Elimizde A isimli bir sınıf bulunuyor olsun. Biz bu sınıfa onu bozmadan birkaç üye fonksiyon eklemek isteyelim. İşte bunun için bu A sınıfından bir B sınıfını türetiriz. Ekleyeceğimiz üye fonksiyonları bu B sınıfına ekleriz. Burada ekleme yapmak istediğimiz asıl sınıf olan A sınıfına "taban sınıf (base class)", eklemelerin yapıldığı sınıf olan B sınıfına da "türemiş sınıf (derived class)" denilmktedir. Türemiş sınıf hem taban sınıf gibi davranmakta hem de kendine özgü fazlalıklara sahip olmaktadır. UML sınıf diyagramlarında türetme ilişkisi türemiş sınıftan tabana sınıfa doğru içi boş bir ok ile temsil edilmektedir. Türetme ilişkisine "kalıtım (inheritance)" da denilmektedir. Buradaki "kalıtım" sözcüğü türemiş sınıfın taban sınıf özelliklerini de bünyesinde barındırmasından hareketle uydurulmuştur. Yani türemiş sınıfın taban sınıf gibi davranması biyolojideki "çocuğun anne babadan birtakım özellikleri alması" olgusuna benzetilmiştir. Biz grafik çizemediğimiz text editötörümüzde türetme ilişkisini aşağıdaki gibi göstereceğiz: A B Burada B sınıfı A sınıfından türetilmiştir. Türemiş sınıftan da sınıflar türetilebilir. Bu durumda türemiş sınıf onun taban sınıflarının hepsinin şlevselliklerine sahip olur. Örneğin: A B C Burada B sınıfı türünden bir nesne hem B sınıfının hem de A sınıfının işlevselliğine sahip olacaktır. C sınıfı türünden bir nesne de hem B sınıfının hem A sınıfının hem de C sınıfının işlevselliğine sahip olacaktır. UML sınıf diyagramlarında türetme ilişkisi "türemiş sınıftan taban sınıfa doğru çekilen içi boş bir okla" temsil edilmektedir. Türetme ilişkisine İngilizce "is a" ilişkisi de denilmektedir. Bir sınıf birden fazla sınıfın taban sınıfı durumunda olabilir. Örneğin: A B C Burada A sınıfı hem B sınıfının hem de C sınıfının taban sınıfı durumundadır. Yani B ve C sınıflarının ortak elemanları A sınıfında bulunmaktadır. Bir sınıfın birden fazla taban sınıfa sahip olması durumu özel bir durumdur. Buna NYPT'de "çoklu türetme (multiple inheritance)" denilmektedir. Örneğin: A B C Burada C sınıfının iki taban sınıfı vardır: A ve B. Java, C#, Swift gibi dillerin bazılarında çoklu türetme yoktur. Yani bu dillerde bir sınıf yalnızca tek bir sınıftan türetilebilir. Ancak C++, Object Pascal, Python gibi dillerde çoklu türetme bulunmaktadır. Yani biz C++'ta bir sınıfın birden fazla taban sınıfa sahip olmasını sağlayabiliriz. Çoklu türetmeyle seyrek de olsa dış dünyada karşılaşılmaktadır. Bir C sınıfı hem A sınıfının hem de B sınıfının özelliklerini taşıyorsa yani hem bir çeşit A hem de bir çeşit B ise bu sınıf A ve B sınıflarından çoklu türetilebilir. Türetme işleminin NYPT bakımından iki önemi vardır: 1) Türetme saysesinde mevcut bir sınıfa onu bozmadan eklemeler yapılabilmektedir. 2) Türetme sayesinde ortak elemanlar taban sınıfta toplanarak kod tekrarı engellenebilmektedir. Kod tekrarının elimine edilmesi programlamanın önemli prensiplerindendir. Prosedürel teknikte kod tekrarı "tekraralan kodun bir fonksiyona dönüştürülmesi ve gerektiğinde o fonksiyonun çağrılması ile" engellenmektedir. İşte NYPT'de de "sınıflar düzeyindeki kod tekrarları türetme yoluyla" elimine edilmektedir. Örneğin A ve B sınıflarında foo ve bar fonksiyonları ortak bir biçimde bulunuyor olsun. Biz foo ve bar fonksiyonlarını taban bir X sınıfında toplayabiliriz. A ve B sınıflarını bu X sınıfından türetebiliriz. Böylece A ve B sınıfında ayrı ayrı foo ve bar fonksiyonlarını bulundurmak yerine X sınıfında bunları yalnızca bir kez bulundurabiliriz: X (foo, bar) A B Sınıf kütüphanelerinde sınıfların çoğu genellikle birbirinden kopuk değil birbirleriyle ilişkili biçimde bulunmaktadır. Yine genellikle sınıf kütühanelerinde bir türetme şeması söz konusu olmaktadır. Bir ütretme şemasında "yukarı çıkıldıkça genelleşme aşağı inildikçe özelleşme" oluşmaktadır. Örneğin: A B C D E F G I J Bu türetme şemasında A sınıfı tüm sınıflardaki ortak özellikleri barındırmaktadır. Yani A sınıfının elemanları her sınıfta olan ortak elemanlardır. Halbuki örneğin J sınıfının elemanları sadece J sınıfına özgüdür. İşte burada olduğu gibi türetme şemasında yukarı çıkıldıkça daha genel fonksiyonlarla, aşağı inildikçe daha özel fonksiyonlarla karşılaşılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türetme konusunun iyi anlaşılması için birkaç örnek vermek istiyoruz. - Bir kargo şirketi için program yazacağımızı düşünelim. Kargo şirketinin elinde Motosiklet, Otomobil, Kamyon ve Tır biçiminde dört tür araç olsun. Bu araçların hepsinin ortak özellikleri vardır. Örneğin hepsinin bir plakası, bir motor hacmi, bir yolcu ve yük kapasitesi bulunmaktadır. Ancak bu araçların kendilerine özgü birtakım özellikleri de vardır. Bu araçların ortak özelliklerini Araç isimli bir sınıfta toplayabiliriz. Diğer sınıfları bu sınıflardan türetebiliriz. Öte yandan kamyon ile tır da birbirlerine benzemektedir. Tır Kamyon özelliklerinin yanı sıra kamyonda olmayan özellikleri de barındırmaktadır. O halde bu taşıtlar için aşağıdaki gibi nir türetme şeması oluşturabiliriz: Araç Motosiklet Otomobil Kamyon Tır Burada Motosiklet de Otomobil de Kamyon da bir çeşit Araç'tır. Öte yandan Tır da bir çeşit Kamyon'dur. - Bir iş yerinde çalışanları görevlerine göre sınıflarla temsil etmek isteyelim. Tüm çalışanların görevleri ne olursa olsun ortak birtakım bilgileri vardır. Örneğin her çalışanın adı soyadı, sosyal güvenlik numarası, bölümü vs. vardır. Biz tüm çalışanların ortak özelliklerini Employee isimli bir sınıfta toplayabiliriz. İşçi bir çalışandır. Bu durumda Worker sınıfını Employee sınıfından türetebiliriz. Yönetici de bir çalışandır. Manager fınıfını da Employee s ınıfından türetebiliriz. Öte yandan üst düzey yönetici de bir çeşit yöneticidir. Biz de Executive sınıfını Manager sınıfından türetbiliriz. Bu türetmelerle aşağıdaki gibi bir türetme şeması oluşabilecektir: Employee Worker SalesPerson Manager ... Executive - GUI kütüphanelerinde her GUI eleman (buna "window", "widget" ya da "control" da denilmektedir) bir sınıfla temsil edilmektedir. Programcı bir GUI eleman oluşturmak için ilgili sınıf türünden bir nesne yaratmaktadır. Ancak her GUI elemanın ortak birtakım özellikleri vardır. Bu tür GUI kütüphanelerinde ortak özellikler taban sınıflarda toplanarak bir türetme şeması oluşturulur. Örneğin .NET'teki Forms denilen kütüphanede tüm GUI elemanların ortak özellikleri Control isimli bir sınıfla temsil edilmiştir. Normal düğmeler (push buttons) Button isimli sınıfla, Radyo düğmeleri RadioButton isimli bir sınıfla, Seçenek Kutuları CheckBox isimli bir sınıfla temsil edilmiş durumdadır. Düğmelerin, radyo düğmelerinin ve seçenek kutularının da ortak özellikleri vardır. Kod tekrarını engellemek için bu ortak özellikler de ButtonBase isimli bir sınıfta toplanmıştır. Benzer biçimde ListBox ve ComboBox sınıflarının ortak özellikleri de ListConstrol sınıfında toplanmıştır. Ana pencereler de bir GUI eleman gibidir. Ana pencereler de Form sınıfı ile temsil edilmiştir. Bu biçimde onlarca GUI eleman bir türetme şeması biçiminde sınıflarla temsil edilmiştir. Bu türetme şemasının en tepesinde her türlü GUI elemandaki ortak özellikleri temsil eden C ontrol sınıfı bulunmaktadır. Control Form ButtonBase ListControl ... Button RadioButtun CheckBox ListBox ComboBox ... .... ... C++'ta en çok kullanılan GUI kütüpanesi ("framework" de diyebiliriz) Qt isimli kütüphanedir. Bu kütüphanedeki tüm sınıflar Q harfi ile başlatılarak isimlendirilmiştir. Genel tasarım yukarıda bahsettiğimiz .NET'in Forms kütüphanesine benzemektedir. Qt kütüphanesindeki GUI elemanlarının ortak özelliğini temsil eden tepedeki sınıf QWidget isimli sınıftır. Her GUI eleman QWidgeT sınıfınan doğrudan ya da dolaylı bir biçimde türetilmiş durumdadır. Örneğin QPushButton, QCheckBox ve QRadioButton sınıflarının ortak özellikleri de QAbstractButton sınıfında toplanmıştır. - PowerPoint benzeri bir program yazacak olalım. Programımızda çeşitli şekiller sürüklenerek bırakılsın, fare ile tıklandığında seçilebilsin ve özellikleri değiştirilebilsin. Bu programdaki tüm şekillerin ortak birtakım özellikleri vardır. Örneğin tüm şekillerin birer çizgi rengi, zemin rengi, birer koordinat bilgisi vardır. O zaman böyle bir programda tüm şekillerin ortak özelliklerini Shape isimli bir sınıfta toplayabiliriz. Tüm şekilleri bu sınıftan türetebiliriz. Örneğin: Shape RectangleShape EllipseShape LineShape .... - Bir starnaç programında satranç taşlarının da ortak birtakım özellikleri vardır. Ancak bu taşlar aslında birbirinden farklıdır. O halde tüm taşların ortak özellikleri Figure gibi bir sınıfta toplanabilir. Taşlar da bu sınıftan türetilebilir. Örneğin: Figure King Queen Rook Bishop Knight Pawn - Yukarıda da belirttiğimiz gibi çoklu türetme gerçek dünyada seyrek karşılaşılan bir durumdur. Ancak burada çoklu türetmeye bir örnek vermek istiyoruz. C++'ın standart kütüphanesindeki ostream isimli sınıf bir dosyaya yazma yapan << operatör fonksiyonlarını barındırmaktadır. Yani biz bu sınıfla yalnızca bir dosyaya yazma yapabiliriz. istream isimli sınıf ise bir dosyadan okuma yapan >> operatör fonksiyonlarını barındırmaktadır. O halde biz istream sınıfı türünden bir nesne ile yalnızca dosyalardan okuma yapabiliriz. İşte C++'ın standart kütüphanesinde iostream isimli sınıf hem istream sınıfından hem de ostream sınıfından çoklu türetilmiştir. iostream sınıfı türünden bir nesne ile biz bir dosyadan hem okuma yapabiliriz hem de ona yazma yapabiliriz. istream ostream iostream Aslında ekrana bir şeyler yazmak için kullandığımız cout ostream sınıfı türünden global bir sınıf nesnesidir. Biz bu nesne yoluyla << operatör fonksiyonunu çağırdığımızda ekrana (stdout dosyasına) yazma yaparız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz içerme ilişkisini (composition) ve birleşme ilişikisini (aggregation) bildiğimiz kurallarla oluşturabildik. Ancak türetme ilişkisi "türetme sentaksıyla" oluşturulmaktadır. C++'ta türetme işlemlerinin bazı ayrıntıları vardır. Biz izleyen paragraflarda C++'ta türetme işlemlerinin nasıl yapıldığı üzerinde duracağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çağrışım ilişkisinde (association) bir sınıf bir sınıfı kullanmaktadır. Ancak bu kullanma onu bünyesine katarak (yani bir veri elemanında saklayarak) yapılmaz. Yalnızca üye fonksiyonlar tarafından yapılır. Yani kullanma yüzeyseldir ve birkaç üye fonksiyonla sınırlıdır. Örneğin Hastane sınıfı gerektiğinde reklam yapacaktır. Bunun için sınıfın reklam_yap gibi bir üye fonksiyonu reklam şirketini kullanabilir. Bir ticari taksi ile şoför arasında bir birleşme ilişkisi vardır. Bunlarla taksinin sahibi arasında bir birleşme ilişkisi vardır. Ancak taksi ile yolcu arasındaki ilişki yüzeyseldir. Bu ilişki çağrışım ilişkisi biçiminde ifade edilebilir. Çağrışım ilişkisi UML sınıf diyagramlarında kullanan sınıftan kullanılan sınıfa doğru ince bir çizgi ve bir okla temsil edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 61. Ders 27/03/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta türetme işleminin genel biçimi şöyledir: class : [türetme_biçimi] { //... }; Buradaki türetme biçimi "public", "protected" ya da "private" anahtar sözcüklerinden biri olabilir. Türetme biçimi belirtilmezse türetme biçimi default olarak türemiş sınıf class anahtar sözcüğü ile oluşturulmuşsa "private", struct anahtar anahtar sözcüğü ile oluşturulmuşsa "public" kabul edilir. En yaygın kullanılan türetme biçimi public türetmesidir. (Java, C# gibi pek çok dilde türetme biçimi yoktur. Ancak o dillerdeki türetme etki bakımından C++'taki "public türetmesi" gibidir.) Örneğin: class A { //... }; class B : public A { //... }; Burada A taban sınıf B ise türemiş sınıftır. Örneğin: class A { //... }; class B : public A { //... }; class C : public A { //... }; Burada B sınıfı da C sınıfı da A sınıfından türetilmiştir. Yani türetme şeması aşağıdaki gibidir: A B C Örneğin: class A { //... }; class B : public A { //... }; class : public B { //... }; Burada B sınıfı A sınıfından, C snıfı da B sınıfından türetilmiş durumdadır. Bu türetmelerin türetme şeması da şöyledir: A B C --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türetme işleminde türemiş sınıf hem taban sınıf gibi kullanılabilmekte hem de ek birtakım elemanlara sahip olabilmektedir. Aşağıdaki örnekte B sınıfı A'dan türetilmiştir. B sınıfı hem A gibi kullanılabilir hem de kendi elemanlarına sahiptir. Biz B sınıfı türünden bir nesneyle A sınıfının foo ve bar üye fonksiyonlarını çağırabiliriz. Aynı zamanda B sınıfının da tar üye fonksiyonunu çağırabiliriz. Tabii A sınıfı türünden bir nesne ile biz yalnızca A sınıfının üye fonksiyonlarını çağırabiliriz. Türetmede türemiş sınıfın taban gibi kullanılabildiğine, taban sınıfın türemiş sınıf gibi kullanılamadığına dikkat ediniz. (Tıpkı çocukların ebeveynlerinin özelliklerini aldıkları ama bunun tersibin nümkün olmadığı gibi.) A (foo, bar) B (tar) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class A { public: void foo(); void bar(); //... }; class B : public A { public: void tar(); }; void A::foo() { cout << "A::foo" << endl; } void A::bar() { cout << "A::bar" << endl; } void B::tar() { cout << "B::tar" << endl; } int main() { B b; b.foo(); b.bar(); b.tar(); A a; a.foo(); a.bar(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz genel olarak her sınıfın iki dosya biçiminde oluşturulabileceğini belirtmiştik. Sınıfın bildirimini ".h" ya da ".hpp" dosyasına, üye fonksiyon tanımlamalarını da ".cpp" dosyasına yerleştiriyorduk. Türetme durumunda da taban ve türemiş sınıflar yine iki ayrı dosya biçiminde organize edilebilirler. Tabii türemiş sınıf tanan sınıfı kullanacağı için türemiş sınıf bildiriminin bulunduğu başlık dosyasından taban sınıfa ilişkin başlık dosyasının include edilmesi gerekmektedir. Aşağıda bu duruma bir örnek verilmiştir. (Ancak biz konuyu kolay anlatabilmek için örneklerimizdeki sınıfları çoğu kez aynı dosya içerisinde bulunduruyoruz. Daha önceden de belirttiğimiz gibi bunların ayrı dosyalarda tutulması daha iyi bir tekniktir.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // a.hpp #ifndef A_HPP #define A_HPP class A { public: void foo(); void bar(); //... }; #endif // a.cpp #include #include "a.hpp" using namespace std; void A::foo() { cout << "A::foo" << endl; } void A::bar() { cout << "A::bar" << endl; } // b.hpp #ifndef B_HPP_ #define B_HPP_ #include "a.hpp" class B : public A { public: void tar(); //... }; #endif // b.cpp #include #include "b.hpp" using namespace std; void B::tar() { cout << "B::tar" << endl; } // app.cpp #include #include "b.hpp" int main() { B b; b.foo(); b.bar(); b.tar(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türemiş sınıf, veri elemanları bakımından da taban sınıfı içermektedir. Yani türemiş sınıf türünden bir nesne hem türemiş sınıfın veri elemanlarını hem de taban sınıfın tüm veri elemanlarını içerir. Türemiş sınıf nesnesi içerisinde taban sınıf ve türemiş sınıf veri elemanları ardışıl bir biçimde bulunmaktadır. (Bu konudaki ayrıntılardan daha önce bahsetmiştik.) Yani türemiş sınıf nesnesindeki taban sınıf veri elemanları da türemiş sınıf veri elemanları da kendi içerisinde ardışıl biçimde bulunmaktadır. Ancak standartlar türemiş sınıf nesnesi içerisinde taban ve türemiş sınıf veri eleman bloklarının birbirine göre yerleri hakkında bir belirlemede bulunmamıştır. Yani standartlara göre türemiş sınıf nesnesinde taban sınıf veri elemanları türemiş sınıf veri elemanlarının yukarısında (yani daha düşük adreste) ya da aşağısında (daha yüksek adreste) bulunabilir. Ancak derleyicilerin hemen hepsi taban sınıf veri elemanlarını türemiş sınıf veri elemanlarının yukarısına (daha düşük adrese) yerleştirmektedir. Örneğin B sınıfı A sınıfından türetilmiş olsun. B sınıfı türünden bir nesnenin tipik olarak bellekteki dizilimi şöyle olacaktır: A-dat B-dat Burada A-dat A sınıfının veri elemanlarını, B-dat B sınıfın veri elemanlarını temsil etmektedir. Standratlar dizilimin bunun tersi biçiminde de olabileceğini söylese de ters yerleşim üretilen kodun etkinliğini düşürebilmektedir. Biz de ilerideki örneklerimizdeki çizimlerde hep taban sınıf veri eleman bloğunun türemiş sınıf veri eleman bloğunun yukarısında göstereceğiz. Örneğin B sınıfı A sınıfından türetilmiş olsun. B sınıfı türünden bir nesnenin adresini aldığımızda o adresten itibaren önce taban sınıf veri elemanları sonra türemiş sınıf veri elemanları bulunacaktır. B sınıfı A sınıfından, C sınıfı da B sınıfından türetilmiş olsun. Yani aşağıdaki türetme şeması uygulanmış olsun: A B C Burada A sınıfı türünden bir nesne yaratığımızda bu nesne yalnızca A sınıfın veri elemanlarını içerecektir: A-dat B sınıfı türünden bir nesne yaratığımızda bu nesne hem A sınıfının hem de B sınıfının veri elemanlarını içerecektir: A-dat B-dat C sınıfı türünden bir nesne yaratığımızda bu nesne da hem A sınıfının hem B sınıfının hem de C sınıfının veri elemanlarını içerecektir: A-dat B-dat C-dat Burada örneğin C sınıfı türünden bir nesnenin adresini aldığımızda o adreste önce A sınıfının veri elemanları bulunacaktır. Biraz ilerlendiğinde B sınıfının, biraz daha ilerlendiğinde C sınıfın veri elemanları bulunacaktır. Aşağıdaki örnekte türemiş sınıf türünden bir nesnenin taban sınıf veri elemanlarını da içerdiği ve derleyicilerin taban sınıf veri eleman bloğu düşük adreste olacak biçimde ardışıl bir yerleştirme yaptığı gösterilmek istenmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: //... int m_x; int m_y; }; class B : public A { public: //... int m_z; int m_k; }; int main() { B b; b.m_x = 10; b.m_y = 20; b.m_z = 30; b.m_k = 40; cout << b.m_x << ", " << b.m_y << ", " << b.m_z << ", " << b.m_k << endl; cout << &b.m_x << endl; cout << &b.m_y << endl; cout << &b.m_z << endl; cout << &b.m_k << endl; cout << sizeof(b) << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türemiş sınıf taban sınıfı içerdiğine göre türemiş sınıfın taban sınıfa erişebilmesi gerekir. Biz türemiş sınıf türünden bir nesne ile taban sınıfın üye fonksiyonlarını çağırmıştık. İşte türemiş sınıflarda taban sınıflar için erişim kuralları türetme biçimine göre değişmektedir. Yukarıda da belirttiğimiz gibi en çok kullanılan türetme biçimi "public" türetmesidir. Biz de şimdi türemiş sınıflardaki tabana erişim kuralları üzerinde duracağız. Bu konu çerçevesinde ilk kez sınıfların "protected" bölümlerinin anlamı üzerinde de açıklamalarda bulunacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- public türetmesinde "taban sınıfın public bölümü türemiş sınıfın public bölümüymüş gibi, taban sınıfın protected bölümü türemiş sınıfın protected bölümüymüş gibi" işlem görür. Taban sınıfın private bölümü tamamen korunmuştur. Türemiş sınıf tarafından erişilemez. Taban Sınıf Türemiş Sınıf public --------> public protected --------> protected private X public türetmesinden çıkan sonuçlar şunlardır: 1) Dışarıdan türemiş sınıf nesnesi, göstericisi ya da referansı yoluyla taban sınıfın ve türemiş sınıfın yalnızca public bölümündeki elemanlara erişebiliriz. 2) Türemiş sınıfın üye fonksiyonları içerisinde biz taban sınıfın public ve protected bölümlerindeki elemanlara doğrudan erişebiliriz. 3) Taban sınıfın private bölümü tam olarak korunmuştur. Türemiş sınıf tarafından erişilemez. Burada taban sınıfın protected bölümüne kendi sınıfı dışında yalnızca türemiş sınıfın üye fonksiyonları tarafından erişilebildiğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: void foo(); int m_x; protected: void bar(); int m_y; private: void tar(); int m_z; }; class B : public A { public: void car(); int m_k; protected: void zar(); int m_r; private: void mar(); int m_m; }; void A::foo() { cout << "A::foo" << endl; } void A::bar() { cout << "A::bar" << endl; } void A::tar() { cout << "A::tar" << endl; } void B::car() { cout << "B::car" << endl; m_y = 300; // geçerli } void B::zar() { cout << "A::zar" << endl; bar(); // geçerli } void B::mar() { cout << "B::mar" << endl; tar(); // geçersiz! } int main() { B b; b.foo(); // geçerli b.m_x = 10; // geçerli b.car(); // geçerli b.m_k = 20; // geçerli return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- protected türetmesinde taban sınıfın "public ve protected bölümleri türemiş sınıfın protected bölümüymüş gibi" işlem görmektedir. Taban sınıfın private bölümü tam olarak korunmuştur. Türemiş sınıf tarafından erişilemez. Şekilsel olarak bu durumu şöyle ifade edebiliriz: Taban Sınıf Türemiş Sınıf public ----> protected protected ----> protected private X protected türetmesinden çıkan sonuçlar şunlardır: 1) Dışarıdan türemiş sınıf nesnesi, göstericisi ya da referansı yoluyla taban sınıfının hiçbir bölümüne erişemeyiz. 2) Türemiş sınıfın üye fonksiyonları içerisinde biz taban sınıfın public ve protected bölümlerindeki elemanlara doğrudan erişebiliriz. 3) Taban sınıfın private bölümü tam olarak korunmuştur. Türemiş sınıf tarafından erişilemez. Pekiyi public ve protected türetmesi arasındaki fark nedir? İşte public türetmesinde dışarıdan türemiş sınıf türünden nesne, referansya da gösterici yoluyla taban sınıfın public bölümüne erişilebilirken, protected türetmesinde erişilememektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: void foo(); int m_x; protected: void bar(); int m_y; private: void tar(); int m_z; }; class B : protected A { public: void car(); int m_k; protected: void zar(); int m_r; private: void mar(); int m_m; }; void A::foo() { cout << "A::foo" << endl; } void A::bar() { cout << "A::bar" << endl; } void A::tar() { cout << "A::tar" << endl; } void B::car() { cout << "B::car" << endl; m_y = 300; // geçerli } void B::zar() { cout << "A::zar" << endl; bar(); // geçerli } void B::mar() { cout << "B::mar" << endl; } int main() { B b; b.foo(); // geçersiz! b.m_x = 10; // geçersiz! b.car(); // geçerli b.m_k = 20; // geçerli return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- private türetmesinde taban sınıfın "public ve protected bölümleri türemiş sınıfın private bölümüymüş gibi" işlem görmektedir. Yine taban sınıfın private bölümü tam olarak korunmuştur, türemiş sınıf tarafından erişilemez. Taban Sınıf Türemiş Sınıf public ----> private protected ----> private private X private türetmesinden çıkan sonuçlar sanki protected türetmesinden çıkan sonuçlarla aynı gibidir: 1) Dışarıdan türemiş sınıf nesnesi, göstericisi ya da referansı yoluyla taban sınıfının hiçbir bölümüne erişemeyiz. 2) Türemiş sınıfın üye fonksiyonları içerisinde biz taban sınıfın public ve protected bölümlerindeki elemanlara doğrudan erişebiliriz. 3) Taban sınıfın private bölümü tam olarak korunmuştur. Türemiş sınıf tarafından erişilemez. Her ne kadar protected türetmesiyle private türetmesinden çıkan sonuçlar aynı gibi gözküyorsa da bir dizi türetme yapıldığında arada farklılıklar oluşabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir dizi türetme yapıldığında yukarıdaki kurallar geçişli biçimde devam etmektedir. Aşağıdaki gibi bir türetme şeması olsun: class A { //... }; class B : public A { //... }; class C : public B { //... }; Burada C sınıfının bir üye fonksiyonu içerisinde biz hem A'nın public ve protected elemanlarını hem de B'nin public ve protected elemanlarını doğrudan kullanabiliriz. Yine dışarıdan C nesnesi, göstericisi ya da referansı yoluyla hem A'nın hem B'nin hem de C'nin public elemanlarına erişebiliriz. Çünkü A'nın public bölümü B'nin public bölümü gibi olduğuna göre, B'nin public bölümü de C'nin public bölümü gibi olduğuna göre sanki A ve B'nin public bölümleri C'nin public bölümü gibi işlem görecektir. Benzer biçimde A'nın protected bölümü B'nin protected bölümü gibi olduğuna göre B'nin protected bölümü de C'nin protected bölümü gibi olduğna göre A'nın ve B'nin protected bölümleri C'nin protected bölümü gibi işlem görecektir. Şimdi B sınıfı A sınıfından private türetilmiş olsun: class A { //... }; class B : private A { //... }; class C : public B { //... }; Burada biz artık C'nin üye fonksiyonları içerisinde A'nın public ve protected elemanlarına erişemeyiz. Çünkü A'nın public ve protected bölümleri artık B'nin private bölümüymüş gibi işlem görmektedir. B'nin private bölümüne de C tarafından erişilememektedir. Şimdi B sınıfı A'dan protected türetilmiş olsun: class A { //... }; class B : protected A { //... }; class C : public B { //... }; Biz burada artık C'nin üye fonksiyonları içerisinde A'nın ve B'nin public ve protected bölümlerindeki elemanları doğrudan kullanabiliriz. Görüldüğü gibi protected ve private türetmesi arasında bu tür durumlarda semantik farklılık oluşmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 62. Ders 01/04/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tüm bunların ışığı altında sınıfların protected bölümleri için şunlar şöylenebilir: Sınıfların protected bölümleri türemiş sınıfların üye fonksiyonları tarafından doğrudan erişilebilen ancak dışarıdan erişilemeyen bölümüdür. Biz bir elemanı protected bölüme neden yerleştiririz? İşte bizim yazdığımız sınıftan bir türetme yapılabileceğini öngörüp türemiş sınıfı yazanlara erişim kolaylığı sağlamak için bazı elemanları protected bölüme yerleştirebilriz. Ancak sınıfın protected bölümünde bir değişiklik yapıldığında yalnızca o sınıfın içinin değil tüm türemiş sınıfların içlerinin yeniden yazılması gerekir. Bu nedenle bazı teorisyenler protected bölümün hiç kullanılmaması gerektiğini de düşünmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir sınıfı yazanlar onu kullanacak kişiler için dokümante etmek isteseler sınıfın hangi bölümlerindeki elemanları dokümante etmeliler? - Sınıfın public bölümü herkes tarafından kullanılabileceğine göre public bölümün dokümante edilmesi gerekir. - Sınıfın protected bölümü sınıftan türetme yapanlar tarafında kullanılabildiği için protected bölümün de dokümante edilmesi gerekir. - Sınıfın private bölümüne dışarıdan hiçbir biçimde erişilemeyeceği için private bölümün dokümante edilmesi gerekmez. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın "taban sınıfları (base classes)" denildiğinde onun bütün taban sınıfları anlaşılır. Bir sınıfın "doğruan taban sınıfları (direct base classes)" denildiğinde ise o sınıfın hemen bir yukarısındaki taban sınıfları anlaşılır. Bir sınıfın "dolaylı taban sınıfları (indirect base classes)" da o sınıfın doğrudan taban sınıflarının taban sınıflarıdır. Örneğin: A B C D Burada D'nin taban sınıfları A, B ve C'dir. D'nin doğrudan taban sınıfı (direct base class) C'dir. D'nin "dolaylı taban sınıfları (indirect base classes)" B ve A'dır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfların veri elemanlarının genellikle private bölümde tutulduğunu belirtmiştik (data hiding). Pekiyi biz türemiş sınıf türünden bir nesne yarattığımızda türemiş sınıf nesnesi hem türemiş sınıfın hem de taban sınıfın veri elemanlarını içerdiğine göre ve türemiş sınıf içerisinden taban sınıfın private bölümüne erişilemeyeceğine göre taban sınıf veri elemanlarına nasıl ilkdeğerleri verilecektir? Örneğin B sınıfı A sınıfından türetilmiş olsun: A B Şimdi B sınıfı türünden bir nesne yaratalım: B b; Burada b nesnesi hem A'nın veri elemanlarını hem de B'nin veri elemanlarını içerecektir. Ancak yukarıdaki gibi türemiş sınıf türünden nesne yaratıldığında türemiş sınıfın yapıcı fonksiyonu çağrılacaktır. Pekiyi bu örnekte örnekte B'nin yapıcı fonksiyonu içerisinde A'nın private bölümüne erişilemeyeceğine göre A'nın private veri elemanları nasıl ilkdeğerlerini alacaktır? İşte C++'ta (Java C# gibi diğer dilklerde de böyle) türemiş sınıf türünden bir nesne yaratıldığında türemiş sınıfın yapıcı fonksiyonu çağrılır. Ancak türemiş sınıfın yapıcı fonksiyonu da otomatik olarak (yani biz istesek de istemesek de) taban sınıfın yapıcı fonksiyonunu çağırmaktadır. Böylece taban sınıfın private veri elemanlarına taban sınıfın yapıcı fonksiyonu tarafından ilkdeğerleri verilir. Örneğin: class A { public: A() { m_x = 10; } private: int m_x; }; class B : public A { public: B() { m_y = 20; } private: int m_y; }; //... B b; Burada b nesnesi için türemiş sınıfın default yapıcı fonksiyonu çağrılacaktır. Ancak B sınıfının default yapıcı fonksiyonu da kendi içerisinde otomatik olarak A sınıfının default yapıcı fonksiyonunu çağıracaktır. Dolayısıyla b nesnesinin kendi private veri elemanlarına B sınıfının yapıcı fonksiyonu, b nesnesinin A sınıfına ilişkin private veri elemanlarına da A sınıfının yapıcı fonksiyonu değer atayacaktır. Burada iki noktayı bilmemiz gerekmektedir: 1) Türemiş sınıfın yapıcı fonksiyonu tam olarak hangi noktada taban sınıfın yapıcı fonksiyonunu çağırmaktadır? 2) Türemiş sınıfın yapıcı fonksiyonu taban sınıfın hangi yapıcı fonksiyonunu çağırmaktadır? Türemiş sınıfın yapıcı fonksiyonu akış yapıcı fonksiyonun ana bloğuna girmeden taban sınıfın yapıcı fonksiyonunu çağırmaktadır. Bu çağırma tipik olarak derleyicinin "türemiş sınıfın yapıcı fonksiyonunun ana bloğunun başına yerleştirdiği gizli bir çağırma kodu yoluyla" yapılmaktadır. Böylece fiilen önce taban sınıfın yapıcı fonksiyonu sonra türemiş sınıf yapıcı fonksiyonu çalıştırılmış olur. Örneğin B sınıfının A sınıfından türeldiğini düşünelim: B::B() { // A sınıfının yapıcı fonksiyonu çalıştırıldıktan sonra akış artık türemiş sınıfın ana bloğunda ilerlemektedir } Taban sınıfın birden fazla yapıcı fonksiyonu varsa türemiş sınıfın yapıcı fonksiyonu taban sınıfın hangi yapıcı fonksiyonunu çağıracaktır? İşte burada da yine daha önce görmüş olduğumuz MIL sentaksı (ctor-initializer) devreye girmektedir. Eğer türemiş sınıfın yapıcı fonksiyonunun MIL sentaksında taban sınıf ismi ve argüman listesi belirtilirse taban sınıfın o argüman listesine uygun yapıcı fonksiyonu çağrılacaktır. Eğer MIL sentaksında hiç taban sınıf ismi belirtilmezse taban sınıfın default yapıcı fonksiyonu çağrılacaktır. Tabii bu durumda taban sınıfın default yapıcı fonksiyonunun var olması ver erişilebilir olması gerekmektedir. (Türemiş sınıf taban sınıfın public ve protected bölümlerine erişebildiğine göre taban sınıfın default yapıcı fonksiyonun bu bölümlerde olması gerekir.) Eğer taban sınıfın ilgili yapıcı fonksiyonu yoksa ya da erişilemez durumdaysa derleme zamanında hata oluşacaktır. Örneğin B sınıfı A sınıfından türetilmiş olsun: B::B() : A(10) { //... } Burada artık B sınıfının default yapıcı fonksiyonu A sınıfının int parametreli yapıcı fonksiyonunu çağıracaktır. Tabii yine türemiş sınıfın parametreleri buradaki çağrıya argüman yapılabilir. Örneğin: B::B(int x, int y) : A(x) { //... } Burada B sınıfı türündne nesnenin şöyle yaratıldığını varsayalım: B b{10, 20}; Artık 10 değeri A'nın yapıcı fonksiyonuna argman olarak geçirilecektir. Aşağıda türemiş sınıfın taban sınıfın yapıcı fonksiyonunu çağırmasına yönelik bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: A(); A(int x); int x() const { return m_x; } private: int m_x; }; class B : public A { public: B(); B(int x, int y); int y() const { return m_y; } private: int m_y; }; A::A() { m_x = 10; } A::A(int x) { m_x = x; } B::B() { m_y = 20; } B::B(int x, int y) : A(x) { m_y = y; } int main() { B b{10, 20}; cout << b.x() << ", " << b.y() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir dizi türetme yapıldığı durumda her sınıfın yapıcı fonksiyonu kendi doğrudan taban sınıflarının yapıcı fonksiyonlarını çağırmaaktadır. Örneğin B sınıfı A sınıfından, C sınıfı da B sınıfından türetilmiş olsun: A B C Burada C'nin doğrudan taban sınıfı B, B'nin doğrudan taban sınıfı A'dır. MIL sentaksında yalnızca sınıfın doğrudan taban sınıfları belirtilebilmektedir. Örneğin: A() { //... } B::B(...) : A(...) { //... } C::C(...) : B(...) { //... } Görüldüğü gibi burada B'nin yapıcı fonksiyonu A'nın yapıcı fonksiyonunu C'nin yapıcı fonksiyonu da B'nin yapıcı fonksiyonunu çağırmaktadır. Aşağıdaki gibi bir tanımlama geçerli değildir: C::C(...) : A(...), B(...) // geçersiz! { //... } Yukarıda da belirttiğimiz gibi MIL sentaksında her sınıf yalnızca kendi sınıfının doğrudan taban sınıfının yapıcı fonksiyonunu çağırabilmektedir. Bir dizi türetme söz konusu olduğunda yapıcı fonksiyonların fiilen yukarıdan aşağıya doğru sırasıyla çağrıldığına dikkat ediniz. Örneğin: C c; Burada c nesnesi için C'nin default yapıcı fonksiyonu çağrılacaktır. C'nin default yapıcı fonksiyonu henüz ana bloğa girmeden B'nin yapıcı fonksiyonunu çalıştırmış olacaktır. B'nin yapıcı fonksiyonu da yine akış ana bloğa girmeden A'nın yapıcı fonksiyonunu çalıştırmış olacaktır. Böylece aslında yapıcı fonksiyonlar yukarıdan aşağıya doğru A, B, C sırasına göre çalıştırılacaktır. Aşağıda bu duruma bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: A(int x); int x() const { return m_x; } private: int m_x; }; class B : public A { public: B(int x, int y); int y() const { return m_y; } private: int m_y; }; class C : public B { public: C(int x, int y, int z); int z() const { return m_z; } private: int m_z; }; A::A(int x) { m_x = x; cout << "A(int) constructor" << endl; } B::B(int x, int y) : A(x) { m_y = y; cout << "B(int, int) constructor" << endl; } C::C(int x, int y, int z) : B(x, y) { m_z = z; cout << "C(int, int, int) constructor" << endl; } int main() { C c(100, 200, 300); cout << c.x() << ", " << c.y() << ", " << c.z() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türemiş sınıf türünden bir nesne yaratıldığında fiili olarak önce taban sınıfın yapıcı fonksiyonunun sonra türemiş sınıfın yapıcı fonksiyonunun çalıştırıldığını gördük. Pekiyi bunun anlamı nedir? İşte türemiş sınıf taban sınıf elemanlarını kullanabildiğine göre akış türemiş sınıfın yapıcı fonksiyonunun ana bloğuna geldiğinde taban sınıf veri elemanlarının ilkdeğer almış olması gerekir. Bu nedenle fiilen önce taban sınıf sonra türemiş sınıf yapıcı fonksiyonları çalıştırılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi C++'ta her zaman yapıcı fonksiyonlarla yıkıcı fonksiyonlar ters sırada çalışmaktadır. Bu durum türetme için de geçerlidir. Türemiş sınıf türünden bir nesne yaratıldığında fiilen önce taban sınıfın sonra türemiş sınıfın yapıcı fonksiyonları çalıştırılıyordu. İşte bu durumda türemiş sınıf nesnesi için yıkıcı fonksiyon söz konusu olduğunda fiilen önce türemiş sınıfın sonra taban sınıfın yıkıcı fonksiyonları çalıştırılmaktadır. Örneğin aşağıdaki gibi bir türetme şeması olsun: A B C Burada C sınıfı türünden bir nesne yaratalım. Fiilen önce A sınıfının sonra B sınıfının sonra da C sınıfının yapıcı fonksiyonları çalıştırılacaktır. İşte bu nesne için yıkıcı fonksiyonlar da ters sırada yani C, B, A sırasına göre (yani önce C'nin sonra B'nin sonra A'nın) çalıştırılacaktır. Derleyiciler tipik olarak "türemiş sınıfın yıkıcı fonksiyonunun ana bloğunun sonunda taban sınıfın yıkıcı fonksiyonunu gizli bir çağırma kodu yoluyla" çağırmaktadır. Örneğin B sınıfı A sınıfından türetilmiş olsun: A B Şimdi biz B sınıfı türünden bir nesne yaratmış olalım. Nesne yaşamını kaybederken B sınıfının yıkıcı fonksiyonu çalışacaktır. B sınıfının yıkıcı fonksiyonu da ana bloğun sonunda A sınıfının yıkıcı fonksiyonunu otomatik olarak çağıracaktır. Örneğin: B::~B() { // B sınıfının yıkıcı fonksiyonundaki kodlar } Her sınıf kendi sınıfının yıkıcı fonksiyonunu ana bloğun sonunda gizlice çağıracağına göre fiilen çağrılma sırası ters sırada olacaktır. Pekiyi buradaki ters sıranın anlamı ne olabilir? Türemiş sınıfın yıkıcı fonksiyonu işlemlerini yaparken taban sınıfın elemanlarını da kullanıyor olabilir. Bu durumda onların daha sonra destruct edilmesi daha anlamlıdır. Aşağıda yapıcı ve yıkıcı fonksiyonların ters sırada çağrılmasına bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: A(int x); ~A(); int x() const { return m_x; } private: int m_x; }; class B : public A { public: B(int x, int y); ~B(); int y() const { return m_y; } private: int m_y; }; class C : public B { public: C(int x, int y, int z); ~C(); int z() const { return m_z; } private: int m_z; }; A::A(int x) { m_x = x; cout << "A(int) constructor" << endl; } A::~A() { cout << "A destructor" << endl; } B::B(int x, int y) : A(x) { m_y = y; cout << "B(int, int) constructor" << endl; } B::~B() { cout << "B destructor" << endl; } C::C(int x, int y, int z) : B(x, y) { m_z = z; cout << "C(int, int, int) constructor" << endl; } C::~C() { cout << "C destructor" << endl; } int main() { C c(100, 200, 300); cout << c.x() << ", " << c.y() << ", " << c.z() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir sınıf hem başka bir sınıftan türetilmiş olsun hem de sınıfın başka sınıf türünden veri elemanları bulunyor olsun. Bu durumda yapıcı ve yıkıcı fonksiyonların çağrılma sıraları nasıl olacaktır? İşte böyle bir durumda C++'ta her zaman önce taban sınıfın yapıcı fonksiyonu önce çalıştırılır, sonra elemanlara ilişkin sınıfların yapıcı fonksiyonları sınıf bildirimindeki sıraya göre çalıştırılır. Sonra da akış türemiş sınıf yapıcı fonksiyonun ana bloğunda ilerler. Tabii yine işlemler derleyicinin türemiş sınıfın yapıcı fonksiyonunun ana bloğunun başına yerleştirdiği gizli çağırma kodları yoluyla yapılmaktadır. Örneğin B sınıfı A sınıfından türemiş olsun. Aynı zamanda da B sınıfının K sınıfı türünden m_k isimli bir veri elemanının olduğunu varsayalım: class B(...) : A(...), m_k(...) { //.... } Burada MIL sentaksındaki sıra ne olursa olsun önce A taban sınıfı için yapıcı fonksiyon çağrılır, sonra m_k nesnesi için yapıcı fonksiyon çağrılır. Bundan sonra akış B'nin yapıcı fonksiyonunun ana bloğunda ilerler. Derleyici gizli çağırma kodlarını şöyle yerleştirecektir: B::B(...) { // B sınıfının yapıcı fonksiyondaki kodlar } Burada MIL sentaksında A belirtilmemiş olsa bile A için default yapıcı fonksiyonu yine K'nın yapıcı fonksiyonundan önce çalıştırılacaktır. Yani derleyici aşağıdakine benzer bir kod üretecektir: B::B(...) { // B sınıfının yapıcı fonksiyondaki kodlar } Tabii yıkıcı fonksiyonların çağrılma sırası yine ters biçimde olacaktır. Bu durumda B nesnesi için yıkıcı fonksiyon çağrıldığında fiilen önce B sınıfının yıkıcı fonksiyonu, sonra m_k nesnesi için K sınıfının yıkıcı fonksiyonu ve en sonunda da nesnenin taban kısmı için A sınıfının yıkıcı fonksiyonu çağrılacaktır. Yıkıcı fonksiyonların çağrılmasının da derleyicinin yıkıcı fonksiyonunun sonuna yerleştirdiği gizli çağırma kodları yoluyla yapıldığını anımsayınız. Bu durumda B sınıfının yıkıcı fonksiyonu derleyici tarafından aşağıdaki gibi oluşturulacaktır: B::~B() { // B sınıfının yıkıcı fonksiyonundaki kodlar } Aşağıdaki örnekte B sınıfı A sınıfından türetilmiştir ve K sınıfı türünden bir veri elemanına sahiptir. Kodu çalıştırarak yapıcı ve yıkıcı fonksiyonların çağrılma sıralarını gözlemleyiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class K { public: K(int k); ~K(); int k() const { return m_k; } private: int m_k; }; class A { public: A(int x); ~A(); int x() const { return m_x; } private: int m_x; }; class B : public A { public: B(int x, int y, int z); ~B(); int y() const { return m_y; } private: int m_y; K m_k; }; K::K(int k) { m_k = k; cout << "K(int) constructor" << endl; } K::~K() { cout << "K destructor" << endl; } A::A(int x) { m_x = x; cout << "A(int) constructor" << endl; } A::~A() { cout << "A destructor" << endl; } B::B(int x, int y, int z) : m_k(z), A(x) { m_y = y; cout << "B(int, int) constructor" << endl; } B::~B() { cout << "B destructor" << endl; } int main() { B b{10, 20, 30}; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 63. Ders 03/04/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta çoklu türetmenin (multiple inheritance) olduğunu söylemiştik. Yani bir sınıfın birden fazla taban sınıfı olabiliyordu. (Java ve C# gibi bazı dillerde çoklu türetmenin olmadığını anımsayınız.) Pekiyi çoklu türetmede türemiş sınıfın veri eleman organizasyonu nasıl olacaktır? Örneğin D sınıfı hem A'dan hem B'den hem de C'den türetilmiş olsun: A B C D Bu durumda D sınıfı türünden bir nesne yaratıldığında bu nesne hem A'nın hem B'nin hem de C'nin veri elemanlarını (static olmayan veri elemanlarını) içerecektir. Pekiyi bunun bellekteki organizasyonu nasıldır? İşte standartlar yine bu konuda da kesin bir belirlemede bulunmamıştır. Ancak derleyiciler tipik olarak sınıfların veri elemanlarını soldan sağa doğru alt alta (düşük adresten yüksek adrese) dizmektedir. Örneğin: D d; Burada pek çok derleyici d nesnesinin veri elemanlarını aşağıdaki gibi dizmektedir: A-dat B-dat C-dat D-dat Mademki standartlar bu tür durumlarda açık bir belirlemede bulunmamaktadır o halde farklı derleyicilerde üretilmiş olan kodları aynı projede kullanamayız. Örneğin bir grup sınıfı Microsoft'un C++ derleyicisinde derleyip kütüphane haline getirdiysek bu kütüphaneyi GNU'nun C++ derleyicisi olan g++'dan kullanmaya çalışmamalıyız. Çünkü C++'ta pek çok unsur derleyicileri yazanların isteğine bırakılmıştır. Bütün kodların aynı derleyicide derlenmesi bir sorun oluşturmayacaktır. Ancak kodun farklı bölümlerinin farklı derleyicilerde derlenmesi muhtemelen sorunlar oluşturacaktır. (Aslında yalnızca C++ için değil C için bile farklı derleyicilerin bir arada kullanılması çeşitli uyumsuzluklara yol açabilmektedir. Üretilen amaç dosya formatı aynı olsa bile derleyiciler farklı ABI (Application Binary Interface) kullanıyor olabilir. Farklı isim dekorasyonlarını kullanıyor olabilir. Ayrıca modern amaç dosya formatlarının derleyici tarafından kullanılan bölümleri (sections) de farklı olabilmektedir.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi çoklu türetme durumunda yapıcı ve yıkıcı fonksiyonların çağrılma sıraları nasıldır? Yukarıda da çeşitli defalar belirttiğimiz gibi C++'ta yapıcı fonksiyonların çağrılma sırası her zaman bildirimdeki sıraya göre yapılmaktadır, MIL sentaksındaki sıranın hiçbir önemi yoktur. Örneğin aşağıdaki gibi bir çoklu türetme olsun: A B C D Burada D sınıfı hem A'dan hem B'den hem de C'den türetilmiş durumdadır. Şimdi D sınıfı türünden bir nesne yaratalım: D d; Buradaki D nesnesinin toplamda 4 parçası vardır. Her parçaya kendi sınıfının yapıcı fonksiyonları tarafından ilkdeğerleri verilecektir. D sınıfının bildiriminin şöyle yapıldığını varsayalım: class D : public A, public B, public C { //... }; İşte d nesnesi için fiilen önce A sınıfının, sonra B sınıfının, sonra C sınıfının sonra da D sınıfının yapıcı fonksiyonları çalıştırılacaktır. Çünkü bildirimdeki sıra A, B, C biçimindedir. Tabii bu sınıfların hangi yapıcı fonksiyonlarının çağrılacağı yine MIL sentaksındaki argüman yapısına göre belirlenecektir. Örneğin: D::D(...) : C(...), B(...), A(....) { //... } MIL sentaksındaki sıranın bir önemi olmadığı için burada yine önce A sınıfının argüman yapısına uygun yapıcı fonksiyonu, sonra B sınıfının argüman yapısına uygun yapıcı fonksiyonu ve sonra da C sınıfının argüman yapısına uygun yapıcı fonksiyonu çağrılacaktır. Yukarıdaki D sınıfın yapıcı fonksiyonu için derleyici aşağıdaki gibi bir kod üretecektir: D::D(...) { // D sınıfının yapıcı fonksiyonundaki kodlar } Tabii MIL sentaksında herhangi bir doğrudan taban sınıf belirtilmezse bu sırada bir değişiklik söz konusu olmamaktadır. Belirtilmeyen sınıflar için onların default yapıcı fonksiyonları çağrılmaktadır. Örneğin: D::D(...) : B(...) { //... } Burada önce A sınıfının default yapıcı fonksiyonu, sonra B sınıfının argüman listesinde belirtilen yapıcı fonksiyonu ve sonra da C sınıfının default yapıcı fonksiyon çağırlacaktır. Derleyici bu durum için şöyle bir kod üretecektir: D::D(...) { // D sınıfının yapıcı fonksiyonundaki kodlar } Yukarıdaki örnekte D sınıfı türünden d nesnesi için yıkıcı fonksiyonlar da ters sırada çağrılacaktır. Yani fiilen önce D sınıfının yıkıcı fonksiyonu, sonra C sınıfının yıkıcı fonksiyonu, sonra B sınıfının yıkıcı fonskyionu ve en sonunda da A sınıfının yıkıcı fonksiyonu çalıştırılacaktır. Derleyici bunu sağlamak için aşağıdaki gibi bir kod üretecektir: D::~D(...) { // D sınıfının yıkıcı fonksiyonundaki kodlar } Tabii çoklu türetmede aslında türemiş sınıfın doğrudan taban sınıfları da başka sınıflardan türetilmiş olabilir. Bu durumda yapıcı fonksiyonlar kol kol çağrılmaktadır. (Derleyicinin ürettiği kodların bunu zorunlu hale getirdiğine de dikkat ediniz.) Örneğin aşağıdaki gibi bir türetme şeması olsun: X Y Z A B C D Burada D sınıfı türünden bir nesne yaratatıldığında yapıcı fonksiyonlar şu sıraya göre fiilen çalıştırılacaktır: X, A, Y, B, Z, C, D. Tabii yıkıcı fonksiyonlar da yine fiilen ters sırada şöyle çağrılacaktır: D, C, Z, B, Y, A, X. Hem çoklu türetme varsa hem de sınıfın başka sınıf türünden veri elemanları varsa bu durumda önce taban sınıflar için yapıcı fonksiyonlar yukarıda belirtildiği kurala göre çağrılacak, sonra veri elemanları için yapıcı fonksiyonlar sınıf bildiriminde belirtilen sıraya göre çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi C++'ta bir nesnenin adresi aynı türden bir göstericiye atanabilmektedir. İstisna olarak nesnelerin adresleri her zaman void bir göstericiye atanabilmektedir. Ancak C++'ta "türemiş sınıf adresi taban sınıf adresine" otomatik olarak dönüştürülebilmektedir. Başka bir deyişle C++'ta türemiş sınıf türünden bir nesnenin adresi onun herhangi bir taban sınıfı türünden göstericiye doğrudan atanabilmektedir. Örneğin B sınıfı A sınıfından türetilmiş olsun: class A { //... }; class B : public A { //... }; Burada B sınıfı türünden bir nesnenin adresi A sınıfı türünden bir göstericiye hiç tür dönüştürmesi yapılmadan doğrudan atanabilir. Örneğin: B b; A *pa; pa = &b; // geçerli, tür dönüştürmesine gerek yok Türemiş sınıftan taban sınıfa otomatik adres dönüştürmesi standartların "standard conversions" bölümünde ele alınmaktadır. Bu dönüştürme referans yoluyla da yapılabilmektedir. Yani taban sınıf türünden bir referans (burada sol taraf değeri referansı) türemiş sınıf türünden bir nesne ile ilkdeğer verilerek tanımlanabilmektedir. Örneğin: B b; A &r = b; // geçerli Yukarıdaki durumun tersi yani "taban sınıf nesnesinin adresinin doğrudan türemiş sınıf türünden göstericiye atanması" geçerli ve anlamlı değildir. Örneğin: A a; B *pb = &a; // geçerli değil! Ya da örneğin: A a; B &r = a; // geçerli değil! Otomatik adres dönüştürmesinin "türemişten tabana" doğru olduğuna, "tabandan türemişe" doğru olmadığına dikkat ediniz. Bir dizi türetme söz konusu olduğunda herhangi bir türemiş sınıf türünden nesnenin adresi onun herhangi bir taban sınıfı türünden göstericiye atanabilmektedir. Örneğin aşağıdaki gibi bir türetme şeması olsun: A B C D Burada D sınıfı türünden bir nesnenin adresi A sınıfı türünden, B sınıfı türünden ya da C sınıfı türünden bir göstericiye doğrudan atanabilir. Benzer biçimde A sınıfı, B sınıfı ya da C sınıfı türünden bir referans D sınıfı türünden bir nesne ile ilkdeğer verilerek tanımlanabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi türemiş sınıf türünden taban sınıf türüne otomatik (implicit) adres dönüştürmesinin anlamı denir? Türemiş sınıf nesnesinin adresi taban sınıf türünden bir göstericiye ya da referansa atandığında bu gösterici ya da referans bağımsız bir taban sınıf nesnesini değil türemiş sınıf nesnesinin taban sınıf parçasını gösteriyor durumda olur. Örneğin: class A { public: //... int m_x; }; class B : public A { public: //... int m_y; }; A *pa; B b; Burada b nesnesinin veri eleman organizasyonu şöyle olacaktır: A-dat B-dat Daha açık bir gösterimle şöyle olacaktır: m_x m_y Biz b nesnesinin adresini aldığımızda aslında o adreste nesnein taban sınıf kısmı vardır. Örneğin: pa = &b; Burada pa göstericisi olmayan bir nesneyi göstermemektedir. b nesnesinin A parçasını göstermektedir. Yani pa göstericisi ile işlem yaptığımızda biz aslında B nesnesinin A kısmı üzerinde işlem yapmış oluruz. Bunun bir sakıncası yoktur. Çünkü pa göstericisinin gösterdiği yerde bir A nesnesi vardır. Ancak bu A nesnesi bağımsız bir A nesnesi değil b nesnesinin A parçasıdır. Şimdi biz bu pa göstericisi ile nesnenin m_x veri elemanını değiştirelim: pa->m_x = 100; Biz burada aslında b nesnesinin m_x elemanını değiştirmiş olmaktayız. Özetleyecek olursak, biz türemiş sınıf nesnesinin adresini taban sınıf türünden bir göstericiye ya da referansa atadıktan sonra o gösterici ya da referans ile onun gösterdiği yeri kullandığımızda aslında türemiş sınıf nesnesinin taban sınıf kısmını kullanmış olmaktayız. Aşağıda bu durumu açıklayan basit bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: int m_x; }; class B : public A { public: int m_y; }; int main() { B b; A *pa; b.m_x = 10; b.m_y = 20; cout << b.m_x << ", " << b.m_y << endl; pa = &b; pa->m_x = 100; cout << b.m_x << ", " << b.m_y << endl; A &r = b; r.m_x = 200; cout << b.m_x << ", " << b.m_y << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de neden yukarıdaki işlemin tersinin geçerli olmadığına bakalım. Eğer taban sınıf türünden bir nesnenin adresi türemiş sınıf türünden bir gösterici ya da referansa atanabilseydi (bu durum geçerli değildir) bu durumda "aslında var olmayan veri elemanalarına erişme gibi bir durum oluşurdu"" ki bu da tanımsız davranış anlamına gelirdi. Örneğin yine aşağıdaki gibi bir türetme şeması söz konusu olsun: A B Biz de A sınıfı türünden bir nesnenin adresini B sınıfı türünden bir göstericiye atamış olalım (bu durum aslında geçerli değildir): A a; B *pb; pb = &a; // geçerli değil ancak geçerli olduğunu varsayalım Normal olarak B türünden bir gösterici ya da referans ile biz A'nın veri elemanlarını da B'nin veri elemanlarını da kullanabiliriz. O halde pb göstericisi ile biz hem A'nın hem de B'nin veri elemanlarını kullanabiliriz. Ancak pb'nin gösterdiği yerde A'nın veri elemanları vardır fakat B'nin veri elemanları yoktur. Eğer böyle bir atama yapılabilseydi o aman biz B türünden bir gösterici ya da referans ile aslında olmayan veri elemanlarını kullanabilme olanağına sahip olacaktık. Türemiş sınıf türünden bir nesnenin adresinin taban sınıf türünden bir gösterici ya da referansa atanabilmesinin nedeni türemiş sınıf nesnesinin taban sınıf veri elemanlarını içermesindendir. Taban sınıf nesnesinin türemiş sınıf veri elemanlarını içermediğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir dizi türetme söz konusu olduğunda türemiş sınıf nesnesinin adresinin onun herhangi bir taban sınıfı türünden gösterici ya da referansa doğurdan atanabileceğini belirtmiştik. Örneğin şöyle bir türetme şeması olsun: A B C Biz burada C sınıfı türünden bir nesnenin adresini A ya da B sınıfı türünden bir gösterici ya da referansa atayabiliriz. Örneğin: C c; B *pb; pb = &c; // geçerli c nesnesinin veri eleman organizasyonu şöyle olacaktır: A-dat B-dat C-dat Şimdi biz c'nin adresini aldığımızda aslında tüm nesnenin başlangıç adresini elde ederiz. Yani o adreste önce A'nın veri elemanları, sonra B'nin veri elemanları sonra da C'nin veri elemanları bulunacaktır. Burada pb göstericisinin gösterdiği yerde A ve B'nin veri elemanları vardır. B sınıfı A sınıfından türetildiği için ve B sınıfı türünden bir nesnede de önce A sonra B veri elemanları bulunacağı için pb'nin B nesnesinin A kısmını göstermesi tamamen normal ve uygun bir durumdur. Yukarıdaki örnekte önemli bir duruma yeniden dikkatini çekmek istiyoruz: c nesnesinin B kısmı yalnızca B'nin veri elemanlarından oluşmamaktadır. Aynı zamanda A'nın veri elemanlarından da oluşmaktadır. Çünkü B sınıfı A sınıfını veri elemanı bakımından içermektedir. Aşağıda bu biçimde atamaya bir örnek verilmiştir --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: int m_x; }; class B : public A { public: int m_y; }; class C : public B { public: int m_z; }; int main() { C c; B *pb; c.m_x = 10; c.m_y = 20; c.m_z = 30; cout << c.m_x << ", " << c.m_y << ", " << c.m_z << endl; pb = &c; pb->m_x = 100; pb->m_y = 200; cout << c.m_x << ", " << c.m_y << ", " << c.m_z << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 64. Ders 15/04/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türemiş sınıf nesnesinin adresinin taban sınıf türünden bir göstericiye ya da referansa atanması bir türetme şeması üzerinde genel işlemleri yapabilen fonksiyonların yazılmasını mümkün hale getirmektedir. Örneğin aşağıdaki gibi bir türetme şeması olsun: Employee Worker Manager SalesPerson Executive Burada her sınıf aslında Employee sınıfından türetilmiştir. Yani her sınıf türünden nesnenin bir Employee kısmı vardır. Aşağıdaki gibi bir fonksiyon olsun: void disp_employee(const Employee &e); Biz bu fonksiyon ile, çalışan kişi kim olursa olsun onun temel bilgilerini görüntüleyebiliriz. Çünkü bu temel bilgiler zaten Employee sınıfı içerisindedir ve her türemiş sınıfın aslında bir Employee kısmı vardır. Örneğin: Worker w; Manager m; SalesPerson sp; Executive e; disp_employee(w); // geçerli disp_employee(m); // geçerli disp_employee(sp); // geçerli disp_employee(e); // geçerli Burada disp_employee fonksiyonunun türetme şeması üzerinde "genel işlem" yapan bir fonksiyon olduğuna dikkat ediniz. Bu fonksiyon const Employee & parametresi alsa da biz bu fonksiyonu Employee sınıfınından türetilmiş olan her nesneyle çağırabiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // employee.hpp #ifndef EMPLOYEE_HPP_ #define EMPLOYEE_HPP_ #include class Employee { public: Employee() = default; Employee(const std::string &name, const std::string &telno); std::string name() const { return m_name; } std::string telno() const { return m_telno; } void name(const std::string &name) { m_name = name; } void telno(const std::string &telno) { m_telno = telno; } private: std::string m_name; std::string m_telno; }; #endif // employee.cpp #include "employee.hpp" using namespace std; Employee::Employee(const string &name, const string &telno) : m_name(name), m_telno(telno) {} // worker.hpp #ifndef WORKER_HPP_ #define WORKER_HPP_ #include #include "employee.hpp" enum class Shift { Morning = 0, Noon = 1, Night = 2 }; class Worker : public Employee { public: Worker() = default; Worker(const std::string &name, const std::string &telno, const std::string department, Shift shift); std::string department() const { return m_department; } Shift shift() const { return m_shift; } void department(const std::string &department) { m_department = department; } void shift(Shift shift) { m_shift = shift; } const char *shift_name() const; private: std::string m_department; Shift m_shift; }; #endif // worker.cpp #include "worker.hpp" Worker::Worker(const std::string &name, const std::string &telno, const std::string department, Shift shift) : Employee(name, telno), m_department(department), m_shift(shift) {} const char *Worker::shift_name() const { static const char *shift_names[] = {"Morning", "Noon", "Night"}; return shift_names[(int)m_shift]; } // manager.hpp #ifndef MANAGER_HPP_ #define MANAGER_HPP_ #include #include "employee.hpp" class Manager : public Employee { public: Manager() = default; Manager(const std::string &name, const std::string &telno, double prim); double prim() const { return m_prim; } void prim(double prim) { m_prim = prim; } private: double m_prim; }; #endif // manager.cpp #include #include "manager.hpp" Manager::Manager(const std::string &name, const std::string &telno, double prim) : Employee(name, telno), m_prim(prim) {} // app.cpp #include #include "employee.hpp" #include "worker.hpp" #include "manager.hpp" using namespace std; void disp_employee(const Employee &e) { cout << e.name() << ", " << e.telno() << endl; } int main() { Worker w{"Ali Serce", "5555555", "Uretim", Shift::Night}; Manager m{"Necati Ergin", "1234567", 1000}; disp_employee(w); disp_employee(m); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türemiş sınıf nesnesinin adresini taban sınıf türünden gösterici ya da referansa atayabilmemiz için türetme biçiminin "public türetmesi" olması gerekir. Zaten en fazla kullanılan türetme biçiminin "public türetmesi" olduğunu anımsayınız. Örneğin: class A { //... }; class B : protected A { //... }; B b; A *pa = &b; // error! Eğer böyle atamaya izin verilseydi o zaman b yoluyla A'nın public bölümüne erişemediğimiz halde bu yolla erişebilir duruma gelirdik. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta derleyici bir isimle karşılaştığında o ismin bir bildirimini bulmak zorundadır. Bir isme ilişkin bildirimin bulunması sürecine standartlarda "isim araması (name lookup)" denilmektedir. İsim araması ikiye ayrılmaktadır: 1) Niteliksiz isim araması (unqualified name lookup) 2) Nitelikli isim araması (qualified name lookup) Düz yazılan yani ".", "->" ve "::" opereatörü olmadan yazılan isimlerin aranması niteliksiz isim arama kurallarına göre yapılır. Ancak nokta operatörünün, ok operatörünün ve :: operatörünün sol tarafındaki isimlerin aranması niteliksiz, sağ tarafındaki isimlerin aranması nitelikli isim arama kuralına göre yapılmaktadır. İsim araması sırasıyla bazı faaliyet alanlarına sırasıyla bakılarak yapılmaktadır. İsim sırasıyla çeşitli faaliyet alanlarında aranır. Eğer isim bulunursa arama devam ettirilmez. İsim ilgili tüm faaliyet alanlarında da bulunamazsa bu durumda derleme sırasında error oluşacaktır. Biz izleyen paragraflarda isim aramasını maddeler halinde açıklayacağız. Bu maddelere "else if" gibi ele alınmalıdır. Yani ancak önceki maddedeki faaliyet alanında isim bulunamazsa sonraki faaliyet alanına bakılmaktadır. C++'ta her zaman önce isim araması yapılır, sonra erişim kontrolü uygulanır. Yani arama yalnızca erişilebilen faaliyet alanlarında yapılmamaktadır. (Halbuki bazı zillerde bazı aramalar yalnızca erişilebilen faaliyet alanlarında yapılmaktadır.) İsim araması kullanılan her isim için yapılmaktadır. Tabii bildirilen isimler aranmaamaktadır. Kullanılan isimler aranmaktadır. Örneğin. int a = 10; // buradaki a aranmaz, zaten bildiriliyor cout << a << endl; // burada cout ve a isimleri aranacak std::string s; // burada std ve string isimleri aranacak ancak s aranmayacak, çünkü bildiriliyor --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Niteliksiz isimlerin aranması aşağıdaki sırada yapılmaktadır. 1) İsim bir fonksiyon içerisinde kullanılmışsa derleyici ismi önce kullanım yerinden yukarıya doğru fonksiyonun yerel blokları içerisinde içten dışa doğru arar. (Kullanım yerinden aşağıda yerel bloklarda arama yapılmadığına dikkat ediniz. ) 2) İsim bir üye fonksiyonun içerisinde kullanılmışsa o üye fonksiyonun ilişkin olduğu sınıf bildiriminin her yerinde aranır (fonksiyon inline olarak sınıf içerisinde yazılmış olsa da sınıf bildiriminin her yerine bakılmaktadır.) 3) İsim üye fonksiyonun ilişkin olduğu sınıfın taban sınıflarında aşağıdan yukarıya doğru onların sınıf bildirimlerinin her yerinde aranır. Eğer çoklu türetme söz konusuysa ismin taban sınıf kollarından yalnızca birinde bulunuyor olması gerekir. Eğer farklı kollarda isim bulunursa bu durum error oluşturur. İsmin farklı kollardaki bulunduğu düzeyin bir önemi yoktur. Yani çoklu türetmede arama kollarda herhangi bir sırada (örneğin soldan sağa) yapılmamaktadır. Başka bir deyişle bir kolun diğer kola herhangi bir üstünlüğü yoktur. 4) İsim kullanıldığı fonksiyonun içinde bulunduğu isim alanı içerisinde kullanım yerinden yukarıdaki bölgede aranır. 5) İsim kullanıldığı isim alanını kapsayan isim alanlarında içten dışa doğru kullanım yerinden yukarıdaki bölgede aranır. 6) İsim nihayet global isim alanında kullanım yerinden yukarıdaki bölgede aranır. Yukarıda da belirttiğimiz gibi C++'ta her zaman önce isim araması yapılır. Sonra erişim kontrolü uygulanır. Yani arama erişilebilen isimler arasında yapılmamaktadır. Örneğin: class A { public: void foo() { int a; a = 10; // yerel olan a } int a; }; Burada isim aranmasında yerel olan a bulunur. Örneği: class A { public: int x; }; class B : public A { private: int x; } class C : public B { public: void foo() { x = 10; // geçersiz! } }; Burada x niteliksiz isim araması sırasında başarılı bir biçimde bulunacaktır. Ancak bulunan x'e erişim mümkün olmadığı için erişim nedeniyle error oluşacaktır. Örneğin: class A { public: int x; }; class X { private: int x; }; class B : public X { //... }; class C : public A, public B { public: void foo() { x = 10; // geçersiz! } }; Burada x ismi A ve B kollarında bulunmaktadır. Bu durum error oluşturur. Bulunma düzeyinin ve erişilebilirliğin bir önemi yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 65. Ders 17/04/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önceden de belirttiğimiz gibi C++'ta bir fonksiyon ismi bir faaliyet alanında bulunduğu zaman yalnızca o faaliyet alanındaki aynı isimli fonksiyonlar overload resolution işlemine sokulmaktadır. Örneğin: class A { public: void foo(double a) { //... } }; class B : public A { void foo(int a) { //... } void bar() { foo(3.2); // B::foo çağrılır } }; Burada her ne kadar taban sınıftaki double parametreli foo fonksiyonu daha iyi bir dönüştürme sağlıyorsa da foo ismi niteliksiz isim araması kurallarına göre B sınıfında bulunacağı için "overload resolution" işlemine yalnızca B sınıfındaki foo fonksiyonları sokulacaktır. bar fonksiyonu içerisinde B sınıfındaki foo fonksiyonu çağrılacaktır. Burada erişim belirleyicisiin bir öneminin olmadığına da dikkat ediniz. Zaten yukarıda da belirttiğimiz gibi C++'ta her zaman önce isim araması yapılır. Sonra erişim kontrolü uygulanır. Örneğin: class A { public: void foo(double a) { //... } }; class B : public A { private: void foo(int a) { //... } }; class C : public B { public: void bar() { foo(3.2); // error! Taban sınıfın private bölümüne erişilemez! } }; Burada önce isim araması yapılır. İsim aramasına yalnızca B sınıfındaki foo aday fonksiyon olarak girer. Ancak seçilen bu fonksiyon sınıfın private bölümünde olduğu için erişilemez. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda belirttiğimiz gibi ".", "->" ve "::" operatörlerinin sağ tarafındaki isimler "nitelikli isim araması" kurallarına göre aranmaktadır. Nitelikli isim araması aşağıdaki gibi yapılmaktadır: 1) Nokta ya da ok operatörünün sağındaki isimler solundaki nesne ya da gösterici hangi sınıf türündense o sınıf bildirimi içerisinde sınıf bildiriminin her yerinde aranır. Eğer isim o sınıfta bulunamazsa aşağıdan yukarıya doğru isim o sınıfın taban sınıflarında onların bildirimlerinin her yerinde sırasıyla aranır. Ayrıca kapsayan isim alanlarında herhangi bir arama yapılmaz. 2) :: operatörünün sağındaki isimler eğer bu operatörün solunda bir isim alanı ismi varsa yalnızca o isim alanında kullanım yerinden yukarıdaki bölgede aranır. Kapsayan isim alanlarına bakılmaz. Eğer :: operatörünün solunda bir sınıf ismi varsa isim o sınıf bildiriminde aranır, bulunamazsa taban sınıfların bildirimlerinde de aşağıdan yukarıya doğru sırasıyla aranır. Örneğin: class A { void foo() { //... } }; class B : public A { public: void foo() { //... } }; //... B b; b.foo(); // B'deki foo Yine önce isim araması yapılır. İsim bir faaliyet alanında bulunursa overload resolution işlemine o faaliyet alanındaki fonksiyonlar sokulur. Örneğin: class A { public: void foo(const char *str) { //... } }; class B : public A { public: void foo(int a) { //... } }; //... B b; b.foo("ankara"); // error! B'de const char * parametreli bir foo fonksiyonu yok Hem türemiş sınıfta hem de taban sınıfta aynı isimli elemanlar varsa taban sınftaki elemanlara erişmek için :: operatörü kullanılabilmektedir. Örneğin: #include using namespace std; class A { public: void foo(double a) { //... } }; class B : public A { public: void foo(int a) { A::foo(3.2); // özyinelemeli çağırma değil, A::foo çağrılır foo(3.2); // dikkat! özyinelemeli çağrı } void bar() { foo(3.2); // B::foo çağrılır A::foo(3.2); // A::foo çağrılır } }; int main() { B b; b.foo(3.2); // B::foo çağrılır b.A::foo(3.2); // A::foo çağrılır return 0; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfların üye fonksiyonları ve veri elemanları static de olabilmektedir. Bir üye fonksiyonu ya da veri elemanını static yapmak için bildirimde static anahtar sözcüğü kullanılmaktadır. Örneğin: class Sample { public: void foo(); // static olmayan (nonstatic) üye fonksiyon static void bar(); // static üye fonksiyon private: int m_a; // static olmayan (nonstatic) veri elemanı static int m_b; // static veri elemanı }; O halde bir üye fonksiyon static olabilir ya da olmayabilir. Tabii static üye fonksiyonlara seyrek biçimde gereksinim duyulmaktadır. İngilizce "static üye fonksiyon" terimi "static member function" biçiminde, "static olmayan üye fonksiyon" terimi de "nonstatic member function" biçiminde ifade edilmektedir. Benzer biçimde veri elemanları da static olabilir ya da static olmayabilir. static veri elemanlarına yine seyrek bir biçimde gereksinim duyulmaktadır. ""static veri elemanı" terimi İngilizce "static data member", "static olmayan veri elemanı" terimi ise İngilizce "nonstatic data member" biçiminde ifade edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın static fonksiyonlarının protiplerinde static anahtar sözcüğü belirtilir. Ancak static anahtar sözcüğü tanımlama sırasında belirtilemez. Örneğin: class Sample { public: static void foo(); //... }; void Sample::foo() { //... } Yukarıda da görüldüğü gibi static anahtar sözcüğü yalnızca prototipte bullanılmaktadır. Ancak static fonksiyon da inline olarak sınıf içerisinde tanımlanabilir. Örneğin: class Sample { public: static void foo() // geçerli { //... } //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bazen bazı üye fonksiyonlar mantıksal bakımdan sınıfla ilişkilidir ancak sınıfın veri elemanlarını kullanmamaktadır. Yani aslında bu fonksiyonlar global fonksiyon olmaya adaydır. Ancak biz bunların sınıfla mantıksal ilgileri yüzünden bunları ilgili sınıfların içerisine yerleştirmek isteriz. Örneğin Date sınıfının içerisindeki üye fonksiyonlar sınıfın M-day, m_month ve m_year veri elemanlarını kullanmaktadır. Bir yılın artık yıl olup olmadığını test etmek için kullanılan isleap isimli bir fonksiyon mantıksal baımdan Date sınıfı ile ilişkilidir. Ancak bu fonksiyon Date sınıfının veri elemanlarını kullanmamaktadır. Bu fonksiyon parametresiyle aldığı yılın artık olup olmadığını test etmektedir. Örneğin: bool isleap(int year) { return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; } Eğer biz bu fonksiyonu Date sınıfının içerisine yerleşleştirirsek onu gerekmediği halde Date sınıfı türünden bir nesneyle çağırmak zorunda kalırız. Örneğin: class Date { public: //... bool isleap(int year); private: int m_day; int m_month; int m_year; }; //... Date d{10, 12, 2009}; if (d.isleap(2000)) { //... } Burada isleap fonksiyonu aslında d nesnesinin veri elemanlarını kullanmaktadır. Ancak bir üye fonksiyon olduğu için mecburen gerekmediği halde Date türünden bir nesneyle çağrılmak zorundadır. İşte static üye fonksiyonlar buradaki problemi çözmek için kullanılmaktadır. static üye fonksiyonlar sınıfın veri elemanlarını kullanmayan ancak mantıksal bakımdan sınıfla ilişkili olan fonksiyonlardır. static bir üye fonksiyon o sınıf türünden bir nesneyle değil sınıf ismiyle ve çözünürlük operatörü ile çağrılabilmektedir. Çrneğin: class Date { public: //... static bool isleap(int year); private: int m_day; int m_month; int m_year; }; //... if (Date::isleap(2000)) { //... } Şimdi bilgisayarın saatine bakarak o andaki tarihi bir Date nesnesi biçiminde veren today isimli bir fonksiyon yazmak isteyelim. Fonksiyonon parametrik yapısı şöyle olacaktır: Date today(); İşte bu today fonksiyonu da aslında mantıksal bakımdan Date sınıfı ile ilişkili olduğu için NYPT gereğince Date sınıfının içerisine yerleştirilmelidir. Öte yandan bu today fonksiyonu nesnenin veri elemanlarını kullanmamaktadır. Bu durumda today fonksiyonu Date sınıfının static üye fonksiyonu yapılabilir. Örneğin: class Date { public: //... static bool isleap(int year); static Date today(); private: int m_day; int m_month; int m_year; }; //... Date d; d = Date::today(); d.disp(); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfların static üye fonksiyonları içerisinde biz sınıfın static olmayan elemanlarını kullanamayız. Yani static üye fonksiyonlar içerisinde biz sınıfın static olmayan veri elemanlarını kullanamayız ve static olmayan üye fonksiyonlarını çağıramayız. Çünkü static üye fonksiyonlar bir nesneyle çağrılmamaktadır. Örneğin: class Sample { public: void foo(); static void bar(); private: int m_a; }; void Sample::bar() { foo(); // geçersiz! static üye fonskiyon static olmayan olmayan üye fonksiyonu çağıramaz m_a = 10; // geçersiz! static üye fonksiyon static olmayan veri elemanlarını kullanamaz } Ancak static bir üye fonksiyonun sınıfın başka bir static üye fonksiyonunu çağırmasında bir sakınca yoktur. Örneğin: class Sample { public: void foo(); static void bar(); static void tar(); private: int m_a; }; void Sample::tar() { //... bar(); // geçerli! Zaten herkes bar fonksiyonunu Sample::bar biçiminde çağırabilir } Sınıfın static üye fonksiyonları sınıfın static veri veri elemanlarını da kullanabilirler. Ancak static veri elemanları izleyen paragraflarda ele alınacaktır. Burada özetle şunları söyleyebiliriz. Sınıfın static üye fonksiyonları sınıfın static elemanlarını kullanabilirler ancak static olmayan elemanlarını kullanamazlar. Sınıfın static olmayan üye fonksiyonları sınıfın static üye fonksiyonlarını doğrudan çağırabilir. Örneğin: class Sample { public: void foo(); static void bar(); static void tar(); private: int m_a; }; void Sample::foo() { //... bar(); // geçerli tar(); //geçerli } Bunda bir sakınca yoktur. Zaten buradaki bar ve tar fonksiyonlarını herkes Sample::bar() ve Sampple::tar() biçiminde çağırabilmektedir. Tabii foo bu çağrımı yaparken sınıf ismini kullanmak zorunda değildir. Benzer biçimde static olmayan üye fonksiyonlar sınıfın static veri elemanlarını da doğrudan kullanabilirler. Özetle sınıfın static olmayan üye fonksiyonları hem sınıfın static olmayan elemanlarını hem de static elemanlarını kullanabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir üye fonksiyonu ne zaman static yapmalıyız? İşte eğer üye fonksiyon sınıfın static olmayan elemanlarını kullanıyorsa zaten biz onu static yapamayız. Ancak üye fonksiyon sınıfın static olmayan elemanlarını kullanmıyorsa teorik olarak biz onu static olmayan üye fonksiyon da yapabiliriz, static üye fonksiyon da yapabiliriz. Tabii bu durumda üye fonksiyonun static üye fonksiyon yapılması doğru tekniktir. Bu durumda özetle "sınıfın static olmayan elemanlarını kullanmayan üye fonksiyonların" static yapılması gerekir. Sınıfın static olmayan üye fonksiyonlarının doğrudan ya da dolaylı olarak sınıfın static olmayan veri elemanlarını kullanıyor olması gerekir. Öneğin: class Sample { public: void foo(); void bar(); private: int m_a; }; Burada foo ve bar mutlaka doğrudan ya da dolaylı olarak sınıfın static olmayan m_a veri elemanını kullanıyor olması gerekir. foo fonksiyonu şöyle tanımlanmış olsun: void Sample::foo() { bar(); } Bu durumda bar fonksiyonun m_a veri elemanını kullanıyor olması gerekir. Eğer bar fonksiyonu m_a veri elemanını kullanmıyorsa zaten bar static yapılmalıdır. bar static yapılırsa foo da static yapılmalıdır. Buradan çıkan sonuç "sınıfın static olmayan bir üye fonksiyonunun enşnde sonunda sınıfın static olmayan bir veri elemanını kullanıyor" olmasıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 66. Ders 22/04/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önce görümüş olduğumuz erişim kuralları sınıfın static elemanları için de geçerlidir. Örneğin sınıfın dışından (yani sınıfın üye fonksiyonu olmayan) bir fonksiyon içeisinden sınıfın yalnızca public bölümündeki static fonksiyonları çağırabiliriz. Ancak sınıfın bir üye fonksiyonu sınıfın herhangi bir bölümündeki static üye fonksiyonları çağırabilir. Türemiş sınıf ismiyle taban sınıfın static fonksiyonları çağrılabilir. Anımsanacağı gibi nitelikli isim aramada :: operatörünün solunda bir sınıf ismi varsa sağındaki isim önce o sınıfta bulunumazsa sırasıyla aşağıdan yukarıya doğru o sınıfın taban sınıflarında aranmaktadır. Örneğin: class A { public: static void foo(); }; class B : public A { //... }; //... B::foo(); // geçerli Burara B sınıfında foo olmadığına göre A sınıfındaki foo çağrılacaktır. Eğer B sınıfında foo olsaydı overload resolution için aday fonksiyonlar B sınıfından elde edilecekti. Yani A sınıfına hiç baılmayacaktı. Örneğin: class A { public: static void foo(); }; class B : public A { public: static void foo(int a); }; //... B::foo(); // geçersiz! B'de uygun (viable) fonksiyon yok static belirleyicisinin fonksiyonun imzası üzerinde bir etkisi yoktur. Yani aynı sınıf içerisinde biri static olan diğeri static olmayan aynı isimli ve aynı parametreik yapıya sahip birden fazla fonksiyon bulunamaz. Örneğin: class Sample { public: static void foo(); void foo(); // geçersiz! imzalar aynı }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın static üye fonksiyonları const ve volatile yapılamazlar. Anımsanacağı gibi sınıfın üye fonksiyonunun const olması o üye fonksiyonun sınıfın static olmayan veri elemanlarını değiştirmeyeceği anlamına geliyordu. static üye fonksiyonlar zaten sınıfın static olmayan veri elemanlarını kullanamadığına göre onların const (ve volatile) olmasının bir anlamı yoktur. Örneğin: class Sample { public: static void foo() const; // geçersiz! static üye fonksiyon const yapılamaz! //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın static elemanlarına sınıf ismiyle değil o sınıf türünden bir nesne, gösterici ya da referansla da erişlebilmektedir. Örneğin: class Sample { public: static void foo(); //... private: int m_a; int m_b; }; //... Sample s; Sample::foo(); // geçerli, normal çağrım s.foo(); // geçerli, decltype(s)::foo ile aynı anlamda Tabii bir nesneyle sınıfın static fonksiyonunun yukarıdaki gibi çağrılması geçerli olsa da aslında yanlış anlaşılmalara yol açabildiği için genellikle iyi bir teknik değildir. Çünkü s.foo() gibi bir ifadeyi gören kişi foo fonksiyonunun static olmayan bir fonksiyon olduğunu düşünür. Kodu gören kişi "eğer foo static bir fonksiyon olsaydı programcı onu Sample::foo() biçiminde çağrırdı" diye akıl yürütmektedir. Java'da da static elemanların bu biçimde kullanılması geçerlidir. Ancak C# böyle bir çağrımı yanlış anlaşılmalara yol yol açması nedeniyle geçersiz kabul etmektedir. Aynı çağrım göstericilerle ve referanslarla da yapılabilmektedir. Örneğin: Sample::foo(); // normal çağrım Sample s; s.foo(); // geçerli, decltype(s)::foo() decltype(s)::foo(); // geçerli, decltype(s) zaten Sample anlamına geliyor Sample *ps = &s; ps->foo(); // geçerli, Sample::foo ile aynı anlamda Pekiyi yanlış anlaşılmalara yol açıyorsa neden C++'ta static elemanların nesneyle, gösterici ya da referansla kullanımına izin verilmiştir? Eskiden C++11 öncesinde decltype operatörü yoktu. Dolayısıyla programcı bir sınıfın ismini bilmediği ancak o sınıf türünden bir nesne, gösterici ya da referansa sahip olduğu durumda nesney ilişkin sınıfın static elemanlarını ancak bu yolla kullanabiliyordu. Bu özellik şablonlarda ve isim aramasının bazı detaylarında yine de programcıya çok seyrek de olsa fayda sağlayabilmektedir. Ancak gerekmediği durumda static elemanların sınıf ismi yerine nesne, gösterici ya da referans ile kullanılması kötü bir tekniktir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfların veri elemanları da static olabilir. Sınıfların static veri elemanlarının toplamda tek bir kopyası vardır. Bunlar için ilgili sınıf türünden nesnelerde yer ayrılmamaktadır. Sınıfın static veri elemanları statik ömürlü nesnelerdir. Yani program çalıştığında yaratılırlar. Program sonlanana kadar bellekte kalırlar. Sınıfın static veri elemanları aslında global değişkenler gibidir. Yalnızca sınıf ile mantıksal bir ilgiden dolayı global yapılmayıp sınıfın içerisine yerleştirilmişlerdir. Sınıfın static veri elemanları o sınıf türünden hiç nesne yaratılmasa bile yine program başladığında yaratılmış bir biçimde bulunurlar. Örneğin: class Sample { //... private: int m_a; int m_b; static int m_c; }; Burada m_a ve m_b sınıfın static olmayan veri elemanalrıdır. mc_c ise sınıfın static veri elemanıdır. Sample sınıfı türünden nesne yaratıldığında m_c bu nesnenin içerisinde bulunmaz. m_c programın "data" alanı içerisinde (yani global değişkenler neredeyse orada) tutulmaktadır. Örneğin: Sample s; Burada s nesnesinin yalnızca m_a ve m_b parçaları vardır. Tabii farkı sınıfların aynı isimli static veri elemanları bulunabilir. Çünkü bunlar farklı sınıflarda olduğu için derleyici tarafından amaç koda farklı isimlerle yazılmaktadır. Örneğin. class Sample { //... private: //... static int m_count; }; class Mample { //... private: //... static int m_count; }; Sınıfın static veri lemanlarının toplamda tek bir kopyası olduğuna göre onlara dışarıdan sınıf ismiyle erişilebilmektedir. Tabii yine erişim kuralları static veri elemanları için de aynı biçimde uygulanmaktadır. Yani biz bir sınıfın static veri elemanına dışarıdan erişeceksek onun sınıfın public bölümünde olması gerekir. Sınıfın static veri elemanlarının ayrıca toplamda bir tane tanımlamasının da yapılması gerekmektedir. Aksi takdirde link aşamasında ilgili static elemanın bulunmamasındna dolayı error oluşacaktır. static veri elemanlarının tanımlamaları sınıfın içinde bulunduğu isim alanında ya da onu kapsayan isim alanlarında sınıf ismi ile niteliklendirilerek yapılır. Tanımlama sırasında static anahtar sözcüğü kullanılamaz. Örneğin: class Sample { //... private: //... static int m_count; }; //... int Sample::m_count; // tanımlama Tanımlama sırasında static veri elemanlarına ilkdeğer verilebilir. Örneğin: int Sample::m_count = 1; // ilkdeğer verilerek tanımlama yapılmış Pekiyi neden static veri elemanlarının ayrıca sınıfın dışında tanımlamasının yapılması gerekmektedir? Bunun birkaç nedeni vardır. En bariz nedeni şudur: static veri elemanına sahip olan bir sınıf bildiriminin bir başlık dosyasında olduğunu varsayalım. Bu sınıfında bu başlık dosyası include edilerek çeşitli dosyalardan kullanıldığını varsayalım. Eğer derleyici static veri elemanını sınıf bildiriminde gördüğünde onun için yer ayırsaydı bu durumda her kaynak dosya derlendiğinde onun için farklı yerler ayrılırdı. Link aşamasına gelindiğinde linker ilgili static nesnenin birden fazla tanımlamasıyla karşılaşırdı. Halbuki derleyici static veri elemanı için bir ayırmamaktadır. Derleyici static veri elemanı kullanıldığında "sanki global değişken extern bildirimi" yapılmış gibi kod üretmektedir. Dolayısıyla nesnenin nerde tanımlandığı linker tarafından aranmaktadır. Tabii static veri elemanlarının tanımlamalarının başlık dosyalarında yapılmaması gerekir. Sınıfın static veri elemanlarının ayrıca dışarıda tanımlanmasının diğer bariz bir nedeni de sınıflar türünden static veri elemanlarının farklı yapıcı fonksiyonlarla yaratılması gerektiği durumlardır. Örneğin: class Sample { public: // private: static string m_name; }; string Sample::m_name("ankara"); Aşağıdaki gibi bir static eleman bildirimi geçerli değildir: class Sample { public: // private: static string m_name = "ankara"; }; Yine sınıfın static veri elemanlarına sınıf türünden neslerle, göstericiler ve referanlarla da erişilebilir. Ancak yine gerekmedikçe (çok seyrek gerekebilir) sınıfın veri elemanalarına sınıfın dışından sınıf ismi ile erişilmelidir. Sınıfın static veri elemanları sınıf bildirimi içerisinde "eksik (incomplete)" bildirilebilmektedir. Örneğin sınıfın veri elemanı bir dizi ise biz dizinin uzunluğunu sınıf bildirimi içerisinde belirtmeyebiliriz, tanımlama sırasında belirtebiliriz: class Sample { //... private: //... int m_a[]; // geçerli, "incomplete" bildirim }; int Sample::m_a[10]; // artık bildirim "complete" hale getirildi Aşağıda static veri elemanlarının kullanımına ilişkin basit bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; struct Sample { //... int m_a; int m_b; static int m_c; }; int Sample::m_c = 10; int main() { cout << Sample::m_c << endl; // 10 Sample::m_c = 20; // 20 cout << Sample::m_c << endl; // 20 Sample s; cout << sizeof(s) << endl; // int 4 byte ise 8 s.m_c = 30; // s.m_c ile decltype(s)::m_c aynı anlamdadır. cout << Sample::m_c << endl; // 30 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın static üye fonksiyonlarının yalnızca sınıfın static elemanlarını kullanabildiğini, static olmayan üye fonksiyonların ise sınıfın hem static olmayan elemanlarını hem de static elemanlarını kullanabildiğini anımsayınız. Bu durumda örneğin sınıfın static ve static olmayan fonksiyonları sınıfın static veri elemanlarını doğrudan kullanabilir. Genellikle veri elemanlarının dış dünyadan gizlenerek sınıfın private bölümüne yerleştirildiğini anımsayınız. Buna "veri elemanlarının gizlenmesi (data hiding)" deniyordu. İşte sınıfların static veri elemanları da yine aynı prenisp nedeniyle genellikle sınıflar private bölümüne yerleştirilmektedir. Tabii onları get ve set eden üye fonksiyonların static olması anlamlıdır. Örneğin: class Sample { public: //... static int get_s() { return m_s; } static void set_s(int s) { m_s = s; } private: //... static int m_s; }; int Sample::m_s; //... Sample::set_s(10); // setter cout << Sample::get_s() << endl; // getter --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: //... static int get_s() { return m_s; } static void set_s(int s) { m_s = s; } private: //... static int m_s; }; int Sample::m_s; int main() { Sample::set_s(10); // setter cout << Sample::get_s() << endl; // getter return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Programcılar tarafından static veri elemanlarının isimlendirilmesi genellikle static olmayan veri elemanlarında olduğu gibi yapılmaktadır. Ancak biz kursumuzda genellikle static veri elemanlarını m_ değil ms_ öneki ile isimlendireceğiz. Örneğin bir sınıftan toplamda kaç nesnenin yaratıldığnı tutmak isteyelim. Bunun için yapıcı fonksiyon içerisinde bir değişkeni artırmamız gerekir. Ancak bu değişken sınıfın static olmayan bir veri elemanı olamaz. İşte böylesi bir elemanı global yapmak yerine sınıfın static veri elemanı yapmak daha iyi bir tekniktir. Örneğin: class Sample { public: Sample() { ++ms_count; } static int count() { return ms_count; } private: static int ms_count; }; int Sample::ms_count; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class SaBmple { public: Sample() { ++ms_count; } static int count() { return ms_count; } private: static int ms_count; }; int Sample::ms_count; int main() { Sample s; Sample k; Sample *ps = new Sample(); cout << Sample::count() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 67. Ders 29/04/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de daha önce yazdığımız Date sınıfına bazı static veri elemanları ekleyelim. Bir tarihin hangi güne karşılık geldiğini anlamnın tipik yolu belli bir tarihten o tarihe kadar geçen gün sayısının hesaplanıp 7 ye bölümünden elde edilen kalana bakılmasıdır. Buradaki orijin olan tarihe "epoch" da denilmektedir. Tabii epoch olarak seçilen tarihin hangi gün olduğunu bilmemiz gerekir. (Aslında bunun önceden bilinmesine gerek de yoktur. Deneme yanılma yoluyla o gün bulunabilir.) Böyle bir fonksiyonu epoch olarak seçtiğimiz tarihten geçen gün sayısını hesaplayacak biçimde yazabiliriz. Tabii bu fonksiyonun static bir fonksiyon olması daha uygundur: class Date { //... static bool isleap(int year); static long totaldays(int day, int month, int year); //... }; bool Date::isleap(int year) { return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; } long Date::totaldays(int day, int month, int year) // 22/04/2024 ? { long tdays = 0; for (int i = 1900; i < year; ++i) tdays += isleap(i) ? 366 : 365; ms_montab[1] = isleap(year) ? 29 : 28; for (int i = 0; i < month - 1; ++i) tdays += ms_montab[i]; tdays += day; return tdays; } Yukarıdaki totaldays fonksiyonunda ayların kaç çektikleri bir dizide saklanmıştır. Bu dizinin yerel bir dizi olması her defasında dizinin gereksiz bir biçimde yaratılmasına yol açacaktır. Bu dizi static yerel bir dizi yapılırsa sınıfın diğer elemanları (eğer kullanacksa) onu kullanamaz hale gelir. O halde bu dizinin sınıfın static veri elemanı yapılması uygun olur. Benzer biçimde belli bir tarihin hangi gün olduğunu veren dayname isimli fonksiyon da static bir fonksiyon olarak yazılabilir. Bu fonksiyonun içerisindeki gün isimlerinin tutulduğu dizi de aynı gerekçeyle sınıfın static veri elemanı yapılabilir. Aşağıda Date sınıfının yeni gerçekleştirimi verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // date.hpp #ifndef DATE_HPP_ #define DATE_HPP_ namespace CSD { class Date { public: Date(); Date(int day, int month, int year); void disp() const; int day() const { return m_day; } void day(int day) { m_day = day; } int month() const { return m_month; } void month(int month) { m_month = month; } int year() const { return m_year; } void year(int year) { m_year = year; } static bool isleap(int year); static long totaldays(int day, int month, int year); static const char *dayname(int day, int month, int year); static Date today(); private: int m_day; int m_month; int m_year; static int ms_montab[12]; static const char *daynames[7]; }; } #endif // date.cpp #include #include #include "date.hpp" using namespace std; namespace CSD { int Date::ms_montab[] = {31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31}; const char *Date::daynames[7] = {"Pazartesi", "Sali", "Carsamba", "Persembe", "Cuma", "Cumartesi", "Pazar"}; Date::Date() { m_day = 1; m_month = 1; m_year = 1900; } Date::Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } void Date::disp() const { cout << m_day << '/' << m_month << '/' << m_year << " - " << dayname(m_day, m_month, m_year) << endl; } bool Date::isleap(int year) { return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; } long Date::totaldays(int day, int month, int year) // 22/04/2024 ? { long tdays = 0; for (int i = 1900; i < year; ++i) tdays += isleap(i) ? 366 : 365; ms_montab[1] = isleap(year) ? 29 : 28; for (int i = 0; i < month - 1; ++i) tdays += ms_montab[i]; tdays += day; return tdays; } const char *Date::dayname(int day, int month, int year) { long tdays; tdays = totaldays(day, month, year); return daynames[(tdays + 6) % 7]; } Date Date::today() { time_t t = time(NULL); tm *pt = localtime(&t); return Date(pt->tm_mday, pt->tm_mon + 1, pt->tm_year + 1900); } } // app.cpp #include #include "date.hpp" using namespace std; using namespace CSD; int main() { Date d = Date::today(); d.disp(); Date d2{23, 4, 1920}; d2.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfların static veri elemanlarıyla ilgili bazı ayrıntılar vardır. Sınıfın const static veri elemanları eğer tamsayı türlerine ve enum türlerine ilişkinse onlara sabit ifadeleriyle "=" sentaksıyla ya da küme parantezi sentaksıyla ilkdeğer verilebilir. Bu durumda bu veri elemanlarının eğer adresi alınmıyorsa dışarıda tanımlamasının yapılmasına gerek kalmaz. Örneğin: class Sample { public: const static int ms_x = 123; //... }; Burada ms_x static veri elemanı const int türdendir ve ona sabit ifadesiyle ilkdeğer verilmiştir. O halde eğer adresini almamışsak onun ayrıca dışarıda tanımlamasının yapılmasına gerek yoktur. Ancak örneğin: class Sample { public: const static int ms_x; //... }; Burada static veri elamanı yine const int türündendir. Fakat ona ilkdeğer verilmemiştir. Dolayısıyla dışarıda ilkdeğer verilerek tanımlama yapılması zorunludur. Örneğin: const int Sample::ms_x = 10; const static veri elemanlarına sınıf bildirimi içerisinde ilkdeğer verilebilmesi onun tamsayı türlerine ya da enum türlerine ilişkin olması gerektiğine dikkat ediniz. Örneğin: class Sample { public: const static double ms_x = 3.14; // geçersiz! static eleman double türden //... }; const static veri elemanları int türünden ya da enum türünden ise sınıf bildirimi içerisinde verilen ilkdeğerlerin de sabit ifadesi olması zorunludur. Örneğin: class Sample { public: const static int ms_x = foo(); // geçersiz! verilen ilkdeğer sabit ifadesi değil //... }; Sınıfın static veri elemanları inline olabilir. static inline veri elemanlarına sınıf bildirimi içerisinde "=" sentaksıyla ya da küme parantezi sentaksıyla ilkdeğer verilebilir. inline static veri elemanlarının dışarıda ayrıca tanımlaması yapılamaz. Başlık dosyasının içerisinde bildirilmiş olan sınıfların içerisindeki static inline veri elemanları bu başlık dosyaları birden fazla kaynak dosyadan include edildiğinde link aşamasında soruna yol açmamaktadır. Örneğin aşağıdaki sınıf bir başlık dosyasının içerisinde bulunuyor olsun: class Sample { public: inline static int ms_x = 123; void foo() { std::cout << ms_x << std::endl; } }; Biz bu bildirimin bulunduğu başlık dosyasını farklı kaynak dosyalardan include edersek bir sorun oluşmayacakltır. Eğer buradaki veri elemanı inline yapılmasaydı biz dışarıda ayrıca onun tanımlamasını bulundurmak zorunda kalırdık. inline static veri elmanları yalnızca başlık dosyalarından oluşan kütüphanelerdeki sınıfların static veri elemanı kullanmasına izin vermektedir. Sınıfın static veri elemanları consexpr de yapılabilmektedir. Bu durumda bu veri elemanlarına sınıf bildirimi içerisinde sabit ifadeleriyle ilkdeğer verilmesi zorunludur. Yine bunlara ilkdeğer "=" sentaksı ile ya da küme parantezi sentaksı ile verilebilmektedir. static constexpt veri elemanlarıaynı zamanda inline da kabul edilmektedir. Örneğin: class Sample { public: static constexpr int ms_x = 10; // geçerli }; Burada ms_x elemanı bir sabit ifadesi gibi kullanılabilmektedir. Tabii static constexpr veri elemanı bir sınıf türünden de olabilir. Bu durumda sınıfın yapıcı fonkssiyonun constexpr olması gerekir. static constexpr veri elemanlarına yine "=" sentaksı ile ve küme parantezi sentaksı ile ilkdeğer verilebilmektedir. Örneğin: class Test { public: constexpr Test(int a) : m_a(a) {} int m_a; }; class Sample { public: static constexpr Test ms_t{10}; // geçerli }; Burada artık Sample::ms_t.m_a ifadesi sabit ifadesi olarak kullanılabilir: int a[Sample::ms_t.m_a]; // geçerli Biz yukarıdaki örnekte sınıf bildirimi içerisindeki ms_t veri elemanına "=" sentaksıyla da ilkdeğer verebilirdik, ancak (...) sentaksıyla ilkdeğer veremeyiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Nesne yönelimli ptogramlama dillerindeki "sınıf" kavramı aslında doğal bir kavram değildir. Burada "doğal bir kavram değil" demekle sınıfların makine dilinde bir karşılıklarının olmadığını kastediyoruz. Fonksiyon kavramı makine dilinde de desteklenen doğal bir kavramdır. C'deki yapı (structure) kavramı makine dilinde de desteklenen doğal bir kavramdır. Ancak C++'taki sınıf kavramı doğal değil yapay bir kavramdır. Bir C++ programı derlendiğinde makine dili dili çıktısı incelenirse orada fonksiyonların olduğu ancak sınıf diye bir bilginin olmadığı görülecektir. Sınıf kavramı doğal olmadığı için "üye fonksiyon" kavramı da doğal değildir. Derleyici üye fonksiyonları C'deki gibi global fonksiyonlar olarak derlemektedir. Benzer biçimde "blok faaliyet alanı" doğal bir kavramdır, ancak "sınıf faaliyet alanı" doğal bir kavram değildir. Makine dilinde yalnızca bir grup fonksiyonun doğrudan erişebileceği bir faaliyet alanı yoktur. Özetle bir C++ programı derlendiğinde C programının derlenmesindeki gibi bir kod üretilmektedir. Özetle işlemciler C++'taki gibi değil C'deki gibi çalışmaktadır. Mikroişlemcilerin çalışmasına bakıldığında "üye fonksiyon" diye bir kavramın bulunmadığı yalnızca "global fonksiyonların" bulunduğu görülmektedir. O halde aslında üye fonksiyonlar derleyiciler tarafından global fonksiyonlar gibi ele alınıp kod üretilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- static olmayan üye fonksiyonlara C++ derleyicisi o üye fonksiyonun çağrılmasında kullanılan nesnesin adresini gizlice geçirmektedir. Üye fonksiyonun sınıfın veri elemanlarına erişmesi doğrudan değil bu gizli gösterici parametresi yoluyla yapılmaktadır. C++'ta üye fonksiyonlara geçirilen bu gizli parametreye "this gösterici" denilmektedir. Örneğin: class Sample { public: void foo(); private: int m_a; }; void Sample::foo() { m_a = 10; } Derleyici foo üye fonksiyonunu aşağıdaki gibi bir hale getirip derlemektedir: void Sample_foo(Sample *this) { this->m_a = 10; } Burada biz foo fonksiyonunu parametresiz tanımladık. Ancak derleyici ona gizli bir parametre yerleştirmektedir. Derleyici üye fonksiyonlar içeisinde sınıfın veri elemanlarına doğrudan değil bu gizlice geçirdiği gösterici yoluyla erişmektedir. Zaten "sınıf faaliyet alanı (class scope)" diye bir kavramın olmadığını böyle bir erişimin makine dilinde mümkün olmadığını yukarıda belirtmiştik. Şimdi biz foo fonksiyonunu şöyle çağırmış olalım: Sample s; s.foo(); Aslında derleyici bu çağrım için şöyle bir kod üretmektedir: foo(&s); Şimdi sınıfta aşağıdaki gibi bir foo fonksiyonu olsun: class Sample { public: void foo(int a); private: int m_a; }; void Sample::foo(int a) { m_a = a; } Burada foo fonksiyonu bir parametreye sahiptir. Ancak derleyici gizli bir parametre geçirdiği için gerçekte derlenmiş olan fonksiyonda iki parametre bulunacaktır. Derlenmiş olan fonksiyon aşağıdaki gibi olacaktır: void Sample_foo(Sample *this, int a) { this->m_a = a; } Biz burada fonksiyonun ismini sınıf ismi ile kombine ederek oluşturdukk. Bildiğiniz gibi aslında üye fonksiyon isimleri amaç dosyaya (object file) hem sınıf ismiyle hem de parametre türleriyle kombine edilerek yazılmaktadır. Buna derleyicilerin "isim dekorasyonu (name decoration ya da nema mangling)" dendiğini anımsayınız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 68. Ders 06/05/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- O halde biz bir sınıfın C eşdeğerini yazabiliriz. Zaten yukarıda da belirttiğimiz gibi gerçek durum C++'taki gibi değil C'deki gibidir. Başka bir deyişle C daha doğal bir programlama dilidir. Bilgisayar C++'taki gibi değil C'deki gibi çalışmaktadır. Aşağıdaki C++ koduna bakınız: class Date { public: Date(int day, int month, int year) { m_day = day; m_month = month; m_year = year; } int day() { return m_day;} int month() { return m_month;} int year() { return m_year;} void disp() const { printf("%02d/%02d/%04d\n", m_day, m_month, m_year); } private: int m_day; int m_month; int m_year; }; //... Date d(6, 5, 2024); d.disp(); Bu kodun eşdeğer C karşılığı şöyle oluşturulabilir: typedef struct { int m_day; int m_month; int m_year; } Date; void Date_Date(Date *this, int day, int month, int year) { this->m_day = day; this->m_month = month; this->m_year = year; } int Date_day(Date *this) { return this->m_day; } int Date_month(Date *this) { return this->m_month; } int Date_year(Date *this) { return this->m_year; } int Date_disp(const Date *this) { printf("%02d/%02d/%04d\n", this->m_day, this->m_month, this->m_year); } //... Date d; Date_date(&d, 6, 5, 2024); Date_disp(&d); Sınıf kavramı doğal bir kavram değildir. Ancak C'deki yapı kavramı doğal bir kavramdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın static üye fonksiyonlarına nesne adresi geçirilmemektedir. Bu nedenle static üye fonksiyonlar sınıfın static olmayan elemanlarını kullanamamktadır. Tabii biz static üye fonksiyonu ilgili sınıf türünden bir nesne, referans ya da gösterici yoluyla da çağırabiliyorduk. Ancak biz böyle çağırmış olsak bile yine static üye fonksiyonlara nesne adresi geçirilmemktedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- static olmayan üye fonksiyonların gizli this parametresi istenirse programcı tarafından açıkça üye fonksiyon içerisinde kullanılabilir. Bu this parametresinin bildirimi hiçbir zaman programcı tarafından yapılamaz. Ancak programcı sanki bu bildirim yapılmış gibi this anahtar sözcüğü ile gizlice geçirilen nesne adresini tutan bu gösterici parametresini kullanabilir. Örneğin: class Sample { public: void foo(int a); private: int m_a; }; void Sample::foo(int a) { m_a = a; } Burada programcı foo içerisinde nesne adresinin aktarıldığı this göstericisini this anahtar sözcüğü yoluyla açıkça kullanabilir. Örneğin. void Sample::foo(int a) { this->m_a = a; } Bit üye fonksiyon içerisinde sınıfın m_a isimli veri elemanına m_a ifadesi ile erişmekle this->m_a ifadesi ile erişmek arasında hiçbir performans farklılığı yoktur. Zaten bu elemana programcı m_a biçiminde erişiyor olsa bile aslında derleyici erişimi this->m_a gibi yapmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın foo isimli static olmayan üye fonksiyonun bar isimli static olmayan üye fonksiyonunu bar() biçiminde çağırdığını varsayalım. Burada bar fonksiyonuna this parametresi olarak ne geçirilecektir? Tabii ki foo fonksiyonuna geçirilmiş olan this parametresi. O halde bar çağrısının bar() biçiminde yapılması ile this->bar() biçiminde yapılması arasında da hiçbir farklılık yoktur. Başka bir deyişle static olmayan bir üye fonksiyon başka bir static olmayan üye fonksiyonu doğrudan çağırsa bile aslında derleyici onu kendisine geçirilmiş olan this göstericisi yoluyla çağırmaktadır. Örneğin: void Sample::foo() { bar(); } Bu tanımlamanın aşağıdaki tanımlamadna hiçbir farkı yoktur: void Sample::foo() { this->bar(); } Sınıf static üye fonksiyonlarında this göstericisi bulunmadığı için onların static olmayan üye fonksiyonları doğrudan çağırması mümkün olmamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- this göstericisi hangi sınıfın static olmayan üye fonksiyonunda kullanılıyorsa o sınıf türünden bir gösterici belirtmektedir. Örneğin Date sınıfının üye fonksiyonu içerisinde kullandığımız this Date türünden, Complex sınıfının üye üye fonksiyonu içerisinde kullandığımız this Complex sınıfı türünden gösterici belirtmektedir. Üye fonksiyonlarının const ve volatile olabildiğini görmüştük. const ve volatile üye fonksiyonların this göstericileri gösterdiği yer const ve volatile olan göstericiler olarak kuabul edilmektedir. Örneğin foo üye fonksiyonun const üye olduğunu ve sınıfın m_a isimli bir veri elemanının olduğunu varsayalım: void Sample::foo() const { m_a = 10; // geçersiz! } const üye fonksiyonların sınıfın veri elemanlarını değiştiremediğini anımsayınız. Yukarıdaki atama bu nedenler geçerli değildir. Bunun C karşılığını şöyle düşünebilirsiniz: void Sample_foo(const Sample *this) { this->m_a = 10; // geçersiz! gösterici const! } Şimdi aynı sınııfn bar üye fonksiyonunun const olmadığını düşünelim: void Sample::bar() { //.... } void Sample::foo() const { bar(); // geçersiz! const bir üye fonksiyon const olmayan bir üye fonksiyonu çağıramaz! } Bunun C karşılığını aşağıdaki gibi düşünebilirisniz: void Sample_bar(Sample *this) { //... } void Sample_foo(const Sample *this) { bar(this); // geçersiz! gösterdiği yer const olan bir gösterici gösterdiği yer const olmayan bir göstericiye atanamaz! } Sınıfın static üye fonksiyonlarının const ve volatile yapılamadığını anımsayınız. Bunun nedeni static üye fonksiyonların zaten this parametrelerinin olmamasıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Üye fonksiyonlarda overload resolution işlemi gizlice geçirilen nesne adresi dikkate alınarak yapılmaktadır. Örneğin: class Sample { public: void foo(); void foo() const; //... }; Sample s; const Sample k; s.foo(); k.foo(); Burada s.foo() çağrısı şöyle düşünülmelidir: foo(&s); foo isimli iki üye fonksiyonun birinci parametreleri ise şöyledir: void Sample_foo(Sample *this) { //... } void Sample_foo(const Sample *this) { //... } Görüldüğü gibi iki üye fonksiyon da aday ve uygundur. Ancak Sample * -> Sample dönüştürmesi Sample * -> const Sample * dönüştürmesinden daha iyidir. O zaman en uygun fonksiyon const olmayan üye fonksiyondur. Örneğimizdeki k.bar() çağrısı da şöyle düşünülmelidir: bar(&k); Burada artık const olmayan foo fonksiyonu aday fonksiyondur ancak uygun değildir. Bu durumda const olan üye fonksiyon çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yapıcı fonksiyonların MIL sentaksında this göstericisi kullanılabilir. Çünkü MIL sentaksında zaten sınıfın veri elemanları kullanılabilmektedir. Örneğin: class Sample { Sample(int a, int b); //... }; Sample::Sample(int a, int b) : this->m_a(a), this->m_b(b) // geçerli! {} --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bizim üye fonksiyonlar içerisinde this göstericisini açıkça kullanmamızın gerektiği durumlar var mıdır? Evet bazı durumlarda biz this göstericisini açıkça kullanmak zorunda kalabiliriz. Örneğin: class Sample { public: void set(int a); private: int a; }; void Sample::set(int a) { a = a; // parametre değişkeni kendisine atanıyor! } Burada a ifadesi "niteliksiz isim arama kurallarına göre" aranacağına göre her iki a da parametre değişkeni olan a'dır. Fakat örneğin: void Sample::set(int a) { this->a = a; // parametre değişkeni olan a, veri elemanı olan a'ya atanıyor } Burada artık doğrudna kullanılan a ismi niteliksiz (unqualified), this-> biçimind ekullanılan a ismi ise nitelikli (qualified) arama kurallarına göre aranacaktır. Dolayısıyla biz burada artık parametre değişkeni olan a'yı sınıfın veri elemanı olan a'ya atamış olmaktayız. O halde static olmayan bir üye fonksiyonda yerel ya da parametre değişkeni ile aynı isimli sınıfın bir veri elemanı varsa biz sınıfın veri elemanına this kullanarak erişebiliriz. Gerçi programcıların önemli bir bölümü veri elemanlarını bazı önek ya da soneklerle isimlendirdiği için böylesi çakışmalara pek maruz kalmamaktadır. Aşağıdaki atama operatör fonksiyonuna dikkat ediniz: Sample &Sample::operator =(const Sample &s) { if (this == &s) return *this; //... } Burada parametre olarak geçirilmiş olan nesnesin adresi ile this adresinin aynı olup olmadığına bakılmıştır. Her ne kadar biz henüz operatör fonksiyonlarını görmemiş olsak da aslında a = b işlemi a.operator =(b) ile aynı anlamdadır. Şimdi programcının aşağıdaki gibi bir tama yapmış olduğunu varsayalım: Sample a; a = a; // a.operator =(a); Burada artık üye fonksiyon içerisinde this == &s koşulu sağlanmaktadır. Yani biz üye fonksiyon içerisinde nesnenin kendisine atanmış olduğunu this yoluyla tespit edebilmiş olmaktayız. Bazı üye fonksiyonlarda sıkça karşılaşılan bir kalıp da return *this kalıbıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 69. Ders 08/05/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir üye fonksiyonun *this ise geri dönmesinin ne anlamı olabilir? Anımsanacağı gibi bir fonksiyonun geri dönüş değeri bir referans ise fonksiyon çağırma ifadesi aslında return deyiminde belirtilen nesneyi temsil etmekteydi. İşte üye fonksiyonun geri dönüş değer değeri kendi sınıfı türünden bir referans olursa ve return ifadesi de *this olursa bu durumda bu üye fonksiyon çağrısındne elde edilen geri dönüş değeri üye fonksiyonun çağrıldığı nesmeyi belirtir. Örneğin: class Sample { public: Sample(int a) : m_a(a) {} Sample &foo(); void disp() const; int m_a; }; //... Sample s{10}; Burada s.foo() çağrısı sonucunda s nesnesinin kendisi elde edilecektir. Örneğin: s.foo().m_a = 20; Biz burada s nesnesinin m_a veri elemanına 20 atamış olduk. Pekiyi bir üye fonksiyonun kendisinin çağrıldığı nesneyle geri dönmesinin ne faydası olabilir? İşte bu tür ifadeler bir dizi çağrıyı zincirlemesine birbirine bağlamak için kullanılabilmektedir. Örneğin: class Out { public: Out &disp(int a); Out &disp(double a); Out &disp(const char *str); //... }; Out &Out::disp(int a) { cout << a; return *this; } Out &Out::disp(double a) { cout << a; return *this; } Out &Out::disp(const char *str) { cout << str; return *this; } //... int a = 10, b = 20; double pi = 3.14; out.disp(a).disp(", ").disp(b).disp(", ").disp(pi).disp("\n"); Burada out.disp(a) çağrısından out nesnesinin aynısı elde edildiği için o da yine disp(", ") çağrısında kullanılabilmektedir. Yani aslında yukarıdaki çağrının eşdeğeri şöyledir: out.disp(a); out.disp(", "); out.disp(b); out.disp(", "); out.disp(pi); disp("\n"); stdout dosyasına birşeyler yazdırmak için kullandığımız cout değişkeni de yukarıdaki out gibi aslında bir sınıf türünden nesnedir. Örneğin: cout << a << ", "; Operatör fonksiyonları sonraki derste ele alınacaktır. Ancak burada cout << a işleminden cout nesnesinin kendisi elde edilmektedir. Bu nesne de cout << ", " çağrısında kullanılmıştır. Pekiyi üye fonksiyon kendi sınıfı türündne bir referans yerine kendi sınıfı türündne bir nesneye geri dönseydi ne olurdu? Örneğin: class Sample { public: Sample foo(); //... }; Burada foo fonksiyonunun şöyle yazılmış olduğunu varsayalım: Sample Sample::foo() { //.... return *this; } Burada foo kendisini çağrıldığı nesneyle değil onun bir kopyası ile geri dönmektedir. Aşağğıdaki örnekle bu durum daha iyi anlaşılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a) : m_a(a) {} Sample foo(); Sample &bar(); void set(int a); void disp() const; //... private: int m_a; }; Sample Sample::foo() { return *this; } Sample &Sample::bar() { return *this; } void Sample::set(int a) { m_a = a; } void Sample::disp() const { cout << m_a << endl; } int main() { Sample s{10}; s.foo().set(20); s.disp(); // 10 s.bar().set(20); s.disp(); // 20 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türemiş sınıf nesnesi yoluyla taban sınıfın üye fonksiyonlarını da çağırabiliyordu. Taban sınıfın üye fonksiyonlarındaki this göstericisinin taban sınıf türünden, türemiş sınıfın üye fonksiyonlarındaki this göstericisinin ise türemiş sınıf türünden olduğuna dikkat ediniz. Ancak C++'ta zaten türemiş sınıf türünden adresler otomatik olarak taban sınıf türünden adreslere dönüşürülebiliyordu. Örneğin: Örneğin class A { public: void foo(); // void foo(A *this) /... }; class B : public A { public: void bar(); // void bar(B *this) //... }; B b; b.foo(); // A_foo(&b) b.bar(); // B_foo(&b) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- NYPT'nin önemli anahtar kavramlarından biri de "çokbiçimlilik (polymorphism)" denilen kavramdır. Bir dile nesne yönelimli denilebilmesi için onda bu çokbiçimlilik özelliğinin buluyor olması gerekmektedir NYPT'de çokbiçimlilik biyolojiden aktarılmış bir terimdir. Biyolojide çokbiçimlilik "bir canlının çeşitli doku ve organlarının temel işlevleri aynı kalmak üzere onların yaşam koşullarına göre farklılaşması" anlamına gelmektedir. Örneğin "kulak" pek çok canlıda vardır. Temel işlevi duymaktır. Ancak her canlının kulağı zamnla az çok diğer canlıların ulağından farklılaşmıştır. Benzer biçimde pek çok canlının akciğeri vardır. Ancak bu akciğerin temel işlevleri aynı olsa da alt türler arasında farklılaştığı görülmektedir. Pek çok teorisyene göre bir dilin nesne yönelimli olması için "sınıf (class)", "türetme (inheritance" dışında çokbiçimliliğe (polymorphism) de sahip olması gerekmektedir. Eğer bir dilde sınıf varsa, türetme varsa ancak çokbiçimlilik yoksa bu dillere "nesne yönelimli (object oriented)" değil "nesne tabanlı (object based)" diller denilmektedir. Çokbiçimlilik NYPT'de değişik bakış açılarına göre değişik biçimlerde tanımlanabilir. Örneğin biz çokbiçimliliği üç açıdan tanımlayabiliriz: 1) Yazılım Mühendisliği Tanımı: Çokbiçimlilik türden bağımsız kod paraçalarının oluşturulması için kullanılan bir tekniktir. 2) Biyolojik Tabım. Çokbiçimlilik taban sınıfın belli bir üye fonksiyonunun türemiş sınıflar tarafından onlara özgü bir biçimde gerçekleştirilmesidir. 3) Aşağı Seviyeli Tanım: Çokbiçimlilik önceden yazılmış kodların sonradan yazılmış kodları çağırabilmesi özelliğidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta çokbiçimlilik sanal fonksiyonlarla (virtual functions) gerçekleştirilmektedir. Bir üye fonksiyonu sanal fonksiyon yapabilmek için sınıf bildiriminde prototipinin önüne "virtual" anahtar sözcüğü getirilir. virtual anahtar sözcüğü fonksiyonun sınıf dışındaki tanımlamasında kullanılmaz. Örneğin: class A { public: void foo(); // normal üye fonksiyon virtual void bar(); // sanal üye fonksiyon //... }; void A::foo() { //... } void A::bar() // tanımlama sıraında virtual anahtar sözcüğü kullanılamaz { //.... } Tabii sanal fonksiyonlar da sınıf içerisinde inline olarka tanımlanabilir. Örneğin class A { public: void foo(); // normal üye fonksiyon virtual void bar() // sanal fonkdiyon { //... } //... }; Global fonksiyonlar sanal fonksiyon yapılamaz. Sınıfın static üye fonksiyonları da sanal fonksiyon yapılamaz. Yalnızca sınıfın static olmayan üye fonksiyonları sanal fonksiyon yapılabilmektedir. (Bu nedenle biz "sanal üye fonksiyon" demeyecğiz. Sanal fonksiyon zaten üye fonksiyon olmak zorundadır.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Taban sınıftaki bir sanal fonksiyon türemiş sınıfta "aynı isimle, aynı parametrik yapıyla ve aynı geri dönüş değeri türü ile" bildirilirse bu duruma "taban sınıftaki sanal fonksiyonun türemiş sınıfta "override" edilmesi denilmektedir. Örneğin: class A { public: void foo(); virtual void bar(); //... }; class B : public A { public: void bar(); // override edilmiş //... }; Burada A taban sınıfındaki bar fonksiyonu B türemiş sınıfında "override" edilmiştir. (Buradaki "override" sözcüğü "üzerine bindirme" gibi bir anlama gelmektedir.) Taban sınıfta sanal olmayan bir fonksiyon türemiş sınıfta aynı biçimde yeniden tanımlanırsa bu bir "override" işlemi değildir. Override terimi sanal fonksiyonlar için kullanılmaktadır. Örneğin: class A { public: void foo(); void bar(); //... }; class B : public A { public: void bar(); // dikkat override işlemi değil! //... }; Burada B sınıfındaki bar bildirimi bir "override" işlemi değildir. Zaten daha önce de taban sınıf ile türemiş sınıfta aynı isimli ve aynı parametrik yapıya sahip üye fonksiyonların bulunabileceğini görmüştük. Taban sınıftaki sanal fonksiyon türemiş sınıfta override edildiğinde türemiş sınıftaki override edilmiş fonksiyon da sanal kabul edilmektedir. Türemiş sınıftaki bildirim virtual kullanılmamış olsa bile kullanılmış gibi kabul edilmektedir. Örneğin class A { public: void foo(); virtual void bar(); //... }; class B : public A { public: void bar(); // virtual void bar //... }; Burada B sınıfındaki bar fonksiyonu da virtual bir fonksiyondur. Yani biz bu fonksiyonun başına virtual getirmesek de (getirebiliriz) zaten getirilmiş gibi işlem görmektedir. Fonksiyonun türemiş sınıfta override edilmesi için şu özelliklerin sağlanması gerekir: - Fonksiyonların ismi ve parametre türleri aynı olmalıdır. - Fonksiyonların geri dönüş değerinin türleri aynı olmalıdır. - Fonksiyonun const ve volatile durumları da aynı olmalıdır. Örneğin: class A { public: virtual void foo(int a); //... }; class B : public A { public: void foo(long a); // dikkat bu bir override işlemi değildir //... }; Burada biz B sınıfında A sınıfındaki sanal fonksiyonu override etmiş olmamaktayız. Çünkü fonksiyonların parametrik yapıları farklıdır. Örneğin: class A { public: virtual void foo(int a); //... }; class B : public A { public: int foo(int a); // dikkat bu bir override işlemi değildir, geçersiz! //... }; Burada da override işlemi yapılamamıştır. Taban sınıftaki sanal fonksiyonu türemiş sınıfta override edebilmek için geri dönüş değerlerinin türlerinin de aynı olması gerekmektedir. Ayrıca C++'ta taban sınıftaki sanal fonksiyonun türemiş sınıfta aynı isim ve parametrik yapıya sahip olarak ancak farklı geri dönüş değeri türüyle bildirilmesi geçerli değildir. Örneğin: class A { public: virtual void foo() const; virtual void bar(int a); virtual void tar(int a); //... }; class B : public A { public: void foo(); // geçerli ancak bu bir override işlemi değil! çünkü const'luk belirtilmemiş void bar(); // geçerli ancak bu bir override işlemi değil! parametrik yapılar farklı int tar(int a); // geçersiz! aynı parametrik yapıyla farklı geri dönüş değeri türüyle override işlemi yapılamaz //... }; Override işlemi bir dizi türetmede devam ettirilebilir. Örneğin: class A { public: virtual void foo(); //... }; class B : public A { public: void foo(); // virtual yazılmış gibi etki gösterir //... }; class C : public B { public: void foo(); // override işlemi yapılmış, virtual yazılmış gibi işlem görür }; Burada C sınıfındaki foo da override edilmiştir. Bir fonksiyonun override edilmiş olması taban sınıfta sanal ise onun türemiş sınıfta yeniden yazılması anlamına gelmektedir. C sınıfının taban sınıfı B'dir. B sınıfında da foo her nekadar virtual anahtar sözcüğü belirtilmemiş olsa da sanaldır. Tabii sanallık daha sonra da başlatılabilir. Örneğin: class A { public: void foo(); // sanal değil //... }; class B : public A { public: virtual void foo(); //... }; class C : public B { public: void foo(); // override işlemi yapılmış, virtual yazılmış gibi işlem görür }; Override işlemi için override edilecek fonksiyonunun sınıfın doğrudan taban sınıfında bulunuyor olması gerekmemektedir. Örneğin: class A { public: virtual void foo(); //... }; class B: public A { //... }; class C : public B { public: void foo(); // geçerli, A'daki sanal fonksiyon override edilmiş //... }; Ayrıca override edilecek fonksiyonun görünür (visible)" olması da gerekmemektedir. Örneğin: class A { public: virtual void foo(); //... }; class B: public A { public: void foo(int a); // geçerli ama override edilmiş değil //... }; class C : public B { public: void foo(); // geçerli, A'daki sanal fonksiyon override edilmiş //... }; Burada C sınıfında biz A sınıfınındaki foo fonksiyonunu niteliksiz isim arama sırasında görmeyiz. Ancak onu override edebiliriz. C++'ta sanal fonksiyon sınıfın farklı bir bölümünde override edilebilmektedir. (Halbuki örneğin Java ve C#'ta bu durum geçerli değildir.) Örneğin taban sınıfın public bölümündeki sanal fonksiyon türeniş sınıfın private bölümünde override edilebilir: class A { public: virtual void foo(); //... }; class B: public A { private: void foo(); // geçerli, override edilmiş //... }; Tabii yukarıdaki durumun tersi de söz konusu olabilirdi. Yani örneğin taban sınıfın private bölümndeki sanal fonksiyon türemiş sınıfın public bölümünde de override edilebilirdi: class A { private: virtual void foo(); //... }; class B: public A { public: void foo(); // geçerli, override edilmiş //... }; Sanal fonksiyonun override edilebilmesi için türetme biçiminin public olması gerekmemektedir. Örneğin: class A { public: virtual void foo(); //... }; class B: A { // private türetesi uygulanmış public: void foo(); // geçerli, override edilmiş //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sanal fonksiyonun belli bir sınıf için "son override edilmiş biçimi" denilen bir kavram vardır. Buna C++ standartlarında "final overrider" denilmektedir. Bir sınıf için bir fonksiyonun son override edilmiş biçimi aşağıdan yukarıya doğru onun ilk override edilmiş biçimidir. class A { public: virtual void foo(); //... }; class B: public A { public: void foo(); // override edilmiş //... }; class C : public B { public: void foo(); // final overrider //... }; Burada C sınıfı için A sınıfının foo sanal fonksiyonunun son override edilmiş biçimi C'deki foo fonksiyonudur. Örneğin: class A { public: virtual void foo(); //... }; class B: public A { public: void foo(); // final overrider //... }; class C : public B { public: //... }; Burada C sınıfı için foo sanal fonksiyonunun yukarıya doğru ilk override edilmi biçimi B'deki foo fonksiyonudur. O halde C sınıfı için foo fonksiyonunun son override edilmiş biçimi B'deki foo fonksiyonudur. Benzer biçimde burada B sınıfı için foo sanal fonksiyonunun son override edilmiş biçimi B'de foo fonksiyonudur. Örneğin. class A { public: virtual void foo(); //... }; class B: public A { public: //... }; class C : public B { public: //... }; Burada C sınıfı için foo sanal fonksiyonunun son override edilmiş biçimi A'deki foo fonksiyonudur. B sınıfı için de foo sanal fonksiyonunun son override edilmiş biçimi A'daki foo fonksiyonudur. Buradan da görüldüğü gibi taban sınıftaki sanal fonksiyonun her türemiş sınıf için bir son override edilmiş biçimi vardır. Görüldüğü gibi "bir sınıf için bir sanal fonksiyonun son override edilmiş biçimi (final overrider)" denilen bir kavram vardır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 70. Ders 15/05/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Taban sınıftaki birs sanal fonksiyon bazen programcılar tarafından "override" edildi sanılmakta ancak programcı bu işlem sırasında parametre türlerini yanlış yazdığı için aslında override işlemi yapılamamaktadır. Bu da böceklere yol açabilmektedir. İşte bu tür hataları engellemek için C++11 ile birlikte C++'a override anahtar sözcüğü de eklenmiştir. Fonksiyonun parametre parantezinden sonra "override" anahtar sözcüğü yazılırsa bu durumda eğer taban sınıflarda override edilecek bir sanal fonksiyon yoksa derleme aşamasında error oluşmaktadır. Örneğin: class A { public: virtual void foo(long a); //... }; class B : public A { public: void foo(int a) override; /* derleme sırasına error oluşacak */ //... }; O halde artık C++11'den sonra override işlemş yapılırken parametre parantezinden sonra override anahtar sözcüğünün yazılması iyi bir tekniktir. Tabii bu durum geçmişe uyumu bozacaktır. Aslında bu özellik C# ve sonra da Java gibi dillerde eskiden beri bulunmaktaydı. C++ da bunu C++11 ile birlikte bünyesine kattı. override anahtar sözcüğü tıpkı virtual anahtar sözcüğü gibi yalnızca prototipte kullanılabilir. Tabii fonksiyon sınıf içerisinde inline olarak da tanımlanabilir. Eğer const ve override anahtar sözcükleri birlikte kullanılıyorsa önce const sonra override anahtar sözcüğünün yazılması gerekmektedir. Örneğin: class A { public: virtual void foo(int a) const; //... }; class B : public A { public: void foo(int a) const override; // override const geçerli değil! //... }; Yine C++11 ile birlikte zaten Java ve C# gibi dillerde olan "final override" kavramı da C++'a eklenmiştir. Eğer fonksiyonun parametre parantezinden sonra "final" anahtar sözcüğü getirilirse bu durumda o sanal fonksiyon artık daha fazla override edilemez. Edilmeye çalışılırsa derleme zamanı sırasında error oluşur. Örneğin: class A { public: virtual void foo(int a) const; //... }; class B : public A { public: void foo(int a) const final; //... }; class C : public B { public: void foo(int a) const; // comppile time error! B'deki foo daha fazla override edilemez! //... }; Tabii override ile final birlikte de kullanılabilir. Bu durumda bunların sırasının bir önemi yoktur. Örneğin: class A { public: virtual void foo(int a) const; //... }; class B : public A { public: void foo(int a) const override final; // override yazılmasaydı da override işlemi yapılmaktadır //... }; Özetle parametre parantezinden sonra ve varsa const anahtar sözcüğünden sonra şu belirleyiciler kullanılabilir: override final override final final override Bu anahtar sözcüklerin hiçbiri tanımlama sırasında kullanılamaz. final anahtar sözcüğü yalnızca sanal fonksiyonlarda kullanılabilir. Sanal olmayan üye fonksiyonların final yapılması anlamsızdır ve yasaklanmıştır. Örneğin: class A { public: virtual void foo(int a) const; //... }; class B : public A { public: void bar(int a) final; // error! bar sanal değil! //... }; Burada B sınıfındaki bar üye fonksiyonu sanal olmadığı için final yapılamaz. override edilmiş fonksiyonların zaten sanal olduğunu anımsayınız. Sanallığı başlatılmış olan bir fonksiyonun aynı zamanda final yapılması yasak olmasa da anlamsızdır. Örneğin: class A { public: virtual void foo(int a) const final; // yasak değil ancak anlamsız! //... }; C# ve Java'da final ancak override fonksiyonlarda kullanılabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yine C++11 ile birlikte Java ve C# gibi dillerde başından beri var olan "final sınıf" kavramı da C++'a sokulmuştur. Programcı bir sınıfı final yaparsa artık o sınıftan sınıf türetmeyi yasaklamış olur. Sınıflar için final anahtar sözcüğü sınıf isminden sonra küme parantezinden önce getirilmektedir. Örneğin: class A final { //... }; class B : public A { // geçersiz! final sınıftan sınıf türetilemez! //... }; Tabii final sınıf türemiş bir sınıf da olabilir. Örneğin: class A { //... }; class B final : public A { //... }; class C : public B { // geçersiz! final sınıftan türetme yapılamaz! //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf türünden gösterici ya da referansın "statik" ve "dinamik" türü vardır. Sınıf türünden olmayan değişkenlerin yalnızca statik türleri vardır. Biz bir değişkenin türü dediğimizde default olarak zaten onun statik türünü kastederiz. Bir sınıf türünden gösterici ya da referansın statik türü bildirimde belirtilen türüdür. Ancak sınıf türünden bir gösterici ya da referans eğer türemiş sınıf türünden bir nesnenin taban kısmını gösteriyorsa gösterdiği nesnenin bütürünün türüne o gösterici ya da referansın dinamik türü denilmektedir. Örneğin aşağıdaki gibi bir türetme şeması söz konusu olsun: A B C C sınıfı türünden bir nesnenin adresini A sınıfı türünen bir göstericiye atayalım: C c; A *pa; pa = &c; Burada pa göstericisinin statik türü A'dır. Çünkü bildirimde kullanılan sınıf A'dır. Ancak pa göstericisinin dinamik türü C'dir. Çünkü pa göstericisi bağımsız bir A nesnesini değil en geniş hali C olan bir nesnenin A kısmını göstermektedir. Örneğin: A *pa; B *pb; C c; pb = &c; pa = pb; Burada pb göstericisinin statik türü B'dir. Ancak dinamik türü C'dir. pa göstericisinin statik türü A'dır. Ancak dinamik türü yine C'dir. Aynı şeyleri referans yoluyla da yapabiliriz. Örneğin: C c; B &b = c; A &a = b; Burada yine b referansının statik türü B, dinamik türü C'dir. A referansının statik türü A, dinamik türü C'dir. Tabii bir sınıf türünden gösterici ya da referansın statik ve dinamik türleri aynı olabilir. Örnein: A a; A *pa = &a; Burada pa göstericisinin staik türü de dinamik türü de A'dır. Yalnızca sınıf türünden gösterici ve referansların dinamik türleri vardır. Sınıf türlerinden gösterici ya da referans olmayan neslerin dinamik türleri yoktur. Örneğin: int *pi; long a; pi = reinterpret_cast(&a); Burada pi göstericisi bir sınıf türünden olmadığı için dinamik türü yoktur. pi int türdendir. Başka bir deyişle pi'nin yalnızca statik türü vardır. Sınıf türünden gösteri ve referansların statik türleri değişmez. Ancak dinamik türleri değişebilir. Örneğin: A B C C c; B b; A a; A *pa; pa = &c; // pa'nın statik türü A, dinamik türü C //... pa = &b; // pa'nın statik türü A, dinamik türü B //... pA = &a; // pa'nın statik türü A, dinamik türü A Örneğin: void foo(A &r) { //... } //... C c; B b; A a; foo(c); // r referansının dinamik türü C foo(b); // r referansının dinamik türü B foo(a); // r referansının dinamik türü A --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çokbiçimli (polymophic) mekanizma şöyledir: Bir sınıf türünden gösterici ya da referans ile bir üye fonksiyon çağrılmış olsun. Bu üye fonksiyon nitelikli isim araması kuralına göre gösterici ya da referansın statik türüne ilişkin sınıfta aranır (tabii o sınıfta bulunamazsa taban sınıflara da bakılır). Sonra bulunan fonksiyonun sanal olup olmadığına bakılır. Eğer bulunan fonksiyon sanal değilse bulunan fonksiyon çağrılır. Eğer bulunan fonksiyon sanal ise o fonksiyonun çağrılmasında kullanılan gösterici ya da referansın dinamik türüne ilişkin sınıfın override edilmiş sanal fonksiyon çağrılır. Eğer gösterici ya da referansın dinamik türüne ilişkin sınıfta ilgili sanal fonksiyon override edilmemişse yukarıya doğru o sanal fonksiyonun override edilmiş olduğu ilk taban sınıfın sanal fonksiyonu çağrılır. Örneğin: class A { public: virtual void foo(); //... }; class B : public A { public: void foo() override; //... }; class C : public B { public: void foo() override; //... }; //... C c; A *pa; pa = &c; pa->foo(); // C:::foo çağrılır Burada pa göstericisinin statik türü A, dinamik türü C'dir. foo fonksiyonu pa'nın statik türüne ilişkin sınıfta aranacaktır ve bulunacaktır. Bulunan fonksiyon sanal bir fonksiyondur. Bu durumda bulunan fonksiyon değil pa göstericisinin dinamik türüne ilişkin sınıfın override edilmiş foo fonksiyonu çağrılacaktır. Eğer foo A sınıfında sanal olmasaydı A sınıfının foo fonksiyonu çağrılırdı. Özetle biz bir gösterici ya da referansla bir sanal fonksiyonu çağırdığımızda aslında gösterici ya da referansın dinamik türüne ilişkin override edilmiş olan fonksiyon çağrılmaktadır. Aşağıda bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: virtual void foo(); //... }; class B : public A { public: void foo() override; //... }; class C : public B { public: void foo() override; //... }; void A::foo() { cout << "A::foo" << endl; } void B::foo() { cout << "B::foo" << endl; } void C::foo() { cout << "C::foo" << endl; } int main() { C c; B b; A a; A *pa; pa = &c; pa->foo(); // C::foo çağrılır pa = &b; pa->foo(); // B::foo çağrılır pa = &a; pa->foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir gösterici ya da referansla bir sanal fonksiyon çağrıldığında eğer gösterici ya da referansın dinamik türüne ilişkin sınıfta o sanal fonksiyon override edilmemişse yukarıya doğru sanal fonksiyonun override edildiği ilk taban sınıfın sanal fonksiyonu çağrılmaktadır. Aşağıdaki örnekte şöyle bir türetme şeması uygulanmıştır: A virtual void foo(); B void foo() override; C A'daki foo sanal fonksiyonu B'de override edilmiştir ancak C'de override edilmemiştir. Bu durumda A sınıfı türünden bir gösterici ya da referansın dinamik türü C olsa bile çağrılan foo fonksiyonu B sınıfının foo fonksiyonu olacaktır. Başka bir deyişle eğer fonksiyon sanal ise o fonksiyonun son ovveride edilmiş biçimi (final overrider) çağrılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: virtual void foo(); //... }; class B : public A { public: void foo() override; //... }; class C : public B { public: //... }; void A::foo() { cout << "A::foo" << endl; } void B::foo() { cout << "B::foo" << endl; } void test(A &r) { r.foo(); } int main() { C c; test(c); // B sınıfın foo fonksiyonu çağrılacaktır return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyonun sanallığı en tepedeki taban sınıfta başlatılmak zorunda değildir. Örneğin: A void bar(); B virtual void bar(); C void bar() override; Burada A sınııfndaki bar fonksiyonu sanal değildir. Ancak B sınıfındaki bar sanaldır ve C'de override edilmiştir. Aşağıdaki koda dikkat ediniz: C c; A *pa; pa = &c; pa->bar(); // A::bar çağrılır Burada bar fonksiyonu A sınıfında aranacak ve bulunacaktır. Ancak bulunan fonksiyon sanal değildir. Dolayısıyla çokbiçimli mekanizma devreye girmeyecek ve A sınıfının bar fonksiyonu çağrılacaktır. Şimdi de aşağıdaki koda dikkat ediniz: C c; B *pb; pb = &c; pb->bar(); // C::bar çağrılır Burada bar fonksiynu B sınıfında aranır. B sınıfında bulunan bar sanal olduğu için çokbiçimli mekanizma devreye girer ve C sınıfındaki bar çağrılır. Aşağıda buna ilişkin bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: virtual void foo(); void bar(); //... }; class B : public A { public: void foo() override; virtual void bar(); //... }; class C : public B { public: void bar() override; //... }; void A::foo() { cout << "A::foo" << endl; } void B::foo() { cout << "B::foo" << endl; } void A::bar() { cout << "A::bar" << endl; } void B::bar() { cout << "B::bar" << endl; } void C::bar() { cout << "C::bar" << endl; } int main() { A *pa; B *pb; C c; pa = &c; pa->foo(); // B:::foo çağrılır pa->bar(); // A::bar çağrılır pb = &c; pb->foo(); // B:::foo çağrılır pb->bar(); // C::bar çağrılır return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte test fonksiyonun parametresi taban sınıf türünden bir göstericidir. Bu test fonksiyonu türemiş sınıf türünden nesnelerin adresleriyle çağrılmıştır. Çokbiçimli mekanizma gereği dinamik türe ilişkin sınıfların override edilmiş üye fonksiyonlarının çağrıldığına dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: virtual void foo(); //... }; class B : public A { public: void foo(); // override edilmiş //... }; class C : public B { public: void foo(); }; void A::foo() { cout << "A::foo" << endl; } void B::foo() { cout << "B::foo" << endl; } void C::foo() { cout << "C::foo" << endl; } void test(A *pA) { pA->foo(); } int main() { A a; B b; C c; test(&a); // A::foo çağrılır test(&b); // B::foo çağrılır test(&c); // C::foo çağrılır return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 71. Ders 20/05/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir eylem alt türlerde farklı asıl işlevi aynı olmak üzere farklı biçimlerde yapılıyorsa o eyleme "çokbiçimli eylem" diyebiliriz. Örneğin bir topun vurulduğunda gitmesi çokbiçimli bir eylemdir. Her top gider ancak kendine göre değişik bir biçimde gider. Örneğin patlak top, hafif top ağır top vurulduğunda aynı biçimde gitmemektedir. Örneğin Tetris oyununda bir şeklin düşmesi çokbiçimli bir eylemdir. Her şekil düşer ancak kendine göre düşer. Örneğin karesel şeklin düşmesi sırasında yapılan çizimlerle çubuksal şeklin düşmesi sırasında yapılan çizimler farklıdır. Örneğin maaş hesaplaması da çokbiçimli bir eylemdir. Her çalışanın maaşı vardır ancak bunlar değişik biçimde hesaplanmaktadır. Örneğin satranç taşlarının hamle yapılırken girmesi de çokbiçimli bir eylemdir. Her taş gider ancak kendine göre farklı bir biçimde gider. Örneğin bir veri yapısına eleman insert edilmesi de çokbiçimli bir eylemdir. Her veri yapısına eleman insert edilebilir ancak bu insert işlemi her veri yapısında farklı biçimde yapılmaktadır. Bağlı listeye eleman insert ederken yapılan işlemlerle dinamik diziye eleman insert edilirken yapılan işlemler farklıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Nesne yönelimli teknikte ideal durumda kod üzerinde değişiklikler yapılmaz. Geliştirme faaliyeti her zaman ekleme yöntemi yapılarak gerçekleştirilir. Böylece daha önceki kod sağlam çalışıyorsa ve ekleme işleminden sonra problem oluşmuşsa problem eklenen kısımla ilgilidir. Bu durumda eklenen kısmın test edilmesi yeterlidir. Halbuki biz kodda bir değişiklik yaparsaktüm kodun yeniden test edilmesi gerekir. Çünkü yaptığımız değişiklik başka yerleri bozuyor olabilir. Bir yeri düzeltirken başka yeri bozulması durumlarıyla hem yazılımda hem diğer alanlarda sıkça karşılaşılmaktadır. NYPT'de mevcut kod iyi bir biçimde test edilir. Yeni eklemeler yapıldığında kodun tamamı değil yalnızca eklenen kısmın testi yapılır. Halbuki klasik şelale modelinde (waterfall model) kodda bir değişiklik yapıldığında tüm testler yinelenmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İşte çokbiçimlilik aslında ekleme yoluyla geliştirme süreci için kullanılan önemli bir tekniktir. Çokbiçimlilik sayesinde kod parçaları taban sınıfa dayalı bir biçimde genel yazılır. Böylece bir ekleme yapıldığında genel yazılmış kodlar üzerinde test işlemleri yapılmaz. Örneğin top ile oynanan bir oyun olsun. Oyundaki top kavramı Top isimli bir sınıfla temsil ediliyor olsun. Topun gitmesi çokbiçimli bir eylemdir. Top sınıfında git isimli sanal bir fonksiyon bulundurulur. Değişik toplar bu Top sınıfından türetilir ve bu git fonksiyonu bu sınıflarda override edilir. Böylece kod taban Top sınıfına dayalı olarak genel bir biçimde oluşturulur. Örneğin: Top NormalTop PatlakTop ZiplayanTop Top *top; top = top_sec("NormalTop"); top->git(); ... top->git(); ... top->git(); ... Burada top_sec fonksiyonu Top sınıfından türetilmiş bir sınıf türünden nesneyi dinamik olarak tahsis edip onun adresiyle geri dönmektedir. Böylece git fonksiyonu çağrıldığında top_sec fonksiyonu hangi çeşit topu verdiyse aslında o top gidecektir. Bu oyuna yeni bir top çeşiti ekleyecek olsak kodun bu kısımlarında bir değişiklik yapmamız gerekmez. Tek yapacağımız şey yeni Top sınıfından yeni bir top sınıfı türetip sanal fonksiyonları o sınıfta override etmektir. Örneğin: Top NormalTop PatlakTop ZiplayanTop HafifTop Tabii top_sec fonksiyonuna da eklemelerin yapılması gerekir. Pekiyi burada yeni top eklenirken top_al fonksiyonunda değişiklik yapılması yine kodda bozulma riski oluşturmaz mı? Aslında bu fonksiyonda değişiklik yapmadan da top_sec fonksiyonunun bizim eklediğimiz hafif topu vermesi sağlanabilir. Buna NYPT'de "fabrika kalıbı (factory pattern)" denilmektedir. Tipik olarak NYPT'de proje içerisinde değişebilecek öğeler belirlenir. Bunlara doğrudan değil taban sınıf yoluyla çokbiçimli mekanizmayla erişilir. Bu biçimdeki erişime "arayüz (interface) yoluyla" erişim de denilmektedir. Aşağıda bu örneğin basit bir simülasyonu yapılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class Top { public: virtual void git(); //... }; class NormalTop : public Top { public: void git() override; //... }; class PatlakTop : public Top { public: void git() override; //... }; class ZiplayanTop : public Top { public: void git() override; }; class Oyun { public: void baslat(); //... private: Top *top_sec(); //... }; void Top::git() { cout << "Top gidiyor" << endl; } void NormalTop::git() { cout << "NormalTop gidiyor" << endl; } void PatlakTop::git() { cout << "PatlakTop gidiyor" << endl; } void ZiplayanTop::git() { cout << "PatlakTop gidiyor" << endl; } Top *Oyun::top_sec() { Top *top; string topismi; cout << "Top ismi: "; cin >> topismi; if (topismi == "NormalTop") top = new NormalTop(); else if (topismi == "PatlakTop") top = new PatlakTop(); else if (topismi == "ZiplayanTop") top = new ZiplayanTop(); else throw runtime_error("gecersiz top"); return top; } void Oyun::baslat() { Top *top; top = top_sec(); top->git(); //... top->git(); //... top->git(); //... } int main() { Oyun oyun; oyun.baslat(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıdaki programda oyuna yeni bir top cinsi ekleyecek olalım. Tek yapacağımız şey aslında bu yeni top sınıfını Top sınıfından türetmek ve sanal fonksiyonları bu sınıfta override etmektir. Tabii top_sec fonksiyonunda bu yeni topun da verilmesini sağlamalıyız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class Top { public: virtual void git(); //... }; class NormalTop : public Top { public: void git() override; //... }; class PatlakTop : public Top { public: void git() override; //... }; class ZiplayanTop : public Top { public: void git() override; }; class HafifTop : public Top { public: void git() override; //... }; class Oyun { public: void baslat(); //... private: Top *top_sec(); //... }; void Top::git() { cout << "Top gidiyor" << endl; } void NormalTop::git() { cout << "NormalTop gidiyor" << endl; } void PatlakTop::git() { cout << "PatlakTop gidiyor" << endl; } void ZiplayanTop::git() { cout << "PatlakTop gidiyor" << endl; } void HafifTop::git() { cout << "HafifTop gidiyor" << endl; } Top *Oyun::top_sec() { Top *top; string topismi; cout << "Top ismi: "; cin >> topismi; if (topismi == "NormalTop") top = new NormalTop(); else if (topismi == "PatlakTop") top = new PatlakTop(); else if (topismi == "ZiplayanTop") top = new ZiplayanTop(); else if (topismi == "HafifTop") top = new HafifTop(); else throw runtime_error("gecersiz top"); return top; } void Oyun::baslat() { Top *top; top = top_sec(); top->git(); //... top->git(); //... top->git(); //... } int main() { Oyun oyun; oyun.baslat(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çok karşılaşılan bir kalıp türemiş sınıflara ilişkin nesne adreslerinin taban sınıfa ilişkin br gösterici dizisinde tutulması ve bu dizi elemanlarıyla taban sınıfın sanal fonksiyonlarının çağrılmasıdır. Örneğin aşağıdaki gibi bir türetme şeması söz konusu olsun: Shape RectangleShape EllipseShape LineShape Shape sınıfının draw isimli sanal bir fonksiyonunun olduğunu ve bu fonksiyonun türemiş sınıflarda override edildiğini varsayalım. Biz farklı türemiş sınıf nesnelerinin adreslerini taban sınıf türünden bir dizi de tutabiliriz. Örneğin: vector shapes; Burada shapes isimli vector nesnesinin elemanları Shape * türündendir. Yani aslında bu shapes nesnesi dinamik farklı olan heterojen nesneleri tutabilmektedir. Şimdi bu vektöre eklemeler yapalım: shapes.push_back(new RectangleShape()); shapes.push_back(new EllipseShape()); shapes.push_back(new LineShape()); shapes.push_back(new RectangleShape()); shapes.push_back(new EllipseShape()); shapes.push_back(new EllipseShape()); Artık bu vektörün her bir elemanı aslında tür bakımından farklı olabilen ancak Shape sınfından türetilmiş olan nesneleri göstermektedir. Şimdi bu shapes vektörünün her elemanı için draw sanal fonksiyonunu çağıralım: for (Shape *shape : shapes) shape->draw(); Vektörün ilgili elemanı hangi sınıf türündense onun draw fonksiyonu çağrılacaktır. Yukarıdaki for döngüsünde farklı türler sanki aynı türmüş gibi işleme sokulmultur. Aşağıda bu örneğe ilişkin kod verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Shape { public: virtual void draw(); //... }; class RectangleShape : public Shape { public: void draw() override; //... }; class EllipseShape : public Shape { public: void draw() override; //... }; class LineShape : public Shape { public: void draw() override; //... }; void Shape::draw() {} void RectangleShape::draw() { cout << "RectangleShape::draw" << endl; } void EllipseShape::draw() { cout << "EllipseShape::draw" << endl; } void LineShape::draw() { cout << "LineShape::draw" << endl; } int main() { vector shapes; shapes.push_back(new RectangleShape()); shapes.push_back(new EllipseShape()); shapes.push_back(new LineShape()); shapes.push_back(new RectangleShape()); shapes.push_back(new EllipseShape()); shapes.push_back(new EllipseShape()); for (Shape *shape : shapes) shape->draw(); //... for (Shape *shape : shapes) delete shape; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- PowerPoint benzeri bir program yazacak olalım. Programda dikdörtgen, elips, doğru gibi farklı çizim öğeleri fareyle çizilip üzerine tıklandığında seçilebilsin. GUI kütüphaneleri fare ile tıklandığında bize tıklanan yerin koordinatı vermektedir. Bu uygulamada her şekil Shape isimli sınıftan türetilmiş sınıflarla temsil edilebilir. Örneğin: Shape RectangleShape EllipseShape LineShape Bir şekil çizildiğinde şeklin koordinatları ilgili sınıfın veri elemanlarında saklanabilir. Böylece tıpkı yukarıdaki örnekte olduğu gibi çizilen her şekil Shape sınıfı türünden adresleri tutan bir vektörde biriktirilebilir: vector shapes; Bir noktanın bir şeklin içinde olmasının tespit edilmesi çokbiçimli bir eylemdir. Çünkü örneğin noktanın dikdört içerisinde olduğunun tespiti ile doğru üzerinde olduğunun tespiti farklı biçimlerde yapılmaktadır. Noktanın ilgili şeklin içerisinde olup olmadığını tespit eden fonksiyon Shape sınıfının sanal bir fonksiyonu olabilir. Bu fonksiyon türemiş sınıflarda override edilebilir. Örneğin: class Shape { public: virtual bool isinside(int x, int y); //... }; Bu durumda fare ile tıklandığında tıklanan yerin çizilmiş bir şeklin içinde olup olmadığı aşağıdaki gibi tespit edilebilir: for (Shape *shape : shapes) if (shape->isinside(x, y)) { //... break; } Öte yandan bir şekle tıklandığında şeklin küçük yuvarlakçıklarla görsel biçimde seçilmesi de çokbiçimli bir eylemdir. Çünkü seçim sırasında bu yuvarlakçıklar o şekle göre çizilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 72. Ders 22/05/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tetris oyununda dörtlü şekiller aşağıya doğru düşmekte bunlar sola sağa hareket ettirilebilme ve döndürülebilmektedir. Şekillerin aşağıya düşmesi, sola, sağa hareket etmesi, döndürülmesi çokbiçimli eylemlerdir. Yani tüm Tetris şekillerinde bu hareketler vardır ancak bu hareketler şekle göre farklı çizimlerle gerçekleştirilmektedir. İşte böyle bir oyunun şekillerle ilgili bu kısmını çokbiçimli mekanizmayı kullanarak yazabiliriz. Tüm şekilleri tepedeki Shape isimli bir sınıftan türetebiliriz. Şekillerin ortak özelliklerini bu Shape sınıfında toplayabiliriz. Şeklin hareketleri Shape sınıfında tanımlanmış sanal fonksiyonlarla sağlanabilir. Bu sanal fonksiyonlar bu şekil sınıflarında override edilebilir. Türetme şeması şöyle olabilir: Shape SquareShape BarShape ZShape TShape LShape Shape sınıfının sanal fonksiyonları şöyle olabilir: class Shape { public: virtual void move_down(); virtual void move_left(); virtual void move_right(); virtual void rotate(); //... }; Bu fonksiyonlar tüm türemiş sınıflarda override edlebilir. Oyunun kendisi de Tetris isimli bir sınıfla temsil edilebilir. Örneğin: class Tetris { public: Tetris(); void run(); //... private: Shape *get_random_shape(); //... }; Oyun aşağıdaki gibi çalıştırılabilir: int main() { Tetris tetris; tetris.run(); return 0; } Tetris sınıfının run üye fonksiyonuna dikkaty ediniz: void Tetris::run() { Shape *shape; int ch; for (;;) { shape = get_random_shape(); for (int i = 0; i < 20; ++i) { shape->move_down(); Sleep(300); if (_kbhit()) { ch = _getch(); if (ch == 'q') { delete shape; goto EXIT; } if (ch == 224) { ch = _getch(); switch (ch) { case Rotate: shape->rotate(); break; case Right: shape->move_right(); break; case Down: goto NEXT; case Left: shape->move_left(); break; } } } } NEXT: delete shape; } EXIT: ; } Burada get_random_shape fonksiyonu bize rasgele bir şekeil nesnenin adresini vermektedir. Yani bu fonksiyonun geri dönüş değeri statik türü Shape olan dinamik türü yukarıdaki şekil sınıflarına ilişkin olan bir nesne adresidir. Dolyısıyla rastgele alınan şekil nesneleri üzerinde ilgili sanal fonksiyonlar çağrıldığında o şekil sınıflarının sanal fonksiyonları çağrılacaktır. Buradaki tuş kontrolü Microsft C derleyicilerinin _kbhit ve _getch fonksiyonları kullanılarak gerçekleştirilmiştir. Tuşa basınca programın beklememesi için "tuşa basılmışsa klavyeden okuma yapma" yoluna gidilmektedir. _getch fonksiyonunda eğer basılan tuş özel bir tuşsa (yani ASCII karakterlerinin dışında bir tuş ise _getch birinci çağırmada 224 özel değerini ikinci çağrışta ise özel tuşun "scan code" değerini vermektedir.) Aşağıda basit bir Tetris simülasonu yapan örnek kodlar verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // shape.hpp #ifndef SHAPE_HPP_ #define SHAPE_HPP_ class Shape { public: virtual void move_down(); virtual void move_left(); virtual void move_right(); virtual void rotate(); //... }; #endif // shape.cpp #include "Shape.hpp" void Shape::move_down() {} void Shape::move_left() {} void Shape::move_right() {} void Shape::rotate() {} // barshape.hpp #ifndef BARSHAPE_HPP_ #define BARSHAPE_HPP_ #include "shape.hpp" class BarShape : public Shape { public: void move_down() override; void move_left() override; void move_right() override; void rotate() override; //... }; #endif // barshape.cpp #include #include "barshape.hpp" using namespace std; void BarShape::move_down() { cout << "BarShape::move_down" << endl; } void BarShape::move_left() { cout << "==> BarShape::move_left <==" << endl; } void BarShape::move_right() { cout << "==> BarShape::move_right <==" << endl; } void BarShape::rotate() { cout << "==> BarShape::rotate <==" << endl; } // zshape.hpp #ifndef ZSHAPE_HPP_ #define ZSHAPE_HPP_ #include "shape.hpp" class ZShape : public Shape { public: void move_down() override; void move_left() override; void move_right() override; void rotate() override; //... }; #endif // zshape.cpp #include #include "zshape.hpp" using namespace std; void ZShape::move_down() { cout << "ZShape::move_down" << endl; } void ZShape::move_left() { cout << "==> ZShape::move_left <==" << endl; } void ZShape::move_right() { cout << "==> ZShape::move_right <==" << endl; } void ZShape::rotate() { cout << "==> ZShape::rotate <==" << endl; } // tshape.hpp #ifndef TSHAPE_HPP_ #define TSHAPE_HPP_ #include "shape.hpp" class TShape : public Shape { public: void move_down() override; void move_left() override; void move_right() override; void rotate() override; //... }; #endif // tshape.cpp #include #include "tshape.hpp" using namespace std; void TShape::move_down() { cout << "TShape::move_down" << endl; } void TShape::move_left() { cout << "===> TShape::move_left <==" << endl; } void TShape::move_right() { cout << "==> TShape::move_right <==" << endl; } void TShape::rotate() { cout << "==> TShape::rotate <==" << endl; } // lshape.hpp #ifndef LSHAPE_HPP_ #define LSHAPE_HPP_ #include "shape.hpp" class LShape : public Shape { public: void move_down() override; void move_left() override; void move_right() override; void rotate() override; //... }; #endif // lshape.cpp #include #include "lshape.hpp" using namespace std; void LShape::move_down() { cout << "LShape::move_down" << endl; } void LShape::move_left() { cout << "==> LShape::move_left <==" << endl; } void LShape::move_right() { cout << "==> LShape::move_right <==" << endl; } void LShape::rotate() { cout << "==> LShape::rotate <==" << endl; } // tetris.hpp #ifndef TETRIS_HPP_ #define TETRIS_HPP_ #include "shape.hpp" class Tetris { public: Tetris(); void run(); //... private: Shape *get_random_shape(); private: const static int NSHAPES = 5; enum Keys {Rotate = 72, Right = 77, Down = 80, Left = 75}; }; #endif // tetris.cpp #include #include #include #include "tetris.hpp" #include #include #include "squareshape.hpp" #include "barshape.hpp" #include "zshape.hpp" #include "tshape.hpp" #include "lshape.hpp" using namespace std; Tetris::Tetris() { srand(time(nullptr)); } void Tetris::run() { Shape *shape; int ch; for (;;) { shape = get_random_shape(); for (int i = 0; i < 20; ++i) { shape->move_down(); Sleep(300); if (_kbhit()) { ch = _getch(); if (ch == 'q') { delete shape; goto EXIT; } if (ch == 224) { ch = _getch(); switch (ch) { case Rotate: shape->rotate(); break; case Right: shape->move_right(); break; case Down: goto NEXT; case Left: shape->move_left(); break; } } } } NEXT: delete shape; } EXIT: ; } Shape *Tetris::get_random_shape() { Shape *shape; switch (rand() % NSHAPES) { case 0: shape = new SquareShape(); break; case 1: shape = new BarShape(); break; case 2: shape = new ZShape(); break; case 3: shape = new TShape(); break; case 4: shape = new LShape(); break; } return shape; } // app.cpp #include #include "tetris.hpp" using namespace std; int main() { Tetris tetris; tetris.run(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önce bir satranç tahtası uygulaması yapmıştık. Bu uygulamada satranç tahtasını Board isimli sınıfla, tahta üzerindeki kareleri Square isimli sınıfla ve taşları da Figure isimli sınıfla temsil etmiştik. Aslında her taş türü ortak özelliklere sahip olsa da birbirinden farklıdır. Satrançta bir taşın gitmesi çokbiçimli bir eylemdir. Yani "her taş gider ama kendisine göre değiş bir biçimde gider". Uygulamayı çokbiçimli mekanizmayı kullanacak hale getirebilmek için önce Figure sınıfından çeşitli taş sınıflarını türetmemiz gerekir. Örneğin: Figure Pawn Knight Bishop Rook Queen King Yine uygulamamızda Square sınıfı Figure sınıfı türünden bir gösterici tutacaktır. Ancak bu göstericinin dinamik türü Figure değil türemiş sınıf türlerine ilişkin olacaktır. Bir taş hareket ettirildiğinde taşın geçerli olarak gidebileceği kareler Figure sınıfının get_valid_moves isimli bir sanal fonksiyonuyla elde edilebilir. Böylece biz bir karedeki taşın türünü bilmeden o taşın kendi hareketlerine göre tahtanın o anki konumununda nerelere gidebileceğini elde edebiliriz. GUI uygulamalarında genellikle bir satranç taşı geçersiz bir kareye bırakıldığında taş eski yerine geri döndürülmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 73. Ders 27/05/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Büyük projeleerde çok miktarda sınıf ve dolayısıyla çok miktarda kaynak dosya söz konusu olabilmektedir. Bir sınıf başka bir sınıfı kullaıyorsa kullanılan sınıfın bildiriminin derleyici tarafından daha önce görülmesi gerekir. Ancak bazen bir sınıf bir sınıfı kullanırken diğeri o sınıfı kullanıyor olabilmektedir. Örneğin: class A { //... B *m_pb; // dikkat! henüz B sınıfı görülmedi! error oluşacak }; class B { //... A *m_pa; }; Burada A sınıfı B sınıfını B sınıfı da A sınıfını kullanmaktadır. Fakat bu kodun derlenmesinde hata oluşacaktır. Çünkü derleyici A sınıfını gördüğünde henüz B sınıfını görmemiştir. Sınıfları yer değiştirsek aynı problem yine oluşacaktır. Halbuki bu tür durumlarla sık karşılaşılmaktadır. Tabii yukarıdaki örnekte elemanların gösterici ya da referans olmaması durumunda zaten mantıksal bir problem vardır ve bu problemin de çözümü olamaz. Örneğin: class A { //... B m_b; // dikkat! henüz B sınıfı görülmedi! error oluşacak }; class B { //... A m_a; }; Burada A ve B sınıfılarının sizoef değeri belli olmadığı için zaten anlamsız bir durum söz konusudr. Pekiyi bir sınıf bir sınıf türünden diğeri de o sınıf türünden gösterici ya da referans veri elemanına sahip ise buradaki bildirim problemi nasıl çözülmektedir? Aslında bu problem C++'a özgü değildir. Yapılar söz konusu olduğunda aynı problem C'de karşımıza çıkmakradır. İşte bu problem "eksik bildirim (incomplete declaration)" kullanılarak çözülmektedir. Bir sınıf türünden bir referans ya da gösterici sınıf bildiriminin tamam mı görülmeden yalnızca onun isimsel bildirimi yapılarak tanımlanabilmektedir. Örneğin: class A; A *pa; // geçerli Tabii bu biçimde bildirilmiş olan göstericilerin ve referansların kullanılması için bildirimin "tamamlanması (complete hale getirilmesi)" gerekmektedir. Başka bir deyişle derleyicinin artık bu sınıf bildiriminin tamımamını görmesi gerekir. Bu durumda yukarıdaki bildirim aşağıdaki gibi düzeltilebilir: class B; class A { //... B *m_pb; // dikkat! henüz B sınıfı görülmedi! error oluşacak }; class B { //... A *m_pa; }; Burada anahtar nokta bir sınıf türünden gösterici ya da referans tanımlamak için derleyicinin o sınıfın bildirimini görmek zorunda olmadığıdır. Çünkü göstericiler ve referanslar için ayrılacak alanın onların türüyle bir ilgisi yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Büyük projelerde önemli bir problem de başlık dosyalarının başka başlık dosylarını include etmesidir. Örneğin biz Sample isimli bir sınıf yazacak olalım. Bu sınıf için "sample.hpp" ve "sample.cpp" dosyalarını oluşturmalıyız. Ancak Sample sınıfı A, B, C, D sınıfları türünden veri elemanlarına sahip ise onlara ilişkin başlık dosyaları mecburen "sample.hpp" içerisinde include edilecektir: // sample.hpp #ifndef SAMPLE_HPP_ #define SAMPLE_HPP_ #include "a.hpp" #include "b.hpp" #include "c.hpp" #include "d.hpp" class Sample { //... private: A m_a; B m_b; C m_c; D m_d; }; #endif Burada Sample sınıfını kullanacak olanlar "sample.hpp" dosyasını include etmelidir. Ancak bu dosya da diğer başlık dosyalarını include ettiği için derleme süresi göreli biçimde uzayacaktır. Proje içerisinde Sample sınıfını kullanan her kaynak dosyada aynı durum söz konusu olacaktır. Küçük projelerde bu türlü durumlar önemli sorun oluşturmamaktadır. Ancak büyük projelerde yukarıda problem derleme zamanının ciddi biçiminde uzamasına yol açabilmektedir. Pekiti bu problem nasıl bertaraf edilebilir? İşte bunun tek çözümü Sample sınıfının A, B, C ve D sınıfı türünden veri elemanları yerine bu sınıflar türünden gösterici veri elemanlarına sahip olmasıdır. Örneğin: // sample.hpp #ifndef SAMPLE_HPP_ #define SAMPLE_HPP_ class A; class B; class C; class D; class Sample { //... private: A *m_pa; B *m_pb; C *m_pc; D *m_pd; }; #endif Artık Sample sınıfını kullanmak isteyen kişiler "sample.hpp" dosyasını include ettikleri zaman diğer include işlemleri için zaman harcamak zorunda kalmayacaklardır. Tabii bu yöntemin de en önemli dezavantajı bu gösterici veri elemanları için elemana sahip sınıfın yapıcı fonksiyonunda (burada Sample) dinamik tahsisat yapıp bunların yıkıcı fonksiyonlarında bu tahsisatı serbest bırakmaktadır. Örneğin Qt isiml,i C++ GUI framework'ünde çalışma yukarıda esasa dayandırılmıştır. Yani "içerme ilişkisi (composition)" nesnnein kendisi ile değil gösterici kullanarak sağlanmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta yukarıdaki temanın daha sistematik hale getirilmiş bir biçimine "pimple (pointer implementation) idiom" denilmektedir. Pimple idiom'un ana amacı yukarıdaki gibi sınıfın başka sınıf türünden private veri elemanları yerine o sınıflar türünden gösterici veri elemanları kullanarak derleme zamanını kısaltmaktır. Pimple idiom bunun biraz daha sistematik hale getirilmiş biçimidir. Pimple idiom genel olarak şöyle kullanılmaktadır: 1) Sınıfın private elemanları başka bir sınıf içerisine alınır. Tabii sınıf dış dünyadan gizlenmelidir. Bu nedenle bu sınıf asıl sınıfın private bölümünde iç sınıf olarak bildirilir. Biz henüz iç sınıfları görmedik. İç bir sınıfın (yani başka bir sınıfın içerisinde bildirilmiş sınıfın) hiçbir ayrıcalığı yoktur. Başka bir deyişle iç sınıf ile dış sınıf arasında bir "data içermesi" söz konusu değildir. İç sınıfın dış sınıfa dış sınfın da iç sınıfa özel bir erişimi yoktur. 2) Sınıfın private veri elemanları için asıl sınıfın private bölümünde bir gösterici veri elemanı tutulur. Tabii bu gösterici eleman için "eksik bildirim (incomplete declartion)" uygulanır. 3) private veri elemanlarına ilişkin sınıf başlık dosyasında değil "cpp" dosyasında bildirilir. 4) private veri elemanlarına ilişkin sınıf nesnesi asıl sınıfın yapıcı fonksiyonunda new operatörü ile tahsis edilir. delete operatörü ile bu nesne yok edilir. Şimdi bu idiom için bir örnek verelim. Örneğimizde Sample sınıfı vector sınıfı türünden ve string sınıfı türünden veri elemanlarına sahip olsun. Eğer pimple idion kullanılmazsa Sample sınıfının bildirimi aşağıdaki olacaktır: // sample.hpp #ifndef SAMPLE_HPP_ #define SAMPLE_HPP_ #include #include class Sample { //... private: std::vector m_v; std::string m_s; }; #endif Buradaki problem "sample.hpp" dosyasını include eden kişinin ve dosyalarını da include etmiş olmasıdır. Şimdi pimple idiom uygulayalım: // sample.hpp #ifndef SAMPLE_HPP_ #define SAMPLE_HPP_ class Sample { public: Sample(); ~Sample(); //... private: struct Impl; Impl *m_impl; }; #endif Görüldüğü gibi Sample sınıfının private elemanları başka bir sınıfın içerisine alınmış ve "içerme ilişkisi (composition)" gösterici yoluyla oluşturulmuştur. Impl sınıfının Sample sınıfı içerisinde bildirildiğine dikkat ediniz. "sample.cpp" dosyası da şöyle olacaktır: #include #include #include #include "sample.hpp" using namespace std; class Sample::Impl { vector m_v; string m_s; //... }; Sample::Sample() { m_impl = new Impl(); //... } Sample::~Sample() { delete m_impl; } Yukarıda da belirttiğimiz gibi pimpl idiom kullanmanın derleme zamanını kısaltması yanında bazı dezavantajları da vardır. Bunlar şöyle sıralanabilir: - Daha karmaşık ve zahmetli bir organizasyon oluşturması - Dinamik tahsisata gereksinim duyulması (dinamik tahsisatlar belli bir zaman almaktadır) - Tüm private veri elemanlarına gösterici yoluyla erişilmektedir. Bir elemana doğrudan erişmekle gösterici erişme arasında nano düzeyde farklılık vardır. Sonuç olarak büyük bir proje söz konusu değilse pimple idiom yerine normal yöntem tercih edilebilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 74. Ders 29/05/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de daha önce yapmış olduğumuz satranç tahtası uygulmasında çokbiçimli mekanizmayı kullanalım. Anımsayacağınız gibi biz daha öce satranç taşlarının hepsini Figure sınıfyla temsil etmiştik. Bir taşın ne taşı olduğunu Figure sınıfının içerisinde tutmuştuk. Şimdi her farklı taş için Figure sınıfından sınıflar türetelim: Figure Pawn Knight Bishop Rook Queen King Tabii taşların ortak birtakım özellikleri yine taban sınıf olan Figure sınıfında bulundurulmalıdır. Örneğin taşın rengi ve taşın bulunduğu kare taban Figure sınıfında saklanabilir. Bu tasarımda Square sınıfı yine Figure türünden bir gösterici veri elemanına saip olacaktır. Ancak bu gösterici veri elemanının dinamik türü (yani gösterdiği yerdeki nesne) Pawn, Knight, Bishop, Rook, Queen ya da King olacaktır. Buradaki Figure sınıfı aslında "taş kavramını temsil eden" bir sınıftır. Dolayısıyla mekanizma sayesinde bir karenin (Square nesnesinin) içerisindeki taşın türünü bilmeden çokbiçimli işlemler yapılabilir. Figure sınıfının aşağıdaki gibi sanal fonksiyonları söz konusu olabilir: #ifndef FIGURE_HPP_ #define FIGURE_HPP_ #include #include "chess.hpp" class Board; class Figure { public: Figure(Color color, const Pos &pos); Color color() const { return m_color; } const Pos &pos() { return m_pos; } virtual void disp() const; virtual char fsym() const; virtual std::vector valid_moves(Board &board); protected: static const char *ms_colors[2]; private: Color m_color; Pos m_pos; }; #endif Her taş sınıfının bir disp üye fonksiyonu vardır. Bu disp üye fonksiyonu her taş sınıfı için o taşa özgü bir yazdırma yapmaktadır. Her taşın bir tahtada görüntülenecek bir sembolü vardır. Ancak bu sembol her taş için farklıdır. Sınıfın fsym üye fonksiyonu taş sınıfına ilişkin bu sembolü vermektedir. Tahtanın belli bir konumunda her taşın gidebileceği yerler o taşa özgü biçimde belirlenmektedir. valid_moves fonksiyonu tahtanın konumunu alarak ilgi taşın gidebileceği kareleri bir vector nesnesi biçiminde vermektedir. Taşın gidebileceği karelerin elde edilmesi işlemi çokbiçimli bir eylemdir. Aşağıda satranç tahtası uygulaması yukarıda açıkladığımız haliyle verilmiştir. Ancak valid_moves fonksiyonlarının içi yazılmamıştır. Bu fonksiyonların içini yazmaya çalışabilirsiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // chess.hpp #ifndef CHESS_HPP_ #define CHESS_HPP_ enum class Color { Black, White }; enum class FigureType { King, Queen, Rook, Bishop, Knight, Pawn }; struct Pos { Pos(char col, char row) : m_col(col), m_row(row) {} char m_col; char m_row; }; #endif // board.hpp #ifndef BOARD_HPP_ #define BOARD_HPP_ #include #include "square.hpp" class Board { public: Board(); Square &at(char col, char row); void move(const std::string &m); void disp() const; private: Square m_squares[8][8]; }; #endif #include #include #include "board.hpp" #include "pawn.hpp" #include "knight.hpp" #include "bishop.hpp" #include "rook.hpp" #include "queen.hpp" #include "king.hpp" using namespace std; Board::Board() { for (int row = 0; row < 8; ++row) for (int col = 0; col < 8; ++col) m_squares[row][col].color((row + col) % 2 ? Color::White : Color::Black); for (int i = 0; i < 8; ++i) { m_squares[1][i].figure(new Pawn(Color::White, Pos(i + 'A', '2'))); m_squares[6][i].figure(new Pawn(Color::Black, Pos(i + 'A', '2'))); } m_squares[0][0].figure(new Rook(Color::White, Pos('A', '1'))); m_squares[7][0].figure(new Rook(Color::Black, Pos('H', '1'))); m_squares[0][7].figure(new Rook(Color::White, Pos('A', '8'))); m_squares[7][7].figure(new Rook(Color::Black, Pos('H', '8'))); m_squares[0][1].figure(new Knight(Color::White, Pos('B', '1'))); m_squares[7][1].figure(new Knight(Color::Black, Pos('G', '1'))); m_squares[0][6].figure(new Knight(Color::White, Pos('B', '8'))); m_squares[7][6].figure(new Knight(Color::Black, Pos('G', '8'))); m_squares[0][2].figure(new Bishop(Color::White, Pos('C', '1'))); m_squares[7][2].figure(new Bishop(Color::Black, Pos('F', '1'))); m_squares[0][5].figure(new Bishop(Color::White, Pos('C', '8'))); m_squares[7][5].figure(new Bishop(Color::Black, Pos('F', '8'))); m_squares[0][3].figure(new Queen(Color::White, Pos('D', '1'))); m_squares[7][3].figure(new Queen(Color::Black, Pos('D', '8'))); m_squares[0][4].figure(new King(Color::White, Pos('E', '1'))); m_squares[7][4].figure(new King(Color::Black, Pos('E', '8'))); } Square &Board::at(char col, char row) { return m_squares[tolower(row) - '1'][tolower(col) - 'a']; } void Board::move(const std::string &m) { string::size_type pos = m.find('-'); if (pos == string::npos) throw invalid_argument("invalid move format"); Square &source = at(m[0], m[1]); Square &target = at(m[3], m[4]); target.put(source.take()); } void Board::disp() const { const char *fg_black = "\x1b[34m"; const char *fg_white = "\x1b[33m"; const char *bg_black = "\x1b[40m"; const char *bg_white = "\x1b[47m"; const char *reset = "\x1b[0m"; for (int row = 7; row >= 0; --row) { for (int col = 0; col < 8; ++col) { cout << (m_squares[row][col].color() == Color::Black ? bg_black : bg_white); cout << ' '; auto figure = m_squares[row][col].figure(); if (figure != nullptr) { cout << (figure->color() == Color::Black ? fg_black : fg_white); cout << m_squares[row][col].figure()->fsym(); } else cout << ' '; cout << ' '; } cout << reset << endl; } cout << reset << endl; } // square.hpp #ifndef SQUARE_HPP_ #define SQUARE_HPP_ #include "chess.hpp" class Figure; class Square { public: Square(); Color color() const { return m_color; } void color(Color color){ m_color = color; } void figure(Figure *figure) { m_figure = figure; } Figure *figure() const { return m_figure; } Figure *take(); void put(Figure *figure); private: Color m_color; Figure *m_figure; }; #endif // square.cpp #include #include "square.hpp" using namespace std; Square::Square() : m_figure(nullptr) {} Figure *Square::take() { Figure *figure; figure = m_figure; m_figure = nullptr; return figure; } void Square::put(Figure *figure) { if (m_figure != nullptr) delete m_figure; m_figure = figure; } // figure.hpp #ifndef FIGURE_HPP_ #define FIGURE_HPP_ #include #include "chess.hpp" class Board; class Figure { public: Figure(Color color, const Pos &pos); Color color() const { return m_color; } const Pos &pos() { return m_pos; } virtual void disp() const; virtual char fsym() const; virtual std::vector valid_moves(Board &board); protected: static const char *ms_colors[2]; private: Color m_color; Pos m_pos; }; #endif // figure.cpp #include #include "figure.hpp" using namespace std; const char *Figure::ms_colors[2] = {"Siyah", "Beyaz"}; Figure::Figure(Color color, const Pos &pos) : m_color(color), m_pos(pos) {} void Figure::disp() const {} char Figure::fsym() const { return '?'; } vector Figure::valid_moves(Board &board) { return vector(); } // pawn.hpp #ifndef PAWN_HPP_ #define PAWN_HPP_ #include "figure.hpp" class Pawn : public Figure { public: Pawn(Color color, const Pos &pos) : Figure(color, pos) {} void disp() const override; char fsym() const override; std::vector valid_moves(Board &board) override; }; #endif // pawn.cpp #include #include #include "pawn.hpp" #include "board.hpp" using namespace std; void Pawn::disp() const { cout << ms_colors[static_cast(color())] << " Piyon" << endl; } char Pawn::fsym() const { return 'p'; } vector Pawn::valid_moves(Board &board) { // İçi doldurulacak return vector(); } // knight.hpp #ifndef KNIGHT_HPP_ #define KNIGHT_HPP_ #include "figure.hpp" class Knight : public Figure { public: Knight(Color color, const Pos &pos) : Figure(color, pos) {} void disp() const override; char fsym() const override; std::vector valid_moves(Board &board) override; }; #endif // knight.cpp #include #include "knight.hpp" using namespace std; void Knight::disp() const { cout << ms_colors[static_cast(color())] << " At" << endl; } char Knight::fsym() const { return 'a'; } vector Knight::valid_moves(Board &board) { // İçi doldurulacak return vector(); } // bishop.hpp #ifndef BISHOP_HPP_ #define BISHOP_HPP_ #include "figure.hpp" class Bishop : public Figure { public: Bishop(Color color, const Pos &pos) : Figure(color, pos) {} void disp() const override; char fsym() const override; std::vector valid_moves(Board &board) override; }; #endif // bishop.cpp #include #include "bishop.hpp" using namespace std; void Bishop::disp() const { cout << ms_colors[static_cast(color())] << " Fil" << endl; } char Bishop::fsym() const { return 'f'; } vector Bishop::valid_moves(Board &board) { // İçi doldurulacak return vector(); } // rook.hpp #ifndef ROOK_HPP_ #define ROOK_HPP_ #include "figure.hpp" class Rook : public Figure { public: Rook(Color color, const Pos &pos) : Figure(color, pos) {} void disp() const override; char fsym() const override; std::vector valid_moves(Board &board) override; }; #endif // rook.cpp #include #include "rook.hpp" using namespace std; void Rook::disp() const { cout << ms_colors[static_cast(color())] << " Kale" << endl; } char Rook::fsym() const { return 'k'; } vector Rook::valid_moves(Board &board) { // İçi doldurulacak return vector(); } // queen.hpp #ifndef QUEEN_HPP_ #define QUEEN_HPP_ #include "figure.hpp" class Queen : public Figure { public: Queen(Color color, const Pos &pos) : Figure(color, pos) {} void disp() const override; char fsym() const override; std::vector valid_moves(Board &board) override; }; #endif // queen.cpp #include #include "queen.hpp" using namespace std; void Queen::disp() const { cout << ms_colors[static_cast(color())] << " Vezir" << endl; } char Queen::fsym() const { return 'v'; } vector Queen::valid_moves(Board &board) { // İçi doldurulacak return vector(); } // king.hpp #ifndef KING_HPP_ #define KING_HPP_ #include "figure.hpp" class King : public Figure { public: King(Color color, const Pos &pos) : Figure(color, pos) {} void disp() const override; char fsym() const override; std::vector valid_moves(Board &board) override; }; #endif // king.cpp #include #include "king.hpp" using namespace std; void King::disp() const { cout << ms_colors[static_cast(color())] << " Sah" << endl; } char King::fsym() const { return 's'; } vector King::valid_moves(Board &board) { // İçi doldurulacak return vector(); } // app.cpp #include #include "board.hpp" using namespace std; int main() { Board board; board.disp(); board.move("e2-e4"); board.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 75. Ders 03/06/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çokbiçimli mekanizmaya diğer bir örnek veri yapıları konusu üzerinde verilebilir. Bazı veri yapılarında bazı işlemler çokbiçimlidir. Yani bu işlemler bu veri yapılarında bulunur ancak bu işlemler her veri yapısında o veri yapısına özgü bir biçimde gerçekleştirilmektedir. Örneğin pek çok veri yapısında "sona eleman ekleme" biçiminde bir işlem vardır. Ancak dinamik dizilerde sona eklenmesiyle bağlı listelerde sona eleman eklenmesi farklı biçimlerde yapılmaktadır. İşte türden bağımsız bir biçimde pek çok veri yapısı için çalışacak bir sınıf yazmak istediğimizde çokbiçimlilikten faydalanabiliriz. Örneğin: List LinkedList ArrayList ... Elemanlar arasında öncelik sonralık ilişkisi olan veri yapılarına "liste tarzı veri yapıları" denilmektedir. Bağlı listeler (linkedList), dinamik büyütülen diziler (ArrayList), liste tarzı veri yapısıdır. Bu liste tarzı veri yapılarında bazı işlemler ortak ancak çokbiçimli olarak bulunurlar. Örneğin "sona eleman ekleme" bağlı listelerde de dinamik büyütülen dizilerde de söz konusudur. araya eleman ekleme de benzer biçimde bu veri yapılarında söz konusudur. Ancak bazı veri yapılarında bu işlemler söz konusu olmayabilir. İşte List sınıfında bu işlemler için sanal fonksiyonlar bulundurulup bunlar türemiş sınıflarda override edilirse türden bağımsız veri yapıları ile işlemler yapılabilir. Örneğin: class List { public: virtual size_t add(int val); virtual void insert(int pos, int val); virtual void remove(int pos); virtual void clear(); //... }; class Sample { public: Sample(List *list) : m_list(list) {} void do_something(); //... private: List *m_list; }; void Sample::do_something() { //... m_list->add(val); //... m_list->insert(pos, val); //... m_list->remove(pos); //... }; //... LinkledList ll; Sample s(&ll); s.do_something(); Burada Sample sınıfı veri yapısı olarak bağlı liste kullanmaktadır. Ancak biz sınıfa dokunmadan Sample sınıfının dinamki büyütülen dizi kullanmasını da sağlayabiliriz. Örneğin: ArrayList al; Sample s(&al); s.do_something(); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- O halde farklı kavramların ortak birtakım eylemsel özellikleri varsa bu eylemsel özellikler bir taban sınıfta toplanıp sanal fonksiyonlarla temsil edilebilirler. Böylece biz de ortak eylemsel özellikleri kullanan türden bağımzıs kod parçaları oluşturabiliriz. Daha önceden de belirttiğimiz gibi proje içerisinde değişebilecek öğeler varsa kodumuzun bu değişimden etkilenmemesi için çokbiçimli mekanizmayı kullanabiliriz. Örneğin bir Parser sınıfı yazmak isteyelim. Parser sınıfı bir kaynaktan karakterleri okuyarak işlemini ypıyor olsun. Burada kaynak çeşitli biçimlerde olabilir. Örneğin karakterler bir dosyadan bir soketten, bir string'ten (yani char türden bir diziden) alınıyor olabilir. Burada kaynak değişebilmektedir. Bu değişimden Parser sınıfımızın etkilenmemesi için kaynağı bir taban sınıf ile temsil edebiliriz ve çokbiçimli mekanizmadan faydalanabiliriz. Örneğin: Source FileSource MemorySource SocketSource Burarada Source sınıfının şöyle bildirildiğini varsayalım: class Source { public: virtual char read_char(); //... }; Bu sanal fonksiyonun türemiş sınıflarda override edildiğini varsayalım. Bu durumda Parser sınıfımızı kaynaktan bağımsız bir biçimde şöyle oluşturabiliriz: class Parser { public: Parser(Source *source) : m_source(source) {} void do_parse(); //... private: Source *m_source }; void Parser::do_parse() { char ch; //... ch = m_source->read_char(); //... ch = m_source->read_char(); //... ch = m_source->read_char(); } Şimdi Parser sınıfımızın kaynak olarak dosya kullanacağını varsayalım: FileSource fs("test.txt"); Parser parser(&fs); parser.do_parse(); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Örneğin C# Programlama Dilinde .NET sınıf kütüphanesinde bulunan Stream isimli sınıf "dosya gibi işlem gören" genel bir kavramı temsil etmektedir. Stream sınıfının Read, Write, Seek gibi çeşitli sanal fonksiyonları vardır. Bu sanal fonksiyonlara sahip olan türemiş sınıflarda override edilmiştir. Bötylece biz Stream sınıfı türünden bir referans parametreli fonksiyona aslında Stream sınıfından türetilen herhangi bir sınıf nesnesini parametere olarak geçirebiliriz. Read, Write ve Seek eylemleri çok biçimli eylemlerdir. Örneğin "dosyadan da okuma söz konusur, soketten de okuma söz konusudur, bellekteki bir diziden de okuma söz konusudur." --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çokbiçimli uygulamalarda çoğu kez taban sınıf aslında türden bağımsız işlem yapmak için bulundurulmaktadır. Yani aslında o taban sınıf türünden bir nesne yaratılmamaktadır. İşte bu tür durumlarda taban sınıftaki sanal fonksiyonların boşuna gövdeye sahip olması gerekmez. Eğer taban sınıftaki sanal fonksiyonun gövdeye sahip olması istenmiyorsa fonksiyonun parametre parantezinden sonra "= 0" sentaksı kullanılır. Yalnızca sanal fonksiyonlarda bu sentaks kullanılabilmektedir. Bu tür fonksiyonlara C++'ta "= 0" sentaksı ile bildirilmiş olan sanal fonksiyonlara "saf sanal fonksiyonlar (pure virtual functions)" denilmektedir. Örneğin: class A { public: virtual void foo() = 0; //... }; Burada foo fonksiyonu "saf sanal (pure virtual)" bir fonksiyondur. En az bir safsanal fonksiyona sahip olan sınıfa "soyut sınıf (abstract class)" denilmektedir. Yukaırdaki örnekte A bir soyut sınıftır. Soyut sınıflar normal üye fonksiyonlara, yapıcı ve yıkıcı fonksiyonlara , veri elemanlarına sahip olabilirler. Soyut sınıflar türünden nesneler yaratılamaz. Çünkü bu durumda gövdesi olmayan bir fonksiyonun çağrılması gibi bir durumla karşılaşılabilir. Örneğin yukarıdaki A sınıfı türünden bir nesne yaratılamaz: A a; // geçersiz! soyut sınıflar türünden nesneler yaratılmaz Yukarıdaki tanımlama geçersizdir. Eğer bu tanımlama geçerli olsaydı bu durumda aşağıdaki bir çağrıda ne olacaktı? a.foo(); // olmayan bir fonksiyon çağrılamaz! Soyut sınıflar türünden nesne yaratımı new operatörüyle de yapılamamaktadır. Örneğin: A *pa = new A(); // geçersiz! soyut bir sınıf türünden dinamik nesne de yaratılamaz Pekiyi soyut sınıflar türünden nesneler yaratılamıyorsa soyut sınıflar ne işe yaramaktadır? İşte soyut sınıflar türünden nesneler yaratılamaz ancak göstericiler ve referanslar tanımlanabilir. Örneğin: A *pa; // geçerli, soyut sınıflar türünden göstericiler tanımlanabilir Bir soyut sınıftan bir sınıf türetilip soyut sınıftaki bütün saf sanal fonksiyonlar override edilirse bu durumda türemiş sınıf soyut olmaz, somut (concrete) hale gelir. Örneğin: class A { public: virtual void foo() = 0; //... }; class B : public A { public: void foo() override; //... }; B b; // geçerli, B soyut değil somut b.foo(); // geçerli, B::foo çağrılacak A *pa; // geçerli, A sınıfı türünden gösterici tanımlanabilir pa = &b; // geçerli pa->foo(); // geçerli, B::foo çağrılır Burada artık olmayan bir fonksiyonun çağrılması gibi potansiyel bir durum oluşmamaktadır. Soyut bir sınıftan türetilmiş olan bir sınıfta soyut sınıfın tüm saf sanal fonksiyonları override edilmemişse türemiş sınıf da soyut olur. Bu durumda türemiş sınıf türünden de nesneler yaratılamaz. Örneğin: class A { public: virtual void foo() = 0; virtual void bar() = 0; //... }; class B : public A { public: void foo() override; //... }; Burada B sınıfında A sınıfının yalnızca foo fonksiyonu override edilmiştir, bar fonksiyonu override edilmemiştir. Bu duurumda B sınıfı da soyut bir sınıftır. B sınıfı türünden de nesneler yaratılamaz. Örneğişn: B b; // geçersiz! B sınıfı da soyut bir sınıf Pekiyi neden burada B sınıfı da soyut bir sınıftır? Çünkü artık B türünden bir nesne ile olmayan bir fonksiyonun çağrılması gibi potansiyel bir durum yine oluşmuştur. Örneğin: B b; b.foo(); // B::foo var b.bar(); // B sınıfında da A sınıfında da bar fonksiyonu yok! A soyut bir sınıf olsun. A sınıfından türetilen B sınıfında A sınıfının bazı saf sanal fonksiyonları override edilmiş olsun, bazıları ise override edilmemiş olsun. Bu durumda B sınıfı da soyut bir sınıftır. Şimdi B sınıfından C sınıfını türetmiş olalım. C sınıfında da B sınıfının override etmediği A sınıfındaki klan saf sanal fonksiyonları override etmiş olalım. Bu durumda C sınıfı somut bir sınıftır. Yani C ınıfı türünden nesneler yaratabiliriz. Örneğin: class A { public: virtual void foo() = 0; virtual void bar() = 0; //... }; class B : public A { public: void foo() override; //... } class C : public B { public: void bar() override; //... }; Burada A ve B sınıfları soyuttur. Ancak C sınıfı soyut değildir. Bir sınıfın soyut olup olmadığını anlamanın kolay bir yolu vardır. Eğer o sınıf türünden bir nesne yaratıldığında "olmayan bir fonksiyonun çağrılması gibi" potansiyel bir durum oluşuyorsa o sınıf soyuttur, oluşmuyorsa o sınıf somuttur. Yukarıdaki örnekte C sınıfı türünden bir nesne yaratmış olalım: C c; Bu c nesnesi ile foo ve bar fonksiyonlarını çağırdığımızda bunlar gerçekten var mıdır? c.foo(); // B::foo çağrılır c.bar(); // C::bar çağrılır O halde C sınıfı soyut değil somuttur. Saf sanal fonksiyonlar gövdeye sahip olmak zorunda değildir. ancak istenirse onlar için gövde de bulundurulabilir. Fakaty saf sanal fonksiyonlar için gövde bulundurulması o sınıfları soyut olmaktan çıkarmamaktadır. Örneğin: class A { public: virtual void foo() = 0; //... }; void A::foo() { //... } Bu tanımala tamamen geçerlidir. Ancak A sınıfı hala soyut bir sınıftır. Örneğin: A a; // geçersiz! A soyut bir sınıf Burada A sınıfı türünden bir nesne yaratılamayacağına göre A sınıfından türetilmiş olan somut sınıfların foo fonksiyonunu override etmiş olması gerektiğine göre A sınıfının foo saf sanal fonksiyonunun gövdesinin olmasının ne anlamı vardır? İşte türemiş sınıf üye fonksiyonları bu durumda sanallağı ortadan kaldırarak taban sınıfın saf sanal fonksiyonunu çağırabilmektedir. Bu konu izleyen paragraflarda ele alınmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Soyut sınıflar türünden nesneler yaratılmaz. Ancak göstericiler ve referanslar tanımlanabilir. Örneğin A bir soyut sınıf olsun B de bu soyut sınıftan türetilmiş soyut olmayan (somut (concrete) de diyebiliriz) bir sınıf olsun: A a; // error! soyut sınıf türündne nesne yaratılamaz. A *pa; // geçerli, soyut sınıf türünden gösterici yaratılabilir. Bir soyut sııf türündne gösterici ya da referansa tipik olarak o sınıftan türetilmiş bir sınıf nesnesinin adresi atanabilir. Örneğin: A *pa; B b; pa = &b; // geçerli A &r = b; // geçerli pa->foo(); // B::foo çağrılır r.foo(); // B::foo çağrılır O halde örneğin daha önce yapmış olduğumuz Tetris örneğindeki düşen şekilleri temsil eden Shape sınıfı soyut bir sınıf olabilir: class Shape { public: virtual void move_down() = 0; virtual void move_left() = 0; virtual void move_right() = 0; virtual void rotate() = 0; //... }; Artık biz zaten Shape sınıfının bu üye fonksiyonları için gövde bulundurmak zorunda değiliz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi biz bir sanal fonksiyonu saf sanal yapmalı mıyız? Eğer bir sanal fonksiyonu saf sanal yaparsak bu durumda sınıfımız soyut olur. Başkalrı da bu sınıfı doğrudan kullanamaz. Başkalarının bu sınıftan faydalanabilmesi için ondan türetme yapıp bu sanal fonkisyonu override etmeleri gerekir. Aksi takdirde türemiş sınıf da soyut olacaktır. Ancak sanal fonksiyon saf yapılmazsa bu durumda o fonksiyonun default bir gerçekleştirimi olduğu için türemiş sınıflar bu fonksiyonu override etmek zorunda kalmayacaklardır. Bazen soyut sınıflar bir "kontrat" oluşlturmak için de kullanılabilmektedir. Örneğin biz bir kişinin bir sınıfı için foo, bar ve tar fonksiyonlarını muttlaka yazmasını istiyorsak bu durumda soyut bir sınıfta bu fonksiyonları saf sanal olarak bulundurabiliriz ve o kişiden bu sınıfı taban sınıf olarak kullanmasını isteyebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 76. Ders 05/06/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çokbiçimli mekanizamanın devreye girdiği ilginç bir durum vardır. A sınıfından B sınıfının türetildiğini düşünelim. A sınıfındaki sanal olmayan foo fonksiyonunun bar fonksiyonunu çağırdığını varsayalım. Eğer burada bu foo fonksiyonu B sınıfı türünden bir nesneyle çağrılırsa çağrılan bar fonksiyonu A sınfının değil B sınıfının bar fonksiyonu olacaktır. Örneğin: class A { public: void foo(); virtual void bar(); //... }; class B : public A { public: void bar() override; //... }; void A::foo() { bar(); } void A::bar() { cout << "A::bar" << endl; } void B::bar() { cout << "B::bar" << endl; } //... B b; b.foo(); Burada foo fonksiyonuna geçirilen this göstericisinin statik türü A, dinamik türü B'dir. foo fonksiyonunda bar fonksiyonun çağrılması aslında this->bar() biçiminde yapılmaktadır. bar fonksiyonu sanal olduğuna ve thsi göstericisinin dinamik türü B olduğuna göre B sınıfının bar fonksiyonu çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class A { public: void foo(); virtual void bar(); //... }; class B : public A { public: void bar() override; //... }; void A::foo() { bar(); } void A::bar() { cout << "A::bar" << endl; } void B::bar() { cout << "B::bar" << endl; } int main() { B b; b.foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıda bir komut satırı uygulamasının kaba kısmını yapan CommandPrompt soyut bir sınıf örneği verilmiştir. Komut satırı sınıfın run üye fonksiyonuyla oluşturulmaktadır. run üye fonksiyonu stdin dosyasından komutu alıp onu boşluklardan parse ederek sınıfın protected bölümündeki m_params isimli bir vector'ün içerisine yerleştirmektedir. Bu işlemi yaptıktan sonra run execute isimli saf sanal fonksiyonu çağırmaktadır. Bu saf sanal fonksiyon türemiş sınıfta override edilerek komut satırının isteğe bağlı ayrıntıları fonksiyonda gerçekleştirileblecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // commandprompt.hpp #ifndef COMMANDPROMPT_HPP_ #define COMMANDPROMPT_HPP_ #include #include class CommandPrompt { public: CommandPrompt() : m_prompt("CSD") {} CommandPrompt(const std::string &prompt) : m_prompt(prompt) {} void run(); protected: virtual bool execute() = 0; std::vector m_params; private: std::string m_prompt; const static int MAX_CMD_LINE = 4096; }; #endif // commandprompt.cpp #include #include #include "commandprompt.hpp" using namespace std; void CommandPrompt::run() { string cmdline; char buf[MAX_CMD_LINE]; char *str; for (;;) { cout << m_prompt << '>'; cin.getline(buf, 1024); m_params.clear(); for (str = strtok(buf, " \t"); str != nullptr; str = strtok(nullptr, " \t")) m_params.push_back(str); if (!execute()) break; } } // app.cpp #include #include "commandprompt.hpp" using namespace std; class MyCommandPrompt : public CommandPrompt { public: MyCommandPrompt(const string &prompt) : CommandPrompt(prompt) {} protected: bool execute() override; }; bool MyCommandPrompt::execute() { if (m_params[0] == "exit") return false; return true; } int main() { MyCommandPrompt mcp("CSD"); mcp.run(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yıkıcı fonksiyonlar da sanal (virtual) olarak bildirilebilirler. Bular da türemiş sınıflarda override edilebilirler. Ancak yapıcı fonksiyonların sanal olması söz konusu değildir. Örneğin: class A { //... virtual ~A(); }; class B : public A { //... ~B(); // override işlemi }; Her ne kadar taban ve türemiş sınıftaki yıkıcı fonksiyonların isimleri farklı gibiyse de istisna olarak burada override işlemi yapılmamkatdır. Pekiyi yıkıcı fonksiyonların sanal yapılmasının amacı nedir? B sınıfının A sınıfından türetildiğini düşünelim. A sınıfında bazı sanal fonksiyonlar B sınıfında override edilmiş olsun. Aşağıdaki duruma dikkat ediniz: A *pa = new B(); Burada new B() işlemi ile türemiş sınıf türünden dinamik bir nesne yaratılmış ve onun adresi taban sınıf türünden bir gsöetericiye yerleştirilmiştir. new B() işleminde B sınıfının yapıcı fonksiyonu çağrılacaktır. Tabii B sınıfının yapıcı fonksiyonu da A sınıfının yapıcı fonksiyonunu çalıştıracaktır. Şimbi biz bu nesneyi delete operatörü ile serbest bırakmak isteyelim: delete pa; Burada delete operatörünün operand'ı taban sınıf türünden olduğu için delete operatörü A sınıfının yıkıcı fonksiyonunu çağıracaktır. Halbuki bir dengenin sağlanması için B sınıfının yıkıcı fonksiyonun çağrılması gerekmektedir. İşte sanal yıkıcı fonksiyonlar bu işe yaramktadır. delete operatörü eğer operand'ı olan sınıftaki yıkıcı fonksiyon sanal ise operand'ı olan göstericinin dinamik türüne ilişkin sınıfın override edilmiş sanal fonksiyonu çağırmaktadır. Örneğin: class A { public: A(); virtual ~A(); //... }; class B : public A { public: B(); ~B(); //.... }; //... A *pa = new B(); delete pa; // B'nin yıkıcı fonksiyonu çağrılır Burada delete pa ifadesinde pa göstericisinin dinamik türü B olduğu için artık B sınıfın yıkıcı fonksiyonu çağrılacaktır. Tabii bu yıkıcı fonksiyon da A sınıfının yıkıcı fonksiyonunu çağıracaktır. override anahtar sözcüğü yıkıcı fonksiyonlarla da kullanılabilir. Örneğin: class A { public: A(); virtual ~A(); //... }; class B : public A { public: B(); ~B() override; // geçerli //.... }; Ancak taban sınıfın yıkıcı fonksiyonunun sanal olup olmadığının bilinmesine gerek duyulmadan kod oluşturmak için yıkıcı fonksiyonlarda bu override anahtar sözcüğünü kullanmayabilirsiniz. Daha önce yapmış olduğumuz Tetris örneğindeki nesne yaratma işlemine dikkat ediniz: Shape *shape; //... shape = get_random_shape(); //... delete shape; Burada get_random_shape fonksiyonu bize Shape sınıfından türetilmiş olan dinamik bir nesnenin adresini vermektedir. Ancak biz bu nesnenin dinamik türünü bilmemekteyiz. Bu gösterici ile delete işlemi yaptığımızda dinamik türe ilişkin sınıfın yıkıcı fonksiyonunun çalışabilmesi için Shape sınıfındaki yıkıcı fonksiyonun sanal olması gerekir. Tabii bizim basit örneğimizde bu sınıflarda yıkıcı fonksiyonlar yoktu. Bu durumda derleyici onları içi boş bir biçimde bizim için yazmıştı. Bu nedenle bu basit örneğimizde bunedenle yıkıcı fonksiyonun sanal yapılmaması bir soruna yol açmamaktadır. Ancak böylesi bir durumda yıkıcı fonksiyonların sanal olması gerekmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta Türemiş sınıf türünden new işlemi yapılıp bu alanın taban sınıf türünden bir gösterici ile delete edilmesi durumunda taban sınıftaki yıkıcı fonksiyon sanal değilse tanımsız davranış olarak değerlendirilmektedir. Bazı durumlarda (basit Tetris örneğimizde olduğu gibi) bu tanımsız davranış bir soruna yol yol açmayabilir. Ancak pek çok durumda da ciddi sorunlara yol açabilmektedir. Örneğin: class A { public: A(); virtual ~A(); //... }; class B : public A { public: B(const string &s) : m_s(s) {} ~B(); private: string m_s; }; //... A *pa = new B("ankara"); //... delete pa; Burada B sınıfı yerine A sınıfının yıkıcı fonksiyonunun çağrılması B sınıfının m_s string veri elemanı için yıkıcı fonksiyonun çağrılamamasına yol açacaktır. Bu da en azından bir bellek sızıntısı oluşturacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi sınıfımız için yıkıcı fonksiyonu ne zaman sanal yapmalyız? Kabaca "eğer sınıfımızda en az bir sanal fonksiyon varsa" sınıfımızdaki yıkıcı fonksiyonun da sanal yapılması uygun bir tekniktir. Çünkü bu sınıfı kullanacak kişiler yukarıdaki gibi çokbiçimli işlemleri yapabilirler. Bizim de başkaları tarafından yazılmış olan sınıflardaki yıkıcı fonksiyonların sanal olup olmadığına dikkat etmemiz gerekebilir. Eğer taban sınıfı yazanlar yıkıcı fonksiyonu sanal yapmamışlarsa biz türemiş sınıf türünden dinamik biçimde tahsis ettiğimiz nesneleri yine türemiş sınıf türünden adreslerle serbest bırakmalıyız. Tabii bunun için dinamik tahsis etmiş olduğumuz nesnenin türünü bilmemiz gerekir. Sınıfımız için yıkıcı fonksiyonu hiç yazmazsak derleyicinin yazdığı yıkıcı fonksiyonun sanal olmadığına dikkat ediniz. Ancak sanal yıkıcı fonksiyon defaulted hale de getirilebilmektedir. Örneğin: A { //; virtual ~A() = dafult; }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Global bir fonksiyon ya da başka bir sınıfın üye fonksiyonu bir sınıfın arkadaş fonksiyonu yapılabilir. Bu durumda arkadaş yapılan fonksiyon özel bir erişim ayrıcalığına sahip olur. Arkadaş fonksiyonlar içeriisinde (parametre değişkenleri de dahil olmak üzere) arkadaş olunan sınıf türünden bir nesne, gösterici ya da referans yoluyla o sınıfın tüm bölümlerine erişebiliriz. friend bildirimi sınıfın herhangi bir bölümünde yapılabilir. Hangi bölümünde yapıldığının bir önemi yoktur. Global bir fonksiyon friend yapıldığında friend bildirimi için o global fonksiyonun prototipinin ya da tanımlamasının daha önceden görülmüş olması gerekmez. Ancak friend bildirimi dışarısı için bir prototip bildirimi olarak kullanılamaz. Örneğin: class Sample { //... friend void foo(); private: int m_a; int m_b; }; //... void foo() { Sample s; s.m_a = 10; // geçerli foo fonksiyonun Sample sınıfına bir erişim yarıcalığı var, Sample sınıfının her bölümüne erişebilir. s.m_b = 20; // geçerli foo fonksiyonun Sample sınıfına bir erişim yarıcalığı var, Sample sınıfının her bölümüne erişebilir. //... } Burada Smaple sınıfı içerisindeki foo fonksiyonu henüz prototipi ya da tanımalaması görülmeden friedn yapılmıştır. foo fonksiyonunun friend yapılması onun Sample sınıfının her bölümüne nesne, gösterici ya da referans yoluyla erişebilmesini sağlamaktadır. Sınıf hangi isim alanında bildirilmişse friend fonksiyon da o isim alanında aranmaktadır. Örneğin: namespace CSD { class Sample { //... friend void foo(); private: int m_a; int m_b; }; //... } void foo() { Sample s; s.m_a = 10; // geçersiz! friend olan foo bu foo değil! s.m_b = 20; // geçersiz! friend olan foo bu foo değil //... } Burada Sample sınıfında friend yapılan foo CSD isim alanındaki foo fonksiyonudur. Çünkü Sample sınıfı CSD isim alanında bildirilmiştir. Tabii başka bir isim alanındaki (global isim alanı da dahil olmak üzere) bir fonksiyon da friend fonksiyon yapılabilir. Ancak bu durumda fonksiyonun bildiriminin ya da tanımlamasının daha önce görülmüş olması gerekir. Örneğin: namespace Mample { //... } namespace CSD { class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend void ::foo(); // geçersiz! bu noktaya kadar global isim alanında foo fonksiyonun görülmesi gerekirdi friend void Mample::foo(); // geçersiz! bu noktaya kadar Mample isim alanında foo fonksiyonun görülmesi gerekirdi private: int m_a; int m_b; }; } Buradaki friend fonksiyonlar nitelikli bir biçimde bildirilmiş olduğu için friend bildirimine kadar onların bildiriminin görülmesi gerekmektedir. Ancak friend bildirimi dışarısı için bir prototip bildirimi yerine geçmemektedir. Örneğin: class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend void foo(); private: int m_a; int m_b; }; int main() { foo(); // geçersiz! foo fonksiyonunun prototipi ya da tanımlaması görülmedi return 0; } void foo() { Sample s(10, 20); cout << s.m_a << ", " << s.m_b << endl; } Burada main içerisinde foo fonksiyonunun çağrılması geçersizdir. Çünkü henüz derleyici foo fonksiyonunun prototiğini görmemiştir. Ancak main fonksiyonunun yukarısana foo için bir prototip yerleştirilirse sorun giderilecektir. friend bir fonksiyonun tanımlaması sınıf içerisinde yapılabilir. Bu fonksiyon sınıfın içinde bulunduğu isim alanınındaki global bir fonksiyon olarak değerlendirilmektedir. Ancak yine bu tanımalama da dışarısı için etkili olmamaktadır. Örneğin: class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend void foo() // foo global isim alanında inline bir fonksiyon olarak tanımlanmaktadır { Sample s(10, 20); cout << s.m_a << ", " << s.m_b << endl; } private: int m_a; int m_b; }; int main() { foo(); // geçersiz! foo fonksiyonunun prototipi ya da tanımlaması görülmedi return 0; } Burada Sample sınıfının içerisinde tanımlanan foo aslında global isim alanındaki bir foo fonksiyonudur. Ancak yine de main içerisinde foo fonksiyonun kullanılması için bir prototip bildiriminin yaılmış olması gerekmektedir. Yukarıdaki örnekte main fonksiyonunun yukarısına foo fonksiyonunuın prototiği yerleştirilirse sorun ortadan kalkacaktır. Tanımlaması sınıf bildiriminin içerisine yapılmış olan friend fonksiyonlar normal üte fonksiyonlarda olduğu gibi inline kabul edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Başka bir sınıfın üye fonksiyonu da arkadaş fonksiyon olabilir. Atbii bu duurmda niteliklendirmenin yapılması gerekir. Örneğin: class Mample { public: void foo(); }; class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend void Mample::foo(); // void Mample::foo() fonksiyonu arkadaş fonksiyon private: int m_a; int m_b; }; void Mample::foo() { Sample s(10, 20); cout << s.m_a << ", " << s.m_b << endl; // geçerli } int main() { Mample m; m.foo(); return 0; } Burada Sample sınıfı içerisinde Mample sınıfının foo fonksiyonu arkadaş fonksiyon yapılmıştır. Sınıf ismi belirtilirken niteliklendirme yapıldığı için ilgili sınıfın üye fonksiyon bildiriminin dah ayukarıda görülmesi gerekmektedir. Bu durumda eksik bildirim (incomplete declarations) uygulanamaz. Örnepşn: class Mample; class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend void Mample::foo(); // geçersiz! private: int m_a; int m_b; }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın tamamı da arkadaş sınıf yapılabilir. Arkadaş sınıf demek kabaca o sınıfın tüm üye fonksiyonlarının arkadaş fonksiyon olması demektir. Örneğin. class Mample { public: void foo(); void bar(); //... }; class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend Mample; private: int m_a; int m_b; }; Burada Mample sınıfı Sample sınıfının arkadaş sınıfıdır. Yani Mample sınıfının foo ve bar üye fonksiyonları Sample sınıfının her bölümüne erişebilir. Arkadaş sınıf bildiriminde arkadaş yapılan sınıf yukarıda eksik (incomplete) biçimde bildirilebilir. Sınıfın asıl bildirimi daha sonra da yapılabilir. Örneğin: class Mample; class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend Mample; private: int m_a; int m_b; }; Mample *pm; // geçerli eksik bildirim ile gösterici ya da referans tanımlanabilir class Mample { public: void foo(); void bar(); //... }; Burada Mample sınıfı için yukarıda eksik bildirim yapılmıştır. Sınıf daha sonra bildirilebilir. Aslında yukarıya eksik bildirim yapmak yerine friend anahtar sözcüğünün yanına class anahtar sözcüğü de eklenebilirdi. Örneğin: class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend class Mample; private: int m_a; int m_b; }; class Mample { public: void foo(); void bar(); //... }; Tabii bu durumda Mample sınıfının Sample sınıfı içerisinde arkadaş olarak bildirilmiş olması dışarısı için bir eksik bildirim (incomplete declaration) oluşturmamaktadır. Örneğin. class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend class Mample; private: int m_a; int m_b; }; Mample *pm; // geçersiz! Smaple sınıfındaki bildirim burada etkili olmamaktadır class Mample { public: void foo(); void bar(); //... }; Bir sınıfın başka bir sınıfın arkadaşı olmasının onun bütün fonksiyonlarının o sınıfın arkadaşı olması gibi bir anlama geldiğini söylemiştik. Ancak arkadaş olunan sınıfa erişim ayrıcalığı yalnızca üye fonksiyonlar için değil sınıf bildirimi için de söz konusudur. Örneğin: class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} //... friend class Mample; private: enum Color { Red, Green, Blue}; int m_a; int m_b; }; class Mample { public: void foo() {} void bar() {} Sample::Color m_color; // geçerli }; Burada Mample sınıfının bildirimindeki Sample::Color türü Sample sınıfının private bölümünde bildirilmiştir. Eğer Mample arkadaş sınıf olmasaydı bu türü kullanamazdı. Yani arkadaşlık aslında sınıfın gövdesini de kapsamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir sınıfın arkadaş fonksiyonu o sınıfın taban sınıfının da her bölümüne erişebilir mi? Taban sınıfın public ve protected bölümleri türetme biçimine göre türemiş sınıfın public, protected ve private bölümleri gibi darandığına göre arkadaş fonksiyon arkadaş olunan sınıfın taban sınıflarının public ve protected bölümlerine erişebilir. Ancak taban sınııfn private bölümü hiçbir zaman türemiş sınıf tarafından kalıtım yoluyla alınmamaktadır (inherit edilmemektedir). Bu nedenle arkadaş fonksiyon taban sınıfın private bölümüne erişemez. Örneğin: class Base { public: int m_x; protected: int m_y; private: int m_z; }; class Sample : public Base { public: Sample(int a, int b) : m_a(a), m_b(b) {} friend void foo(); private: int m_a; int m_b; }; void foo() { Sample s(10, 20); cout << s.m_a << ", " << s.m_b << endl; // geçerli cout << s.m_x << endl; // geçerli cout << s.m_y << endl; // geçerli cout << s.m_z << endl; // geçersiz! } Bu örnekte taban sınıfın public nölümü türemiş sınıfın public bölümüymüş gibi, taban sınıfın protected bölümü türemiş sınıfın protected bölümüymüş gibi işlem görmektedir. Taban sınıfın private bölümü ise tamamen erişime kapalıdır. Arkadaş foo fonksiyonu Sample sınıfının her bölümüne erişebildiğine göre Base sınıfının public ve protected bölümlerine de erişebilir. Tabii burada türetme biçimi protected ya da private olsaydı da bir şey değişmeyecekti. (Ancak bir dizi türetme yapıldığında durum farklılaşabilecektir.) Türemiş bir sınıf arkadaş sınıf yapıldığında bu durum onun taban sınıflarının arkadaş yapıldığı anlamına gelmemektedir. Benzer biçimde taban sınıf arakadaş yapıldığında bu durum türemiş sınıfların arkadaş yapıldığı anlamına gelmemektedir. (Bir benzetmeyle durumu şöyle açıklayabiliriz. Biz birisine vekalet verdiğimizde onun babası bu vekalate sahipmiş gibi davranamaz. Benzer biçimde babaya vekalet verdiğimizde onun çocuğu da vekalete sahipmiş gibi davranamaz.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 78. Ders 12/06/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi C++'ta arkdaşlık gerçekten gerekli midir? Arkadaş fonksiyonların ve sınıfların arkadaş olunan sınıfın private bölümüne erişimi kapsülleme ve "veri elemanlarının gizlenmesi" prensiplerine aykırıdır. Örneğin sınıfın private elemanları değiştirilirse yalnızca sınıfın üye fonksiyonlarının içini değil arkadaş fonksiyonların içini de yeniden düzenlemek gerekir. İşte arkadaşlık mutlak anlamda gerekli değildir. Örneğin Java ve C# gibi dillerde böyle bir özellik yoktur. Ancak bazı durumlarda arkadaşlık yazım kolaylığı ve pratiklik sunmaktadır. Bu özelliğin kısıtlı biçimde gerektiğinde kullanılması tavsiye edilebilir. Örneğin bir bağlı liste sınıfı yazacak olalım. Bu sınıfta bağlı listenin düğümleri başka bir sınıfla temsil ediliyor olsun: class Node { //... }; class LinkedList { //... }; Burada LinkedList sınıfının üye fonksiyonları Node sınıfını kullanmaktadır. Bu durumda LinkedList sınıfı Node sınıfının arkadaş sınıfı yapılırsa LinkedList sınıfının üye fonksiyonları Node sınıfının her bölümüne erişebilir. Örneğin: class Node { public: Node(int val) : m_val(val) {} friend class LinkedList; private: int m_val; Node *m_next; Node *m_prev; }; class LinkedList { public: LinkedList() : m_head(nullptr), m_tail(nullptr), m_count(0) {} ~LinkedList(); void add(int val); void walk() const; //... private: Node *m_head; Node *m_tail; std::size_t m_count; }; Burada Node sınıfı dış dünyadan gizlenmemiştir. Ancak LinkedList sınıfına bir erişim ayrıcalığı verilmiştir. Tabii alternatif olarak Node sınıfı LinkedList sınfının private bölümünde bulundurulabilirdi ve Node sinıfının tüm elemanları public de yapılabilirdi. Aşağıda örneğin daha somut bir biçimi verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // linkedlist.hpp #ifndef LINKEDLIST_HPP_ #define LINKEDLIST_HPP_ #include namespace CSD { class Node { public: Node(int val) : m_val(val) {} friend class LinkedList; private: int m_val; Node *m_next; Node *m_prev; }; class LinkedList { public: LinkedList() : m_head(nullptr), m_tail(nullptr), m_count(0) {} ~LinkedList(); void add(int val); void walk() const; std::size_t count() { return m_count; } //... private: Node *m_head; Node *m_tail; std::size_t m_count; }; } #endif // linkedlist.cpp #include #include "linkedlist.hpp" using namespace std; namespace CSD { void LinkedList::add(int val) { Node *new_node = new Node(val); if (m_tail != nullptr) m_tail->m_next = new_node; else m_head = new_node; new_node->m_prev = m_tail; new_node->m_next = nullptr; m_tail = new_node; ++m_count; } void LinkedList::walk() const { Node *node; for (node = m_head; node != nullptr; node = node->m_next) cout << node->m_val << ' '; cout << endl; } LinkedList::~LinkedList() { Node *node, *temp_node; node = m_head; while (node != nullptr) { temp_node = node->m_next; delete node; node = temp_node; } } } // app.cpp #include #include "linkedlist.hpp" using namespace std; using namespace CSD; int main() { LinkedList ll; for (int i = 0; i < 100; ++i) ll.add(i); ll.walk(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Arkadaş fonksiyonlar özellikle operatör fonksiyonları konusunda sıkça kullanılmaktadır. İzleyen paragraflarda operatör fonksiyonları konusu ele alınmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Operatör fonksiyonları (operator overloading) C++'ın yanı sıra C#, Swift, Python gibi dillerde de olan bir özelliktir. Operatör fonksiyonları sayesinde sınıf nesneleri sanki temel türlerden nesnelermiş gibi +, -, * gibi operatörlerle işleme sokulabilmektedir. Operatör fonksiyonları aslında dile ilave bir işlevsel katmaz. Yalnızca okunabilirlik sağlamaktadır. Örneğin Java'da opretaor fonksiyonları (metotları) yoktur. Sınıflarla ilgili işlemler normal fonksiyon çağırma sentaksıyla yapılmaktadır. C++'ta operatör fonksiyonları oldukça ayrıntılı bir konudur. Pek çok nesne yönelimli programlama dilince bu özellik C++'a kıyasla daha basit ve ayrıntısız biçimde tasarlanmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Operatör fonksiyonlarının ne işe yaradığını basit bir örnekle açıklayalım. Bir Complex sayı sınıfı yazmak istediğimizi düşünelim. Aynı zamanda bu sınıf iki Complex sayı üzerinde toplama, çıkartma, çarpma, bölme gibi işlemleri yapabiliyor olsun. Pekiyi bu işlemleri bu sınıfa nasıl yaptırabiliriz? Şüphesiz en doğal yol sınıfa bu işlemleri yapabilecek üye fonksiyonlar yerleştirmektir. Yerleştirilen bu üye fonksiyonlar statik yapılabilir ya da yapılmayabilir. Örneğin: class Complex { public: Complex() = default; Complex(double real, double imag = 0) : m_real(real), m_imag(imag) {} void disp() const; Complex add(const Complex &z) const; //... private: double m_real; double m_imag; }; Buradaki static olmayan add üye fonksiyonu iki Complex sayıyı toplayıp sonucu bir Complex nesnesi biçiminde vermektedir. Bu fonksiyon aşağıdaki gibi kullanılabilir: Complex x{3, 2}, y{4, 3}, result; result = x.add(y); Burada x nesnesiin adresi add üye fonksiyonuna this göstericisi olarak aktarılacaktır. y nesnesi de z referansına yine adres yoluyla aktarılacaktır. Yani add içerisinde doğrudan kullanılan m_real ve m_imag aslında x nesnesinin elemanları, z ile kullanılan m_real ve m_imag ise y nesnesinin elemanlarıdır. Üye fonksiyon şöyle yazılabilir: Complex Complex::add(const Complex &z) const { Complex result; result.m_real = m_real + z.m_real; result.m_imag = m_imag + z.m_imag; return result; } Burada bir sorun yoktur. Ancak yukarıdaki işlem aşağıdaki gibi ifade edilseydi daha sade ve daha doğal bir görünüm oluşurdu: result = x + y; İşte operatör fonksiyonları operatör işlemlerinin bu biçimde doğal olarak yazılmasını sağlamaktadır. Yukarıda da belirttiğimiz biz işlemi daha doğal olarak x + y biçiminde yazsak da aslında izleyen paragraflarda göreceğiniz gibi arka planda operatör fonksiyonu denilen bir fonksiyon çağrılmaktadır. Operatör fonksiyonları yalnızca okunabilirlik ve algı bakımından bir fayda sağlamaktadır. Pekiyi biz yukarıdaki örnekte bir Complex sayı ile bir double sayıyı toplamak istesek ne yapabiliriz? C++'ta farklı farklı parametrik yapılara ilişkin aynı isimli fonksiyonlar bulnabileceğine göre add fonksiyonunu aşağıdaki gibi overload edebiliriz: class Complex { public: Complex() = default; Complex(double real, double imag = 0) : m_real(real), m_imag(imag) {} void disp() const; Complex add(const Complex &z) const; Complex add(double real) const; private: double m_real; double m_imag; }; Şimdi artık aşağıdaki gibi bir işlemi de yapabiliriz: Complex x{3, 2}, y{4, 3}, result; result = x.add(3); Tabii burada add fonksiyonlarını static bir fonksiyon olarak da yazılabilirdik: class Complex { public: Complex() = default; Complex(double real, double imag = 0) : m_real(real), m_imag(imag) {} void disp() const; static Complex add(const Complex &x, const Complex &y); static Complex add(const Complex &x, double real); private: double m_real; double m_imag; }; Tabii bu durumda static iye fonksiyonlar sınıf ismi belirtilerek çağrılmalıdır: Complex x{3, 2}, y{4, 3}, result; result = Complex::add(x, y); result.disp(); Buradaki static fonksiyonların yazımına dikkat ediniz: Complex Complex::add(const Complex &x, const Complex &y) { Complex result; result.m_real = x.m_real + y.m_real; result.m_imag = x.m_imag+ y.m_imag; return result; } Complex Complex::add(const Complex &x, double real) { Complex result; result.m_real = x.m_real + real; result.m_imag = x.m_imag; return result; } static fonksiyonlar sınıfın üye fonksiyonları olduğu için onların içerisinde o sınıf türünden nesne, gösterici ya da referans yoluyla sınıfın her bölümüne erişebildiğini anımsayınız. Pekiyi şimdi de toplama işini yapan add fonksiyonunu static değil global bir fonksiyon olarak yazmayı deneyelim. Global fonksiyonların sınıfın public bölümü dışındaki elemanlarına erişemediğini anımsayınız. Bu durumda sınıfın m_real ve m_imag veri elemanları için ya getter fonksiyonlar yazılmalı ya da bu fonksiyonlar arkadaş fonksiyon yapılmalıdır. Örneğin. class Complex { public: Complex() = default; Complex(double real, double imag = 0) : m_real(real), m_imag(imag) {} void disp() const; friend Complex add(const Complex &x, const Complex &y); friend Complex add(const Complex &x, double real); private: double m_real; double m_imag; } //... Complex add(const Complex &x, const Complex &y) { Complex result; result.m_real = x.m_real + y.m_real; result.m_imag = x.m_imag+ y.m_imag; return result; } Complex add(const Complex &x, double real) { Complex result; result.m_real = x.m_real + real; result.m_imag = x.m_imag; return result; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta operatör fonksiyonları iki biçimde tanımlanabilmektedir: 1) Üye fonksiyon biçiminde (bunlara "üye operatör fonksiyonları" da denilmektedir) 2) Global fonksiyonlar biçiminde (bunlara "global operatör fonksiyonları" da denilmektedir) Global operatör fonksiyonları herhangi bir isim alanında bulunabilir. Normal isim arama kuralları operatör fonksiyonları için de geçerlidir. C++'ta operatör fonksiyonları sınıfın static üye fonksiyonları biçiminde olamaz. Biz kursumuzda önce üye fonksiyonları üzerinde ilerleyeceğiz sonra global operatör fonksiyonlarını da ele almaya başlayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Operatör fonksiyonlarının aslında diğer fonksiyonlardan önemli bir farkı yoktur. Ancak bunların isimleri özel biçimde belirtilmektedir. Operatör fonksiyonlarının isimleri "operator" anahtar sözcüğü ile operatör sembolünden oluşmaktadır. operator anahtar sözcüğü ile operator sembolü bitişik yazılabilir ancak prgramcılar genellikle operator anahtar sözcüğü ile operatör sembolü arasında bir boşluk (SPACE) bulundurmaktadır. Örneğin: operator + operator > operator ++ operator ! ..... Operatör fonksiyonlarının paramerte sayılarında da bir kısıt vardır. Şöyle ki: 1) Eğer operatör fonksiyonu iki operand'lı bir operatöre ilişkinse bu fonksiyon üye operatör fonksiyonu olarak yazılırken bir parametreye global operatör fonksiyonu olarak yazılırken iki parametreye sahip olmak zorundadır. 2) Eğer operatör fonksiyonu tek operand'lı bir operatöre ilişkinse bu fonksiyon üye operatör fonksiyonu olarak yazılırken sıfır parametreye global operatör fonksiyonu olarak yazılırken bir parametreye sahip olmak zorundadır. Örneğin / operatörü için operatör fonksiyonu yazmak isteyelim. Eğer biz bu operatör fonksiyonunu üye operatör fonksiyonu olarak yazacaksak fonksiyonun bir parametreye global operatör fonksiyonu olarak yazacaksak iki parametreye sahip olması gerekir. Ya da örneğin ! operatör için operatör fonksiyonu yazmak isteyelim. Eğer operatör fonksiyonunu üye fonksiyonu olarak yazcaksak fonksiyonun sıfır parametreye global operatör fonksiyonu olarak yazacaksak bir parametreye sahip olması gerekmektedir. Operatör fonksiyonlarının geri dönüş değerlerinin türleri üzerinde özel bir kısıt yoktur. Ancak izleyen paragraflarda da görüleceği üzere bunların geri dönüş değerleri işlevleriyle uyumlu olmalıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi daha önce yapmış olduğumuz Complex sınıfı örneğindeki add fonksiyonlarını üye operatör fonksiyonlarıyla yer değiştirelim: class Complex { public: Complex() = default; Complex(double real, double imag = 0) : m_real(real), m_imag(imag) {} void disp() const; Complex operator +(const Complex &z) const; Complex operator +(double real) const; private: double m_real; double m_imag; }; //... Complex Complex::operator +(const Complex &z) const { Complex result; result.m_real = m_real + z.m_real; result.m_imag = m_imag + z.m_imag; return result; } Complex Complex::operator +(double real) const { Complex result; result.m_real = m_real + real; result.m_imag = m_imag; return result; } Aslında bu örnek ile konuya girişte yaptığımız örnek arasındaki tek farklılık fonksiyonların isimlerindedir. Ancak operatör fonksiyonları bizim artık bu toplama işlemini doğal bir gösterimle yapmamıza olanak sağlamaktadır. Örneğin: Complex x{3, 2}, y{4, 3}, result; result = x + y; // result = x.operator +(y) result.disp(); result = x + 3; // result = x.operator +(3) result.disp(); Burada result = x + y ifadesinin eşdeğeri aslında result = x.operator +(y) biçimindedir. Benzer biçimde result = x + 3 ifadesinin eşdeğeri de result = x.operator +(3) biçimindedir. Biz bu işlemleri hem operator senktasıyla hem de fonksiyon çağırma sentaksıyla da yapabiliriz. Yani aşağıdaki koda yukarıdakiyle eşdeğerdir ve geçerlidir: Complex x{3, 2}, y{4, 3}, result; result = x.operator +(y) result.disp(); result = x.operator +(3) result.disp(); Tabii operatör fonksiyonlarından amaçladığımız şey aslında operatörlerle doğal bir gösterim oluşturmaktır. Dolayısıyla bizim özel bir gerekçemiz yoksa (bazen olabilir) operatör sentaksını tercih etmemiz gerekir. Önceki örnekteki operatör fonksiyonlarını şimdi global operatör fonksiyonu olarak yazalım: class Complex { public: Complex() = default; Complex(double real, double imag = 0) : m_real(real), m_imag(imag) {} void disp() const; friend Complex operator +(const Complex &r, const Complex &y); friend Complex operator +(const Complex &r, double real); private: double m_real; double m_imag; }; //... void Complex::disp() const { cout << m_real << '+' << m_imag << 'i' << endl; } Complex operator +(const Complex &x, const Complex &y) { Complex result; result.m_real = x.m_real + y.m_real; result.m_imag = x.m_imag + y.m_imag; return result; } Complex operator +(const Complex &x, double real) { Complex result; result.m_real = x.m_real + real; result.m_imag = x.m_imag; return result; } Kullanımda değişen bir şey omayacaktır: Complex x{3, 2}, y{4, 3}, result; result = x + y; // result = operator +(x, y) result.disp(); result = x + 3; // result = operator +(3) result.disp(); Yine biz operatör sentaksı yerine fonskiyon çağırma semtaksını da kullanabilirdi: Complex x{3, 2}, y{4, 3}, result; result = operator +(x, y) result.disp(); result = operator +(3) result.disp(); Operatör fonksiyonlarının operatör sentaksıyla da fonksiyon çağırma sentaksıyla da kullanılabildiğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ derleyicisi bir operatörle karşılaştığında önce operand'ların türlerine bakar. Eğer operand'lar temel türlere ilişkinse işlem öncesi otomatik tür dönüştürmesi yoluyla işlemi yapar. Yani işlemler C'de olduğu gibi yütürülür. Ancak operand'lardan en az bir tanesi bir sınıf türündense derleyici bu işlemi yapabilecek üye operatör fonksiyonları ve global operatör fonksiyonları araştırır. Örneğin a @ b biçiminde (burada @ herhangi bir operatörü temsil ediyor olsun) bir operatör işleminin yapıldığını düşünelim. a değişkeni bir sınıf türünden olsun. O halde bu işlem A sınıfındaki "operator @" isimli üye fonksiyon tarafından da global düzeydeki "operator @" isimli bir fonksiyon tarafından da yapılabilir. Yani bu işlemin iki fonksiyon çağrı eşdeğeri olabilir: a.opetor @(b) operator @(a, b) İşte derleyici a @ b işleminde a ifadesinin ilişkin olduğu sınıfta (burada A sınıfı) ve global düzeyde "operator @" fonksiyonlarını araştırıp bunların en uygun olanını bulmaya çalışmaktadır. Daha teknik bir açıklamayı şöye yapabiliriz: 1) a @ b gibi bir işlem için derleyici a operand'ının ilişkin olduğu sınıfta "operator @" isimli üye opreatör fonksiyonlarını ve global düzeyde operator @ isimli global fonksiyonları aday fonksiyonlar olarak belirler. (Yani üye operatör fonksiyonları için çağrının a.operator @(b) biçiminde, global operatör fonksiyonları için ise çağrının operator(a, b) biçiminde yapılmış olduğunu varsayar.) Aday fonksiyonları overload resolution işlemine sokarak en uygun fonksiyonu bulmaya çalışır. Eğer en uygun fonksiyon bulunamazsa ya da birden fazla olacak biçimde bulunursa bu durum error oluşturacaktır. Burada üye fonksiyonları ile global fonksiyonlarının farklı parametre sayısına sahip olduğu halde birlikte overload resolution işlemine sokulması size tuhaf gelebilir. Ancak standartlara göre bu süreçte üye fonksiyonlar onların çağrılmasında kullanılanılan nesnenin adresinin this göstericisi ile eşleştirildiği kabul edilerek global fonksiyonlar gibi overload resolution işlemine sokulmaktadır. Örneğin: a.operator @(b) işlemi overload resolution sırasında sanki aşağıdaki ile eşdeğer kabul edilmektedir: operator @(&a, b) Bu fonksiyonun da birinci parametresinin this göstericisi olduğu kabul edilmektedir. 2) @a ya da a@ gibi bir işlem için derleyici a operand'ının ilişkin olduğu sınıfta operator @ ve global operator @ üye fonksiyonlarını aday fonksiyon olarak belirler. (Yani üye operatör fonksiyonları için çağrının a.operator @(), global operatör fonksiyonları için de çağrının operator @(a) biçiminde yapılmış olduğunu varsayar.) Yine derleyici bu aday operatör fonksiyonlarını yukarıda belirttiğimiz gibi overload resolution işlemine sokar. Eğer en uygun fonksiyonu bulamazsa ya da birden fazla olarak bulursa bu durum error oluşturur. Örneğin: Complex x{3, 2}, y{4, 3}, result; result = x + y; Burada ifade derleyici tarafından adeta iki biçimde yazılmış gibi ele alınmaktadır: result = x.operator +(y) result = operator +(x, y) Bu çağrılara uygun Complex sınıfındaki operator + üye fonksiyonları ve global operator + fonksiyonları arasında en uygun fonksiyon belirlenmeye çalışılacaktır. Buradan çıkan bir sonuç şudur: Bir operatör işlemini aynı kalitede yapabilecek hem üye operatör fonksiyonu hem de global operatör fonksiyonu varsa bu operatörün kullanımı (bulunması değil) error oluşturacaktır. Bu nedenle programcının bir operatör işlemini ya üye operatör fonksiyonuna ya da global operatör fonksiyonuna yaptırması gerekir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta pek çok operatöre ilişkin operatör fonksiyonları yazılabilmektedir. Ancak istisna olarak aşağıdaki operatörlere ilişkin operatör fonksiyonları yazılamamaktadır: . :: ?: .* --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 79. Ders 24/06/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Operatör fonksiyonları yoluyla operatör öncelikleri değiştirilememektedir. Örneğin: Complex x, y, z, result; //... result = x + y * z; Burada temel türler içim operatör öncelikleri nasılsa operatör fonksiyonları da o önceliklere göre çağrılacaktır. Yani yukarıdaki işlemin üye operatör fonksiyonu eşdeğeri şöyledir: result = x.operator +(y.operator *(z)); Tabii aslında atama işlemi de bir operatör fonksiyonuyla yapılmaktadır. O halde yukarıdaki işlemin gerçek eşdeğeri aşağıdaki gibidir: result.operator = (x.operator +(y.operator *(z))) Tabii aynı işlemler global operatör fonksiyonlarıyla da yapılabilirdi: result = operator +(x, operator * (y, z)); Tabii operatörler arasındaki "soldan sağalık ya da sağdan solalık (associativity)" durumu da değiştirilemez. Örneğin: Complex x, y, z, result; //.. result = x + y + z; Burada önce x + y toplamı yapılacak sonra bu toplam z ile toplanacaktır. Yani bunun üye operatör fonksiyonu eşdeğeri şöyledir: result = x.operator +(y).operator +(z); Tabii bu tür işlemler yapılırken ara işlemlerde geçici nesneler yaratılmaktadır. Dolayısıyla bu geçici nesneler için sınıfın yıkıcı fonksiyonları da çalıştırılacaktır. Daha önceden de çeşitli defalar belirttiğimiz gibi C++'ta her zaman yapıcı fonksiyonlarla yıkıcı fonksiyonlar ters sırada çağrılmaktadır. Dolayısıyla alt işlemler sırasında yaratılan geçici nesneler için de yıkıcı fonksiyonlar ters sırada çağrılır. Örneğin: result = x + y + z; İşleminde iki geçici nesne yaratılmaktadır. Birincisi x + y işleminin sonucunda yaratılan geçici nesnedir. İkincisi de bunun z ile toplanması sonucunda yaratılan geçici nesnedir. C++'ta bir ifade de bir geçici nesne yaratılmış ise o nesneye ilişkin yıkıcı fonksiyonlar tüm ifade (burada atama işlemi de dahildir) bittiğinde çağrılır. Tabii burada yıkıcı fonksiyonların çağrılması yapıcı fonksiyonlara göre ters sırada gerçekleşecekir. Örneğin: result = x + y + z; Burada üç işlem (operatör) söz konusudur: İ1: x + y ------> geçici nesne İ2: İ1 (geçici nesne) + z ------> geçici nesne İ3: result = İ2 Önce İ2'deki geçici nesne için sonra İ1'deki geçici nesne için yıkıcı fonksiyonlar çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta aşağıdaki operatörlere ilişkin operatör fonksiyonları global operatör fonksiyonu biçiminde yazılamamaktadır: = () [] -> Bu operatörlere ilişkin operatör fonksiyonlarının üye operatör fonksiyonları biçiminde yazılması gerekmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Operatör fonksiyonları anlamlı ve herkes tarafından tahmin edilebilecek bir işleve sahip olacaksa bulundurulmalıdır. Operatör fonksiyonlarına o operatörün sembolü ile mantıksal biçimde ilişkili olmayan işlemlerin yaptırılması kötü bir tekniktir. Örneğin tarih bilgileri üzerinde işlemler yapan Date sınıfı için çıkartma operatör fonksiyonunun yazılması anlamlı olabilir. Genellikle beklenti iki tarihin çıkartılmasından aradaki gün farkının elde edilmesi biçimindedir. Ancak iki tarihin toplanmasının ya da çarpılmasının bir anlamı yoktur. Ancak Date sınıfı için >, <, >=, <=, == ve != operatör fonksiyonlarının bulundurulması anlamlıdır. Programcının eğer anlamlıysa o gruptaki tüm operatörleri yazması iyi bir tekniktir. Örneğin bir sınıf için + operatör fonksiyonunu yazılıyorsa eğer anlamlıysa -, * ve / operatör fonksiyonlarını da yazılmalıdır. Benzer biçimde sınıf için == operatör fonksiyonu yazılıyorsa != operatör fonksiyonu da ve eğer anlamlıysa >, < , >=, <= operatör fonksiyonları da yazılmalıdır. Ya da örneğin sınıf için ++ operatör fonksiyonu yazılıyorsa eğer anlamlıysa -- opreatör fonksiyonu da yazılmalıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Operatör fonksiyonlarının geri dönüş değerleri üzerinde herhangi bir kısıt yoktur. Örneğin anlamsız olsa da + operatör fonksiyonun geri dönüş değeri void olabilir. Global operatör fonksiyonlarının en az bir parametresinin bir sınıf türünden olması gerekmektedir. Yani örneğin biz iki int değeri toplayan bir operatör fonksiyonu yazamayız. Ancak bir sınıf nesnesi ile bir int değeri toplayan bir operatör fonksiyonu yazabiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda belirttiğimiz birkaç istisna operatör dışında operatör fonksiyonarının çoğu hem üye operatör fonksiyonu olarak hem de global operatör fonksiyonu olarak yazılabilmektedir. Pekiyi biz hangisini tercih etmeliyiz? Pek çokları gibi biz de şunu tavsiye edeceğiz: "Eğer operatör fonksiyonunu üye operatör fonksiyonu olarak yazabiliyorsanız üye operatör fonksiyonu olarak yazın. Üye operatör fonksiyonu olarak yazamıyorsanız mecburen onu global operatör fonksiyonu" olarak yazacaksınız. Örneğin z değişkeni Complex türünden olsun. Biz de bir Complex sayı ile bir double sayıyı toplayabilecek bir operatör fonksiyonu yazmak isteyelim. Bunu üye operatör fonksiyonu olarak yazabiliriz: class Complex { public: //... Complex operator +(double real) const; }; Böylece biz z + 10 gibi bir işlemi bu operatör fonksiyonun yaptırabiliriz. Ancak 10 + z gibi bir işlem üye operatör fonksiyonuna yaptırılamaz. Bu durumda mecburen bu işlemin global operatör fonksiyonu yoluyla yaptırılması gerekir. Örneğin: Complex operator +(double real, const Complex &z); Global operatör fonksiyonlarının normal global fonksiyonlar gibi ele alındığına dikkat ediniz. Bu nedenle eğer bu fonksiyonların ilgili sınıfın sınıfın private veri elemanlarına erişmesi isteniyorsa arkadaş yapılması gerekebilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Operatör fonksiyonu yazarken "değişme özelliğinin" sağlanması iyi bir tekniktir. Yani x + y işlemi için operatör fonksiyonu yazılıyorsa y + z işlemi için de eğer gerekiyorsa operatör fonksiyonu bulundurulmalıdır. Çünkü kişilerin beklentisi +, * gibi operatörlerde değişme özelliğinin sağlanmasıdır. Örneğin x değişkeni bir sınıf türünden olsun. Eğer x + 10 gibi bir işlemi yapacak bir operatör fonksiyonu yazılıyorsa 10 + x işlemini yapacak bir opetaör fonksiyonu da yazılmalıdır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz artık operatör fonksiyonu yazılabilecek tüm operatörlerin operatör fonksiyonlarının nasıl yazılabileceği üzerinde tek tek duracağız. Operatör fonksiyonlarını tanıtırken çeşitli sınıflarla örnek vereceğiz. Örneklerimizden biri rasyonal sayılar üzerinde işlem yapan Rational isimli bir sınıf olacaktır. Bilindiği gibi matematikte iki tamsayının bölümü biçiminde ifade edilebilen sayılara rasyonel sayılar denilmektedir. Tüm rasyonel sayıların devirli ondalık açılımları vardır. Rational sınıfı aldığı rasyonel sayıyı en sade biçimde tutmaya çalışmaktadır. Sadeleştirme için pay ve payda "ortak bölenlerinin en büyüğü (greatest common divisor)" ile bölünmektedir. İki sayının ortak bölenlerinin en büyüğünü bulmak için en etkili algoritma "Öklit algoritması" denilen algoritmadır. Sınıftaki temel üye fonksiyonlar şunlardır: namespace CSD { class Rational { public: Rational(int a, int b = 1); void disp() const; //... private: void simplify(); static int gcd(int a, int b); private: int m_a; int m_b; }; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 80. Ders 26/06/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- +, -, * ve / operatörlerine ilişkin operatör fonksiyonları yazılırken genel olarak fonksiyonlarların geri dönüş değerlerinin ilgili sınıf türünden olması anlamlıdır. Örneğin iki rosyaonel sayıyı toplayan + operatör fonksiyonun geri dönüş değeri de Rasyonel bir sayı olmalıdır. Böylece operatörler aynı ifadede kombine edilerek kullanılabilirler. Bu operatörlere ilişkin operatör fonksiyonları üye operatör fonksiyonu olarak ve global operatör fonksiyonu olarak yazılabilir. Ancak yukarıda da belirttiğimiz gibi biz mümkün olduğunca üye operatör fonksiyonlarını tercih etmeliyiz. Ayrıca bu fonksiyonlar eğer anlamlıysa farklı türlere çalışabilecek biçimde overload da edilebilirler. Örneğin iki rasyonel sayıyı toplayan + operatör fonksiyonu Rational sınıfının üye fonksiyonu olarak şöyle yazılabilr: Rational Rational::operator +(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b + m_b * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Benzer biçimde Rational sınıfına -, *, / operatör fonksiyonları da eklenebilir. Bu durumda Rational sınıfı aşağıaki duruma gelecekt: class Rational { public: Rational(int a = 0, int b = 1); Rational operator +(const Rational &r) const; Rational operator -(const Rational &r) const; Rational operator *(const Rational &r) const; Rational operator /(const Rational &r) const; void disp() const; //... private: static int gcd(int a, int b); void simplify(); private: int m_a; int m_b; }; Pekiyi bir rasyonel sayı ile bir tamsayının toplanması anlamlı mıdır? Evet anlamlıdır. Zaten tamsayılar aslında paydası 1 olan rasyonel sayılardır. Bir Ratioanal nesnesi ile bir int nesne aslında yukarıdaki operatör fonksiyonları varsa işleme sokulabilecektir. Örneğin: Rational x{1, 2}, result; result = x + 1; // aslında geçerli Biz sınfımız için int parametreli bir + operatör fonksiyun yazmamaış olsak bile x + 1 işlemi aslında geçerlidir. Bu konu ileride ayrı bir başlık halinde ele alınacaktır. Yukarıdaki işlemin eşdeğeri şöyledir: result = x.operator +(1); + operatör fonksiyonun parametresinin const Rational & türünden olduğuna dikkat ediniz. İşte aslında aşağıdaki ildeğer verme işlemi de geçerlidir: const Rational &r = 1; C++ standartlarına göre burada 1 değeri Rational nesnesine dönüştürüelecek sonra atama işlemi yapılacaktır. Başka bir deyişle aslında bu ifade aşağıdaki ile eşdeğerdir: const Rational &r = Rational(1); Biz bu konuyu henüz ele almadık. İzleyen paragraflarda "sınıflar ile ilgili tür dönüştürmeleri" konusu içerisinde bu durumu açıklayacağız. Ancak yine de Rational nesnesi ile int değerler arasında doğrudan işlem yapan operatör fonksiyonlarının sınıfa eklenmesi bu işlemlerin daha hızlı yapılmasına yol açacaktır. Bu durumda sınıfa aşağıdaki üye operatör fonksiyonları da eklenebilir: class Rational { public: Rational(int a = 0, int b = 1); Rational operator +(const Rational &r) const; Rational operator -(const Rational &r) const; Rational operator *(const Rational &r) const; Rational operator /(const Rational &r) const; Rational operator +(int a) const; Rational operator -(int a) const; Rational operator *(int a) const; Rational operator /(int a) const; void disp() const; //... private: static int gcd(int a, int b); void simplify(); private: int m_a; int m_b; }; Ancak r bir Rational nesnesi a da int bir değer olmak üzere biz örneğin r + a gibi bir işlemi yeni eklediğimiz + operatör fonksiyonuyla yapabiliriz. Ancak bunun tersi olan a + r işlemini yukarıdaki opreatör fonksiyonlarıyla yapamayız. Bu tür işlemleri ancak global operatör fonksiyonlarıyla yapılabileceğini anımsayınız. O halde bizim aşağıdaki global operatör fonksiyonlarını da yazmamız gerekir: Rational operator +(int a, const Rational &r); Rational operator -(int a, const Rational &r); Rational operator *(int a, const Rational &r); Rational operator /(int a, const Rational &r); Bu global fonksiyonların sınıfın arkadaş yapılması gerekmektedir. Ayrıca burada bir noktaya dikkatinizi çekmek istiyoruz: Aslında yukarıdaki + ve * global operatör fonksiyonları üye operatör fonksiyonları kullanılarak yazılabilir. Tabii böyle bir satırlık fonksiyonların inline yapılması uygun olur. inline fonksiyonların başlık dosyalarına bulundurulması gerektiğini anımsayınız: inline Rational operator +(int a, const Rational &r) { return r + a; } inline Rational operator *(int a, const Rational &r) { return r * a; } Bu eklemeler yapıldıktan sonra başlık dosyasındaki bildirimler şu hale gelecektir: class Rational { public: Rational(int a = 0, int b = 1); Rational operator +(const Rational &r) const; Rational operator -(const Rational &r) const; Rational operator *(const Rational &r) const; Rational operator /(const Rational &r) const; Rational operator +(int a) const; Rational operator -(int a) const; Rational operator *(int a) const; Rational operator /(int a) const; friend Rational operator -(int a, const Rational &r); friend Rational operator /(int a, const Rational &r); void disp() const; //... private: static int gcd(int a, int b); void simplify(); private: int m_a; int m_b; }; inline Rational operator +(int a, const Rational &r) { return r + a; } inline Rational operator *(int a, const Rational &r) { return r * a; } Yukarıdaki örnek fonksiyonların yazımı aşağıda bütünsel olarak verilmiştir: --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /* rational.hpp */ #ifndef RATIONAL_HPP_ #define RATIONAL_HPP_ namespace CSD { class Rational { public: Rational(int a = 0, int b = 1); Rational operator +(const Rational &r) const; Rational operator -(const Rational &r) const; Rational operator *(const Rational &r) const; Rational operator /(const Rational &r) const; Rational operator +(int a) const; Rational operator -(int a) const; Rational operator *(int a) const; Rational operator /(int a) const; friend Rational operator -(int a, const Rational &r); friend Rational operator /(int a, const Rational &r); void disp() const; //... private: static int gcd(int a, int b); void simplify(); private: int m_a; int m_b; }; inline Rational operator +(int a, const Rational &r) { return r + a; } inline Rational operator *(int a, const Rational &r) { return r * a; } } #endif /* rational.cpp */ #include #include #include #include "rational.hpp" namespace CSD { using namespace std; Rational::Rational(int a, int b) { if (b < 0) { m_a = -a; m_b = -b; } else { m_a = a; m_b = b; } if (b == 0) throw invalid_argument("denominator shall not be zero"); simplify(); } Rational Rational::operator +(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b + m_b * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator -(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b - m_b * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator *(const Rational &r) const { Rational result; result.m_a = m_a * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator /(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b; result.m_b = m_b * r.m_a; result.simplify(); return result; } Rational Rational::operator +(int a) const { Rational result; result.m_a = a * m_b + m_a; result.m_b = m_b; return result; } Rational Rational::operator -(int a) const { Rational result; result.m_a = -a * m_b + m_a; result.m_b = m_b; return result; } Rational Rational::operator *(int a) const { Rational result; result.m_a = a * m_a; result.m_b = m_b; return result; } Rational Rational::operator /(int a) const { Rational result; result.m_a = m_a; result.m_b = a * m_b; return result; } void Rational::disp() const { cout << m_a; if (m_b != 1 && m_a != 0) cout << '/' << m_b; cout << endl; } void Rational::simplify() { int gcd_val = gcd(abs(m_a), abs(m_b)); m_a /= gcd_val; m_b /= gcd_val; } int Rational::gcd(int a, int b) { int temp; while (b != 0) { temp = b; b = a % b; a = temp; } return a; } Rational operator -(int a, const Rational &r) { Rational result; result.m_a = a * r.m_b - r.m_a; result.m_b = r.m_b; return result; } Rational operator /(int a, const Rational &r) { Rational result; result.m_a = a * r.m_b; result.m_b = r.m_a; return result; } } /* app.cpp */ #include #include "rational.hpp" using namespace std; using namespace CSD; int main() { Rational x{1, 5}, y{2, 3}, z{1, 6}, result; result = x + y * z - 1; result.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İşaret + ve işaret - operatörlerinin tek operand'lı operatör olduğunu anımsayınız. Bu operatörler operand'ları üzerinde bir değişilik yapmamakta diğer opereatörlerde olduğu gibi sonuca ilişkin yeni nesne üretmektedir. Yani örneğin r değişkeninin Rational sınıfı türünden olduğunu kabul edelim. Bu durumda -r işlemi ile r nesnesi değişmeyecektir, r'nin negatifine ilişkin yeni bir nesne operatör fonksiyonunun geri dönüş değeir olarak verilecektir. Aynı durum işaret + operatörü için de benzerdir. Bu operatör fonksiyonları üye operaör fonksiyonu olarak ya da global operatör fonksiyonu olarak yazılabilir. Yukarıda da belirttiğimiz gibi bu tür durumlarda biz operatör fonksiyonunun üye fonksiyonu olarak yazılmasını tavsiye ediyoruz. İşaret + operatörünün nesnenin aynı değeri ile geri döndüğünü anımsayınız. Bu durumda bu operatör fonksiyonu *this ile geri döndürülebilir ve fonksiyonun geri dönüş değeri aynı sınıf türünden const bir referans olabilir. Örneğin Ration sınıfında işaret + operatörü üye fonksiyon olarak şöyle yazılabilir: const Rational &Rational::operator +() { return *this; } Böylece +r gibi bir ifade kullanıldığında bunun r'den bir farkı olmayacaktır. İşaret + ve işaret - operatörleri nesne belirtmediği için bu fonksiyonun , geri dönüş değeri const & yapılmıştır. Böylece aşağıdaki gibi bir ifade geçerli olmayacaktır: Rational x{1, 2}, y; +y = x; // geçersiz! +y const & belirttiği için Rational sınıfınn kopya atama operatör fonksiyonu çağrılamayacaktır İşaret - operatör fonksiyonu mantıksal olarak operand'ının negatif değeri ile geri dönmelidir. Tabii geri dönüş değeri olarak nesnenin kendisini değil negatif bir kopyasını vermelidir. Örneğin Rational sınıfı için - operatör fonksiyonu şöyle yazılabilir: Rational Rational::operator -() const { Rational result; result.m_a = -m_a; result.m_b = m_b; return result; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 81. Ders 01/07/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- ++ ve -- operatörlerine ilşkin operatör fonksiyonlarının yazımı biraz ilginçtir. Öncelikle bu operatörler hakkında şu anımsatmayı yapalım: 1) Bu operatörlerin önek kullanımları nesnenin kendisini artırmakta ya da eksiltmektedir ve nesnenin kendisine ilişkin sol taraf değeri üretmektedir. Bu operatörler için yazılacak operatör fonksiyonunun bu semantiği sağlaması gerekir. 2) Bu operatörlerin sonek kullanımları nesnenin kendisi artırmakta ya da eksiltmektedir ancak sonraki işleme nensnenin artmamış ve eksiltilmemiş hali sokulmaktadır. Bu operatörlerin son ek kullanımları bir sol taraf değeri belirtmemektedir. Bu operatörler için yazılacak operatör fonksiyonunun bu semantiği sağlaması gerekir. Yukarıdaki semantik tek bir fonksiyon tarafından karşılanamayacağı için bu operatörlerin önek ve sonek biçimleri ayrı iki operatör fonksiyonu olarak yazılmaktadır. Ancak bunların birbirlerine karışmaması için sonek versiyonlar "dummy bir int parametre" almaktadır. Pekiyi bu fonksiyonların önek ve sonek biçimlerinin parametrik yapıları nasıl olmalıdır? İşte bu oparatör fonksiyonları eğer üye fonksiyonu olarak yazılacaksa bunların sıfır parametresi olmalıdır. Ancak geri dönüş değerlerinin aynı sınıf türünden referans olması ve fonksiyonun artırım işleminden sonra *this ile geri dönmesi uygun olacaktır. Örneğin sınıfın ismi T olmak üzere bu operatörlerin önek biçimleri şöyle oluşturulmaldır: class T { public: // T &operator ++(); // önek ++ T &operator --(); // önek -- //... }; T &T::operator ++() // önek ++ { return *this; } T &T::operator --() // önek -- { return *this; } Tabii bu operatör fonksiyonları global olarak yazılacaksa bir parametre sahip olacaklardır. Burada bu operatör fonksiyonlrı çağrıldığında artırılmış nesnenin kendisinin elde edildiğine dikkat ediniz. Eğer bu operatörlerin sonek kullanımına ilişkin operatör fonksiyonu üye operatör fonksiyonu olarak yazılacaksa dummy bir int parametre belirtilmelidir. Bu int parametre hiç kullanılmayacağı için ona isim verilmesine de gerek yoktur. Sonek ++ ve -- operatörlerine sonek semantiği verebilmek için artırım ya da eksiltimin nesne üzerinde yapılması ancak fonksiyonun nesnenin artırılmamış ya da eksiltilmemiş bir kopyası ile geri döndürülmesi gerekmektedir. Bu durumda fonksiyonun geri dönüş değerinin kendi sınıfı türünden olması (kendi sınıf türünden referans değil) atamayı engellemek için const yapılması uygun olmaktadır. Örneğin: Örneğin: class T { public: // const T operator ++(int); // sonek ++ const T operator --(int); // sonek -- //... }; const T T::operator ++(int) { T temp = *this; return temp; } const T T::operator --(int) { T temp = *this; return temp; } Burada fonksiyonların nesnelerin artırılmamış ya da eksiltilmemiş biçimlerinin kopyalarına geri döndüğüne ancak nesneler üzerinde artırma ve eksiltme işlemini yaptığına dikkat ediniz. Fonksiyonların geri dönüş değerlerinin const olması sonek kullanımların atama gibi işlemlere sokulamamasına yol açmaktadır. Böylece bu fonksiyonlar nesne belirten bir ifade gibi kullanılamatacaklardır. Örneğin Rational sayı sınıfı için ++ ve -- operatör fonksiyonları üye fonksiyon olarak aşağıdaki gibi yazılabilir: class Rational { public: //... Rational &operator ++(); // prefix ++ Rational &operator --(); // prefix -- const Rational operator ++(int); // postfix -- const Rational operator --(int); // postfix - void disp() const; //... private: int m_a; int m_b; }; Rational &Rational::operator ++() // prefix ++ { m_a = m_a + m_b; return *this; } Rational &Rational::operator --() // prefix -- { m_a = m_a - m_b; return *this; } const Rational Rational::operator ++(int) // postfix ++ { Rational temp = *this; m_a = m_a + m_b; return temp; } const Rational Rational::operator --(int) // postfix -- { Rational temp = *this; m_a = m_a - m_b; return temp; } Pekiyi derleyici ++ ve -- operatörleriyle karşılaştığında operand'lar bir sınıf türündense ne yapmaktadır? Örneğin x T sınıfı türünden bir nesne olsun ve biz onu ifade içerisinde ++x biçiminde kullanmış olalım. İşte derleyici bu durumda Sanki bu işlemi x.operator++() ve operator ++(x) biçiminde ele almaktadır. Bu durumda sınıftaki ++ operatör fonksiyonları ve global ++ operatör fonksiyonları aday fonksiyonlar olarak seçilecektir. Ancak sonek ++ operatör fonksiyonlarının dummpy int parametreleri olduğu için bunlar asla uygun fonksiyon olarak seçilemeyecektir. Şimdi biz bu x nesnesini x++ biçiminde kullanmış olalım. Bu durumda bu işlemin eşdeğeri x.operator ++(0) ya da operator ++(x, 0) biçimindedir. Buradaki argümanın 0 olmasının bir önemi yoktur. İşte derleyici bu durumda sınıftaki ++ operatör fonksiyonlarını ve global ++ operatör fonksiyonlarını aday fonksiyon olarak belirler. Ancak artık önek ++ fonksiyonları uygun fonksiyon olarak seçilemeyecektir. Standartlarda sonek ++ ve -- operatörlerinin operand'ları birer sınıf türündense derleyicinin dummy int parametre için 0 değerini argüman yaptığı belirtilmiştir. Yani x++ gibi bir işlemin derleyici için eşdeğeri x.operator ++(0) ya da operator ++(x, 0) biçimindedir. Tabii biz sonek ++ vee operatörlerini fonksiyon çağırma sentaksıyla kullanırsakbu argümana istediğimiz değeri verebiliriz. Tabii bu dummy parametrenin isimlendirilerek kullanılmasının bir anlamı yoktur. Örneğin c.operator ++(10) gibi bir çağrım da geçerlidir. Aşağıda rasyonel sayı işlemlerini yapan Rational sınıfı görmüş olduğumuz operatör fonksiyonları eklenerek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // rational.hpp #ifndef RATIONAL_HPP_ #define RATIONAL_HPP_ namespace CSD { class Rational { public: Rational(int a = 0, int b = 1); Rational operator +(const Rational &r) const; Rational operator -(const Rational &r) const; Rational operator *(const Rational &r) const; Rational operator /(const Rational &r) const; Rational operator +(int a) const; Rational operator -(int a) const; Rational operator *(int a) const; Rational operator /(int a) const; friend Rational operator -(int a, const Rational &r); friend Rational operator /(int a, const Rational &r); const Rational &operator +() const; Rational operator -() const; Rational &operator ++(); // prefix ++ Rational &operator --(); // prefix -- const Rational operator ++(int); // postfix -- const Rational operator --(int); // postfix - void disp() const; //... private: static int gcd(int a, int b); void simplify(); private: int m_a; int m_b; }; inline Rational operator +(int a, const Rational &r) { return r + a; } inline Rational operator *(int a, const Rational &r) { return r * a; } } #endif // rational.cpp #include #include #include #include "rational.hpp" namespace CSD { using namespace std; Rational::Rational(int a, int b) { if (b < 0) { m_a = -a; m_b = -b; } else { m_a = a; m_b = b; } if (b == 0) throw invalid_argument("denominator shall not be zero"); simplify(); } Rational Rational::operator +(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b + m_b * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator -(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b - m_b * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator *(const Rational &r) const { Rational result; result.m_a = m_a * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator /(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b; result.m_b = m_b * r.m_a; result.simplify(); return result; } Rational Rational::operator +(int a) const { Rational result; result.m_a = a * m_b + m_a; result.m_b = m_b; return result; } Rational Rational::operator -(int a) const { Rational result; result.m_a = -a * m_b + m_a; result.m_b = m_b; return result; } Rational Rational::operator *(int a) const { Rational result; result.m_a = a * m_a; result.m_b = m_b; return result; } Rational Rational::operator /(int a) const { Rational result; result.m_a = m_a; result.m_b = a * m_b; return result; } void Rational::disp() const { cout << m_a; if (m_b != 1 && m_a != 0) cout << '/' << m_b; cout << endl; } void Rational::simplify() { int gcd_val = gcd(abs(m_a), abs(m_b)); m_a /= gcd_val; m_b /= gcd_val; } int Rational::gcd(int a, int b) { int temp; while (b != 0) { temp = b; b = a % b; a = temp; } return a; } Rational operator -(int a, const Rational &r) { Rational result; result.m_a = a * r.m_b - r.m_a; result.m_b = r.m_b; return result; } Rational operator /(int a, const Rational &r) { Rational result; result.m_a = a * r.m_b; result.m_b = r.m_a; return result; } const Rational &Rational::operator +() const { return *this; } Rational Rational::operator -() const { Rational result; result.m_a = -m_a; result.m_b = m_b; return result; } Rational &Rational::operator ++() // prefix ++ { m_a = m_a + m_b; return *this; } Rational &Rational::operator --() // prefix -- { m_a = m_a - m_b; return *this; } const Rational Rational::operator ++(int a) // postfix ++ { Rational temp = *this; cout << a << endl; m_a = m_a + m_b; return temp; } const Rational Rational::operator --(int) // postfix -- { Rational temp = *this; m_a = m_a - m_b; return temp; } } // app.cpp #include #include "rational.hpp" using namespace std; using namespace CSD; int main() { Rational x{1, 2}, y{1, 3}, result; result = x++ - y; result.disp(); // 1/6 x.disp(); // 3/2 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Karşılaştırma operatörleri iki operand'lı operatörler olduğu için bunlar üye fonksiyon olarak yazılacaksa bir parametreye global operatör fonksiyonu olarak yazılacaksa iki parametreye sahip olmak zorundadır. Bu operatör fonksiyonlarının geri dönüş değerlerinin bool türden olması en normakl durumdur. Operatör fonksiyonları konusunun girişinde de belirttiğimiz gibi eüer mümkünse tüm karşılaştırma operatörleri için operatör fonksiyonlarının yazılması iyi bir tekniktir. Eğer nmümkün değilse karşıt operatörlerin yazılmasına gayret edilmelidir. Yani örneğin iki olduğu arasında büyüklük küçüklük ilişkisi olmayabilir ancak eşitlik ilişkisi olabilir. Bu durumda == operatör fonksiyonuyazılacaksa != operatör fonksiyonu da yazılmalıdır. > operatör fonksiyonu yazılacaksa < operatör fonksiyonun yazılması da anlamlıdır. Tabii bir operatör fonksiyonun tezatı aslında diğeri kullanılarak da yazılabilir. Örneğin == operatör fonksiyonu yazılmışsa !(a == b) işlemi zaten a != b anlamına gelmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında iki karşılaştırma operatörü yazılırsa diğer karşılaştırma operatörleri bu iki operatör kullanılarak yazılabilir. Bu iki operatörden biri < ya da > diğeri ise == ya da != operatörü olabilir. Örneğin < ve == operatörlerine ilişkin operatör fonksiyonlarının yazılmış olduğunu kabul edelim. Şimdi biz a ve b nesneleriyle bu operatörleri kullanarak 6 karşılaştırma işlemini şöyle yapabiliriz: 1) a < b İşlemi: Zaten bu operatör fonksiyonu yazılmıştır. 2) a == b İşlemi: Zateb bu operatör fonksiyonu yazılmıştır 3) a > b İşlemi: Bu işlemin eşdeğeri b < a olduğuna göre ve < operatör fonksiyonu yazılmış olduğuna göre bu operatör fonksiyonu < operatör fonksiyonu çağrılarak yazılabilir. Örneğin: bool opeartor >(a, b) { return b < a; } 4) a <= b İşlemi: Bu işlem "küçüktür ya da eşittir" biçiminde ifade edilebilir. Yani bu operatör fonksiyonu şöyle yazılabilir: bool opeartor <=(a, b) { return a < b || a == b; } 5) a >= b işlemi: Bu işlem de "büyüktür ya da eşittir" biçiminde ifade edilebilir. Yani bu operatör fonksiyonu şöyle yazılabilir: bool opeartor >=(a, b) { return b < a || a == b; } 6) a != b İşlemi: Bu işlem "eşit değildir" biçiminde ifade edilebilir. Bu operatör fonksiyonu şöyle yazılabilir: bool operator !=(a, b) { return !(a == b); } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 82. Ders 08/07/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarında özellikle standart kütüphanede matematikteki "sıralama ilişkisine (ordering relation)" göndermeler yapılmıştır. Yani bazı olgular bu sıralama ilişkisine ilişkin terminoloji kullanılarak açıklanmıştır. Biz de burada sıralama ilişkisine ilişkin iki önemli kavramı açıklayacağız. Bunlardan biri İngilizce "total ordering" denilen ilişkidir. Buna Türkçe "tam sıralama ilişkisi" diyebiliriz. Diğeri ise "partial ordering" denilen ilişkidir. Buna da Türkçe "kısmi sıralama ilişkisi diyebiliriz. Eğer bir kümenin elemanları arasında tam sıralama ilişkisi varsa herhangi iki eleman a ve b olmak üzere aşağıdaki üç durumdan biri söz konusudur: 1) a < b 2) b < a 3) a == b Yani biz o kümenin herhangi iki elemanını aldığımızda ya biri diğerinden küçüktür ya da bunlar eşittir. Tam sıralama ilşkisi tipik olarak <= operatörü ile betimlenmektedir. Örneğin tamsayılar kümesi tam sıralama ilişkisine sahiptir. Yani herhangi iki eleman aldığımızda ya biri diğerinden küçüktür ya da bunlar eşittir. Tam sıralama ilişkisi kümenin elemanlarının sıraya dizilebilmesi anlamına gelmektedir. Eğer bir kümede tam sıralama ilişkisi varsa biz o küme üzerinde işlem yapan bir sınıf yazmak istersek o sınıf için yukarıda da belirttiğimiz gibi yalnızca < ve == operatör fonksiyonlarını yazabiliriz. Diğer karşılaştırma operatörleri bu operatör fonksiyonları kullanılarak tanımlanabilmektedir. Kısmi sıralama ilişkisi tam sıralama ilişkisine benzemektedir. Ancak kısmi sırlama ilişkisinde kümenin tüm elemanları arasında küçüklük ya da eşitlik ilişkisi olmak zorunda değildir. Yalnızca bazı elemanları arasında bu ilişkisi varsa buna kısmi sıralama ilişkisi denilmektedir. Bu durumda kısmi sıralama ilişkisinde kümenin herhangi iki elemanı a ve b olmak üzere şu dört durumdan biri söz konusudur: 1) a < b 2) b < a 3) a == b 4) a ve b karşılaştırılmaz Örneğin IEEE gerçek sayılar kümesi kısmi sıralama ilişkisine sahiptir. Çünkü kümedeki NaN gibi bir gerçek sayı belirtmeyen bit kombinasyonları vardır. Bu NaN sayıları diğer gerçek sayılarla karşılaştırılamaz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın standart kütüphanesinde bulunan bazı fonksiyonlar ve sınıflar bir biçimsde tam sıralama ilişkisini kullanmaktadır. Örneğin sort isimli fonksiyon şablonu herhangi türden bir diziyi sıraya dizmektedir. Ancak bu sıraya dizme işlemini < operatörünü kullanarak yapmaktadırç. Dolayısıyla sort fonksiyonunun bizim sınıf nesnelerinden oluşan dizimizi sıraya dizmesini istiyorsak sınıfımızda < operatör fonksiyonunu bulundurmalıyız. Diğer karşılaştırma operatör fonksiyonlarını bulundurmak zorunda değiliz. Biz henüz şablon (template) işlemlerini görmedik. Ancak burada sort fonksiyonun nasıl kullanıldığını açıklamak istiyoruz. C'de bir diziyi parametre olarak almak isteyen fonksiyonlar genellikle dizinin başlangıç adresini ve uzunluğunu parametre olarak almaktadır. Halbuki C++'ın fonksiyon şablonları dizinin başlangıcına ve bitişine ilişkin adresleri (yani ilk elemanın ve son elemandan sonraki olmayan elemanın adreslerini) almaktadır. Örneğin a dizisi 10 eleman uzunluğunda olsun. Biz bu a dizisini sort fonksiyonuyla şöyle sıraya dizeriz: sort(a, a + 10); Aşağıda sort fonksiyonun kullanımına örnekler verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class Number { public: Number(int val) : m_val(val) {} int val() const { return m_val; } bool operator <(const Number &r) const { return m_val < r.m_val; } private: int m_val; }; int main() { Number numbers[] = {Number(12), Number(5), Number(7), Number(56), Number(45)}; int a[] = {4, 78, 2, 12, 5}; string names[] = {string("selami"), string("ali"), string("veli"), string("ayse"), string("fatma")}; sort(numbers, numbers + 5); for (int i = 0; i < 5; ++i) cout << numbers[i].val() << " "; cout << '\n'; sort(a, a + 5); for (int i = 0; i < 5; ++i) cout << a[i] << " "; cout << '\n'; sort(names, names + 5); for (int i = 0; i < 5; ++i) cout << names[i] << " "; cout << '\n'; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de rasyonel sayılar üzerinde işlemler yapan Rational sınıfımıza karşılaştırma operatör fonksiyonlarını ekleyelim. Bu durumda sınıf bildirimi şu hale gelecektir: class Rational { public: Rational(int a = 0, int b = 1); Rational operator +(const Rational &r) const; Rational operator -(const Rational &r) const; Rational operator *(const Rational &r) const; Rational operator /(const Rational &r) const; Rational operator +(int a) const; Rational operator -(int a) const; Rational operator *(int a) const; Rational operator /(int a) const; friend Rational operator -(int a, const Rational &r); friend Rational operator /(int a, const Rational &r); const Rational &operator +() const; Rational operator -() const; Rational &operator ++(); // prefix ++ Rational &operator --(); // prefix -- const Rational operator ++(int); // postfix -- const Rational operator --(int); // postfix - bool operator <(const Rational &r) const; bool operator >(const Rational &r) const; bool operator <=(const Rational &r) const; bool operator >=(const Rational &r) const; bool operator ==(const Rational &r) const; bool operator !=(const Rational &r) const; void disp() const; //... private: static int gcd(int a, int b); void simplify(); private: int m_a; int m_b; }; inline Rational operator +(int a, const Rational &r) { return r + a; } inline Rational operator *(int a, const Rational &r) { return r * a; } Bu karşılaştırma operatör fonksiyonlarının Rational sınıfı için yazılması aslında oldukça kolaydır. Biz rasyonel sayıların payını paydasına bölüp elde edilen noktalı sayıları karşılaştırabiliriz. Her ne kadar iki gerçek sayının tam eşitlik karşılatırması yuvarlama hatalarından dolayı sorunluysa da bizim sınıfımızda rasyonel sayılar üzerinde sadeleştirmeler yaptığımız için böylesi bir sorun oluşmayacaktır. Örneğin < operatör fonksiyonu üye fonksiyon olarak şöyle yazabiliriz: bool Rational::operator <(const Rational &r) const { return static_cast(m_a) / m_b < static_cast(r.m_a) / r.m_b; } Aşağıda sınıfın tüm hali verlmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // rational.hpp #ifndef RATIONAL_HPP_ #define RATIONAL_HPP_ namespace CSD { class Rational { public: Rational(int a = 0, int b = 1); Rational operator +(const Rational &r) const; Rational operator -(const Rational &r) const; Rational operator *(const Rational &r) const; Rational operator /(const Rational &r) const; Rational operator +(int a) const; Rational operator -(int a) const; Rational operator *(int a) const; Rational operator /(int a) const; friend Rational operator -(int a, const Rational &r); friend Rational operator /(int a, const Rational &r); const Rational &operator +() const; Rational operator -() const; Rational &operator ++(); // prefix ++ Rational &operator --(); // prefix -- const Rational operator ++(int); // postfix -- const Rational operator --(int); // postfix - bool operator <(const Rational &r) const; bool operator >(const Rational &r) const; bool operator <=(const Rational &r) const; bool operator >=(const Rational &r) const; bool operator ==(const Rational &r) const; bool operator !=(const Rational &r) const; void disp() const; //... private: static int gcd(int a, int b); void simplify(); private: int m_a; int m_b; }; inline Rational operator +(int a, const Rational &r) { return r + a; } inline Rational operator *(int a, const Rational &r) { return r * a; } } #endif // rational.cpp #include #include #include #include "rational.hpp" namespace CSD { using namespace std; Rational::Rational(int a, int b) { if (b < 0) { m_a = -a; m_b = -b; } else { m_a = a; m_b = b; } if (b == 0) throw invalid_argument("denominator shall not be zero"); simplify(); } Rational Rational::operator +(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b + m_b * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator -(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b - m_b * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator *(const Rational &r) const { Rational result; result.m_a = m_a * r.m_a; result.m_b = m_b * r.m_b; result.simplify(); return result; } Rational Rational::operator /(const Rational &r) const { Rational result; result.m_a = m_a * r.m_b; result.m_b = m_b * r.m_a; result.simplify(); return result; } Rational Rational::operator +(int a) const { Rational result; result.m_a = a * m_b + m_a; result.m_b = m_b; return result; } Rational Rational::operator -(int a) const { Rational result; result.m_a = -a * m_b + m_a; result.m_b = m_b; return result; } Rational Rational::operator *(int a) const { Rational result; result.m_a = a * m_a; result.m_b = m_b; return result; } Rational Rational::operator /(int a) const { Rational result; result.m_a = m_a; result.m_b = a * m_b; return result; } void Rational::disp() const { cout << m_a; if (m_b != 1 && m_a != 0) cout << '/' << m_b; cout << endl; } void Rational::simplify() { int gcd_val = gcd(abs(m_a), abs(m_b)); m_a /= gcd_val; m_b /= gcd_val; } int Rational::gcd(int a, int b) { int temp; while (b != 0) { temp = b; b = a % b; a = temp; } return a; } Rational operator -(int a, const Rational &r) { Rational result; result.m_a = a * r.m_b - r.m_a; result.m_b = r.m_b; return result; } Rational operator /(int a, const Rational &r) { Rational result; result.m_a = a * r.m_b; result.m_b = r.m_a; return result; } const Rational &Rational::operator +() const { return *this; } Rational Rational::operator -() const { Rational result; result.m_a = -m_a; result.m_b = m_b; return result; } Rational &Rational::operator ++() // prefix ++ { m_a = m_a + m_b; return *this; } Rational &Rational::operator --() // prefix -- { m_a = m_a - m_b; return *this; } const Rational Rational::operator ++(int a) // postfix ++ { Rational temp = *this; cout << a << endl; m_a = m_a + m_b; return temp; } const Rational Rational::operator --(int) // postfix -- { Rational temp = *this; m_a = m_a - m_b; return temp; } bool Rational::operator <(const Rational &r) const { return static_cast(m_a) / m_b < static_cast(r.m_a) / r.m_b; } bool Rational::operator >(const Rational &r) const { return static_cast(m_a) / m_b > static_cast(r.m_a) / r.m_b; } bool Rational::operator <=(const Rational &r) const { return static_cast(m_a) / m_b <= static_cast(r.m_a) / r.m_b; } bool Rational::operator >=(const Rational &r) const { return static_cast(m_a) / m_b >= static_cast(r.m_a) / r.m_b; } bool Rational::operator ==(const Rational &r) const { return static_cast(m_a) / m_b == static_cast(r.m_a) / r.m_b; } bool Rational::operator !=(const Rational &r) const { return static_cast(m_a) / m_b != static_cast(r.m_a) / r.m_b; } } // app.cpp #include #include #include "rational.hpp" using namespace std; using namespace CSD; int main() { Rational x{1, 3}, y{1, 2}; if (x > y) cout << "x > y" << endl; else if (x < y) cout << "x < y" << endl; else if (x == y) cout << "x < y" << endl; Rational rnumbers[] = {Rational(1, 3), Rational(3, 4), Rational(4, 3), Rational(2, 3), Rational(3, 7)}; sort(rnumbers, rnumbers + 5); for (auto &r : rnumbers) r.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesini [] operatörüyle kullanabilmek için o sınıfta bir [] operatör fonksiyonunun bulunuyor olması gerekir. a bir sınıf nesnesi olmak üzere a[n] işleminin eşdeğeri a.operator[](n) biçimindedir. Gerçekten de standart kütüphanedeki vector gibi string gibi sınıflar bu operatör fonksiyonuna sahiptir. [] opeartör fonksiyonu üye operatör fonksiyonu olarak yazılmak zorundadır. [] operatör fonksiyonun bir paramtresi olmak zorundadır. Buı operatör fonksiyonlarının geri dönüş değerleri herhangi bir türdne olabilirse de en uygun durum bu fonksiyonların geri dönüş değerlerinin bir referans olmasıdır. Böylelikle köşeli parantezli ifadeye atama yapılabilir. Örneğin: result = a[n] + 10; ifadesinin eşdeğeri şöyledir: result = a.operator [](n) + 10; Örneğin: a[n] = 10; ifadesinin eşdeğeri de şöyledir: a.operator [](n) = 10; İşte bir fonksiyonun geri dönüş değerine bir atama yapabilmek için fonksiyonun geri dönüş değerinin bir referans belirtmesi gerekir. Şimdi a nesnesinin const bir sınıf nesnesi olduğunu düşünelim. Bu durumda const nesneyle const olmayan üye fonksiyon çağrılamayacağı için [] operatör fonksiyonunun const bir biçiminin de bulunması gerekir. Tabii const nesnelerin [] operatöryle kullanımları sonucunda elde edilen nesneye atama yapılmasının engellenmesi için bu const biçimin geri dönüş değerinin de const referans olması uygun olur. Özetle eğer T sınıfı için [] operatör fonksiyonu yazılmak isteniyorsa bu fonksiyonların parametrik yapılarınınm aşağıdaki gibi olması uygundur. T &operator [](K index); const T &operator[] (K index) const; Burada T türü [] ile elde edilen nesnenin türünü temsil etmektedir. K türü ise [] içerisinde kullanılan ifadenin türünü temsil etmektedir. C'de ve C++'ta köşeli parantezin bir operatör olduğunu dolayısıyla onun içerisindeki ifadeinin de tamsayı türlerine ilişkin olması gerektediğini anımsayınız. Ancak C++'ta eğer [] operatörünün operandı bir sınıf türünden nesne ise bu durumda köşeli parantezin içerisindeki ifadenin tamsayı türündne olması gerekmez. Örneğin a bir sınıf türünden nesne belirtiyor olsun: result = a["ankara"]; Burada a bir adres belirtiyorsa bu ifade geçersizdir. Ancak C++ta a bir sınıf türünden nesne belirtiyorsa bu ifadenin eşdeğeri şöyledir: result = a.operator []("ankara"); O halde böylesi bir kullanım C++'ta mümkün olabilir. Tabii bir sınıfta [] operatörüne ilişkin operatör fonksiyonun bulunması için sınıfın "bir indekse dayalı bir değeri elde edecek bir organizasyona sahip olması" gerekir. Yani sınıfımız için bu [] kullanımı anlamlı ise bu operatör fonksiyonunu yazmalıyız. Aşağıdaki örnekte Date sınıfının day, month, year bileşenleri [] operatör fonksiyonu ile elde edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Date { public: Date(int day, int month, int year) : m_day(day), m_month(month), m_year(year) {} void disp() const; int &operator[] (size_t index); const int &operator[] (size_t index) const; private: int m_day; int m_month; int m_year; }; void Date::disp() const { cout << m_day << '/' << m_month << '/' << m_year << endl; } int &Date::operator[] (size_t index) { if (index == 0) return m_day; if (index == 1) return m_month; if (index == 2) return m_year; throw invalid_argument("index out of range"); } const int &Date::operator[] (size_t index) const { if (index == 0) return m_day; if (index == 1) return m_month; if (index == 2) return m_year; throw invalid_argument("index out of range"); } int main() { Date d(12, 11, 2007); cout << d[0] << endl; cout << d[1] << endl; cout << d[2] << endl; d[0] = 13; d[1] = 6; d[2] = 2022; d.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de int türden bir diziyi temsil eden IntArray isimli bir sııf için [] operatör fonksiyonunu yazalım. Anımsayacağınız gibi zaten C++'ın standart kütüphanesinde C++11 ile eklenen array isimli bir sınıf şablonu bulunmaktadır. Aşağıda bu IntArray sınıfının gerçekleştirimini veriyoruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // intarray.hpp #ifndef INTARRAY_HPP_ #define INTARRAY_HPP_ #include #include namespace CSD { class IntArray { public: IntArray(); IntArray(size_t size); IntArray(std::initializer_list il); IntArray(const IntArray &ia); IntArray(IntArray &&ia); ~IntArray(); IntArray &operator =(const IntArray &ia); IntArray &operator =(IntArray &&ia); int &operator[](size_t index); const int &operator[](size_t index) const; void disp() const; private: int *m_array; std::size_t m_size; }; } #endif // intarry.cpp #include #include #include "intarray.hpp" using namespace std; namespace CSD { IntArray::IntArray() { m_array = nullptr; m_size = 0; } IntArray::IntArray(size_t size) { m_array = new int[size]; m_size = size; } IntArray::IntArray(initializer_list il) { m_size = il.size(); m_array = new int[m_size]; for (size_t i = 0; int val : il) m_array[i++] = val; } IntArray::IntArray(const IntArray &ia) { m_array = new int[ia.m_size]; for (size_t i = 0; i < ia.m_size; ++i) m_array[i] = ia.m_array[i]; m_size = ia.m_size; } IntArray::IntArray(IntArray &&ia) { m_array = ia.m_array; m_size = ia.m_size; ia.m_array = nullptr; } IntArray::~IntArray() { delete[] m_array; } void IntArray::disp() const { for (size_t i = 0; i < m_size; ++i) cout << m_array[i] << " "; cout << endl; } IntArray &IntArray::operator =(const IntArray &ia) { if (this == &ia) return *this; delete[] m_array; m_array = new int[ia.m_size]; for (size_t i = 0; i < ia.m_size; ++i) m_array[i] = ia.m_array[i]; m_size = ia.m_size; return *this; } IntArray &IntArray::operator =(IntArray &&ia) { if (this == &ia) return *this; swap(m_array, ia.m_array); m_size = ia.m_size; return *this; } int &IntArray::operator[](size_t index) { if (index >= m_size) throw invalid_argument("index aout of range"); return m_array[index]; } const int &IntArray::operator[](size_t index) const { if (index >= m_size) throw invalid_argument("index aout of range"); return m_array[index]; } } // app.cpp #include #include "intarray.hpp" using namespace std; using namespace CSD; IntArray foo() { IntArray ia = {1, 2, 3, 4, 5}; //... return ia; } int main() { IntArray ia; const IntArray cia = {10, 20, 30, 40}; ia = foo(); ia.disp(); ia[4] = 500; ia.disp(); cia.disp(); cout << cia[2] << endl; // cia[2] = 10; ---> error! return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta bir sınıf nesnesi sanki bir fonksiyonmuş gibi fonksiyon çağırma operatörüyle kullanılabilir. Bunun için sınıfın fonksiyon çağırma operatör fonksiyonun yazılmış olması gerekir. a bir sınıf türünden nesne belirtmek üzere: a(...) ifadesinin eşdeğeri: a.operator()(...) biçimindedir. Burada operator () operatör fonksiyonun ismini belirtmektedir. Buradaki parantezlerin içi boş olmak zorundadır. Diğer (...) ise gerçekten fonksiyon çağırma operatörünü belirtmektedir. Bir sınıf nesnesinin bir fonksiyon gibi kullanılmasına " fonksiyon nesneleri (function object)" ya da İngilizce kısaca "functor" denilmektedir. C++'ın standart kütüphanesinde bir fonksiyon isteyen şablon sınıflar bu biçimde functor nesnelerini de kabul ederler. Sınıflar durumsal bilgileri tutabildikleri için callback mekanizmasında fonksiyonlara gör avantajlara sahip olmaktadır. Fonksiyon çağırma operatör fonksiyonu üye operatör fonksiyonu olarak yazılmak zorundadır. Fonksiyon çağırma operatör fonksiyonları herhangi bir parametrik yapıya ve geri dönüş değerine sahip olabilirler. Aşağıdaki örnekte Pow isimli sınıf nesne yaratılırken yapıcı fonksiyonla üst değerini parametre olarak almaktadır. Sonra da fonksiyon çağırma operatör fonksiyonu parametre yoluyla aldığı değerin burada belirtilen üssüne geri dönmektedir. Örneğin: class Pow { public: Pow(double pow) : m_pow(pow) {} double operator ()(double base) { return pow(base, m_pow); } private: double m_pow; }; Kullanım çyle olabilir: Pow pow{2}; double result; result = pow(3); // result = pow.operator ()(3) cout << result << endl; // 9 --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Pow { public: Pow(double pow) : m_pow(pow) {} double operator ()(double base) { return pow(base, m_pow); } private: double m_pow; }; int main() { Pow pow{2}; double result; result = pow(3); // result = pow.operator ()(3) cout << result << endl; // 9 result = pow(4); // result = pow.operator ()(3) cout << result << endl; // 16 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Örneğin C++'ın standart kütüphanesi içerisindeki for_each fonksiyonu dolaşılabilir bir dizilimi (bu terimi özennikle kullandık) paramatere olarak alır ve dizilimin her elemanı ile bu fonksiyonu çağırır. for_eack bir fonksi,yon şablonu biçiminde yazılmıştır. Biz bu fonksiyona normal bir fonksiyonu parametre olarak geçirebileceğimiz gibi fonksiyon çağırma operatör fonksiyonu yazılmış olan bir fonksiyon nesnesini de parametre olarak geçirebiliriz. Daha önceki örnekte de belirttiğimiz gibi bu tür standart fonksiyonlara parametre olarak bir dizi geçirilecekse dizi onun ilk elemanının ve son elemanından sonraki elemanın adresi yoluyla geçirilmelidir. Örneğin: void foo(int val); int a[] = {1, 2, 3, 4, 5}; for_each(a, a + 5, foo); Burada dizinin her elemanı için foo fonksiyonu çağrılacaktır. Dizi elemanları bu fonksiyona argüman olarak geçirilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class Pow { public: Pow(double pow) : m_pow(pow) {} void operator ()(double base) { cout << pow(base, m_pow) << endl; } private: double m_pow; }; void foo(int val) { cout << val << endl; } int main() { int a[] = {1, 2, 3, 4, 5}; Pow pow{2}; for_each(a, a + 5, foo); cout << "----------------" << endl; for_each(a, a + 5, pow); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyon nesnelerinin kullanımına diğer bir örnek de copy_if isimli standart C++ fonksiyonuna dayalı olarak verilebilir. copy_if fonksiyonu bir dizilim içerisindeki değerleri başka bir dizilem kopyalamak için kullanılmaktadır. Ancak kopyalamada tüm elemanlar değil bizim istediğimiz koşulu sağlayan elemanlar kopyalanmaktadır. Fonksiyon bizden bir fonksiyon ister, kaynak dizilimdeki elemanları bu fonksiyona argüman yaparak fonksiyonu çağırır. Eğer fonksiyonm true ile geri dönerse o elemanı kopyalamaya dahil eder, false ile geri dönerse o elemanı kopyalamaya dahil etmez. Fonksion hedef diziye son kopyalanan elemanın hedef dizideki adresinden sonraki adresi geri döndürmektedir. Fonksiyonun parametreleri şöyledir. copy_if(source_beg, source_end, target_beg, function) Örneğin: int a[10] = {4, 5, 6, 12, 45, 67, 21, 45, 78, 23}; int b[10]; int *end; end = copy_if(a, a + 10, b, foo); Burada a dizisinin tüm elemanları sırasıyla foo fonksiyonuna sokulacak eğer foo fonksiyonu true ile geri dönerse o eleman b'ye kopyalanacaktır. Fonksiyon b'de kopyalanan son elemandan sonraki adresle geri dönmektedir. C++'ın standart kütüphanesinde bool değere geri dönen bu biçimde callback fonksiyonlara "predicate" denilmektedir. Bu predicate fonksiyon tek parametreye sahipse "unary predicate", iki parametreye sahipse "binary predicate" denilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class UnaryPredicate { public: UnaryPredicate(int val) : m_val(val) {} bool operator ()(int a) { return a > m_val; } private: int m_val; }; int main() { int a[10] = {4, 5, 6, 12, 45, 67, 21, 45, 78, 23}; int b[10]; int *end; end = copy_if(a, a + 10, b, UnaryPredicate(30)); for (int *pi = b; pi < end; ++pi) cout << *pi << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf türünden nesne ile * gösterici operatörünü (indirection operator) kullanırsak bu durumda sınıfın operator * fonksiyonu çağrılmaktadır. Başka bir deyişle a bir sınıf türünden nesne olmak üzere *a işlemi ile a.operator *() çağrısı eşdeğerdir. * gösterici operatörü tek operand'lı (unary) bir operatör olduğu için bu operatör fonksiyonu üye operatör fonksiyonu olarak yazılacaksa operatör fonksiyonunun sıfır parametresi, global operatör fonksiyonu olarak yazılacaksa operatör fonksiyonunun bir parametresi olmak zorundadır. Pekiyi * gösterici operatörüne ilişkin operatör fonksiyonu ne ile geri dönmelidir? Bu operatör bir nesne ürettiğini göre fonksiyonun geri dönüş değeri de bir referans türünden olmalıdır. Çünkü bir fonksiyon çağrısının nesne belirtmesi için fonksiyonun geri dönüş değerinin referans olması gerekir. * gösterici operatörüne ilişkin operatör fonksiyonlarının yazılmasının amacı "bir sınıfın gösterici gibi davranmasını" sağlamaktır. Böyle sınıflara C++'ta "akıllı göstericler (smart pointers)" denilmektedir. Tabii sınıfın aynı zamanda "gösterdiği yer const olan bir const göstericiyi" temsil edebilmesi için sınıfa const bir * operatör fonksiyonun da eklenmesi gerekir. const * operatör fonksiyonunun geri dönüş değerinin const referans olması gerekir. Örneğin: class IntPointer { public: IntPointer(int *pi) : m_pi(pi) {} ~IntPointer() { delete m_pi; } int &operator *() { return *m_pi; } const int &operator *() const { return *m_pi; } private: int *m_pi; }; Burada IntPoiner sınıfının yapcı fonksiyonu bir nesnenin adresini almış ve o adresi sınıfın gösterici veri elemanında saklamıştır. Sonra da * operatörü uygulandığında bu operatmr fonksiyonu bu göstericinin gösteridği yerdeki nesneyi geri döndürmüştür. Sınıfı aşağıdaki gibi kullanabiliriz: IntPointer ip(new int(10)); cout << *ip << endl; // cout << ip.operator *() << endl; *ip = 20; // ip.operator *() = 20 cout << *ip << endl; // cout << ip.operator *() << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Akıllı göstericiler oluşturmak için gereken diğer bir operatör fonksiyonu da -> operatör fonksiyonudur. Her ne kadar -> operatörü iki operand'lı bir operatörse de sanki tek operand'lı bir operatörmüş gibi yazılmaktadır. -> operatörüne ilişkin operatör fonksiyonun üye operatör fonksiyonu oalrak yazılması zorunlu tutulmuştur. -> operatör fonksiyonunun sıfır parametresi olmak zorundadır. a bir sınıf türünden nesne belirtmek üzere a->b ifadesiin eşdeğeri şöyledir: a.operator ->()->b Burada bir noktaya dikkatini çekmek istiyoruz: a->b işleminde a.operator ->() ifdadesi yalnızca a-> işlemine karşı gelmektedir. O halde a->b ifadesinin eşdeğeri a.operator ->()->b biçiminde olacaktır. Bu durumda a->b ifadesinin geçerli olabilmesi için -> operatör fonksiyonunun bir sınıf nesnesinin adresiyle geri dönmesi o geri döndürülen sınıfın da b isimli bir elemanın olması gerekmektedir. Burada yine * operatöründeki gibi const bir göstericinin takilt edilmesi gerekiyorsa sınıfa const bir -> operatör fonksiyonu eklenmelidir. Tabii artık bu fonksiyonun geri döndürdüğü sınıf türünden adresin const olması gerekir. Örneğin: struct Sample { Sample(int x, int y, int z) { this->x = x; this->y = y; this->z = z; } int x, y, z; }; class SamplePointer { public: SamplePointer(Sample *ps) : m_ps(ps) {} ~SamplePointer() { delete m_ps; } Sample &operator *() { return *m_ps; } const Sample &operator *() const { return *m_ps; } Sample *operator ->() { return m_ps; } const Sample *operator ->() const { return m_ps; } private: Sample *m_ps; }; Burada SamplePointer sınıfı Sample sınıfı türünden bir göstericiyi temsil etmektedir. Dolayısıyla sınıfa * operatör fonksiyonunun yanı sıra -> operatör fonksiyonu da eklenmiştir. Sınıfın kullanımına şöyle bir örnek verilebilir: SamplePointer sp{new Sample(10, 20, 30)}; cout << sp->x << ", " << sp->y << ", " << sp->z << endl; Bu önekte sp->x gibi bir ifadenin sp.operator ->()->x ile eşdeğer olduğuna dikkat ediniz. Tabii sınıfın -> operatör fonksiyonu ancak sınııfn bir sınıf türünden göstericiyi taklit etmesi durumunda bulundurulabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; struct Sample { Sample(int x, int y, int z) { this->x = x; this->y = y; this->z = z; } int x, y, z; }; class SamplePointer { public: SamplePointer(Sample *ps) : m_ps(ps) {} ~SamplePointer() { delete m_ps; } Sample &operator *() { return *m_ps; } const Sample &operator *() const { return *m_ps; } Sample *operator ->() { return m_ps; } const Sample *operator ->() const { return m_ps; } private: Sample *m_ps; }; int main() { SamplePointer sp{new Sample(10, 20, 30)}; const SamplePointer csp{new Sample(100, 200, 300)}; cout << (*sp).x << ", " << (*sp).y << ", " << (*sp).z << endl; cout << sp->x << ", " << sp->y << ", " << sp->z << endl; sp->x = 40; // sp.operator ->()->x = 40 sp->y = 50; // sp.operator ->()->y = 50 sp->z = 60; // sp.operator ->()->z = 50 cout << sp->x << ", " << sp->y << ", " << sp->z << endl; cout << (*csp).x << ", " << (*csp).y << ", " << (*csp).z << endl; csp->x = 1000; // error return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir sınıfın bir göstericiyi taklit etmesinin nasıl bir faydası olabilir? Böyle bir durumun en bariz faydası "dinamik tahsisatlar için otomatik boşaltımın" sağlanabilmesidir. Yani bu sayede programcı dinamik olarak tahsis ettiği nesnenin adersini bir akıllı gösterici sınıfına verir. delete işlemi de sınıf nesnesi faaliyet alanını bitirdiğinde çağrılacak yıkıcı fonksiyon ile otomatik yapılır. Örneğin: //... { IntPointer ip(new int(10)); cout << *ip << endl; *ip = 20; cout << *ip << endl; } //.... Burada new ile tahsisatı rpogramcı yapmıştır. Ancak delete ile geri bırakma otomatik biçimde nesne faaliyet alanından çıktığında yapılacaktır. Burada size bir delete işlemi için bu kadar zahmete katlanmak saçma gelebilir. Ancak başka birtakım bağlamlarda bu otomatik boşaltım oldukça fayda sağlamaktadır. Örneğin birtakım dinamik tahsisatlar yapıldıktan sonra bir exception oluşursa bellek sızıntısı bu yolla engellenebilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 84. Ders 17/07/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte şablon tabanlı unique_ptr ismiyle genel bir akıllı gösterici (smart pointer) sınıfı standart kütüphaneye eklenmiştir. unique_ptr sınıfı (ismi üzerinde) bir nesneyi gösteren tek bir akıllı gösterici nesnesinin bulunmasını hedefleyen bir sınıftır. uniqe_ptr bir göstericiyi taklit eder. Ancak o göstericinin gösterdiği yeri gösteren başka biruniqe_ptr nesnesinin bulunamayacağını da garanti etmektedir. Başka bir deyişle hiçbir zaman iki unique_ptr nesnesi aslında aynı dinamik alanı gösteremez. unique_ptr sınıfı başlık dosyasında bildirilmiştir. unique_ptr şablon bir sınıf olduğu için bu sınıf türünden nesne yaratırken nesnenin taklit edeceği göstericinin türü açısal parantezler içerisinde belirtilmek zorundadır. uniqe_ptr nesnesi dinamik tahsis edilmiş bir nesnenin adresiyle yaratılmalıdır. Örneğin: unique_ptr pi(new int); unique_ptr ps(new string("ankara")); unique_ptr sınıfının default yapıcı fonksiyonu vardır. Bu fonksiyon tuttuğu göstericiye NULL adres yerleştirir. Dolayısıyla default yapıcı fonksiyon ile tanımlanmış olan nesne aslında hiçbir nesneyi göstermemektedir. Örneğin: unique_ptr uptr; unique_ptr sınıfının yapıcı fonksiyonu "explicit" olduğu için (explicit yapıcı fonksiyonlar ileride ele elınacaktır) biz nesneyi '=' ile llkdeğer vererek yaratamayız. Örneğin: unique_ptr pi = new int; // error! unique_ptr pi(new int); // geçerli unique_ptr pi{new int}; // geçerli Fonksiyon çağrısı sırasında argümanlardna parametre değişkenlerine yapılan atama ve return işlemi sırasında yapılan atama standartlara göre "copy initialization" kategorisinde olduğu için yine explicit yapıcı fonksiyonlar çağrılamayacaktır. Örneğin. unique_ptr foo() { return new int(10); // error! } unique_ptr sınıfının * ve -> operatörleri overload edilmiştir. Örneğin: unique_ptr us(new string("ankara")); string result; cout << *us << endl; result = us->substr(0, 3); cout << result; unique_ptr sınıfının yıkıcı fonksiyonu dinamik olarak tahsis edilmiş alanı delete etmektedir. Sınıfın kopya yapıcı fonksiyonu ve kopya operatör fonksiyonu "deleted" biçimdedir. Yani aşağıdaki gibi biz unique_ptr nesnesinin kopyasını çıkartamayız: unique_ptr us1(new int); // geçerli unique_ptr us2(ps1); // geçersiz! us2 = us1; // geçersiz! Ancak sınıfın taşıma yapıcı fonksiyonu ve taşıma atama operatör fonksiyonu bulunmaktadır. Bu fonksiyonlar kaynak nesnedeki adresi hedef nesneye yerleştirip kaynak nesnenin tuttuğu adresi NULL adres yapmaktadır. Örneğin: unique_ptr us1{new string("ankara")}; unique_ptr us2; us2 = move(us1); // geçerli! Burada us1nesnesinin gösterdiği string nesnesinin adresi us2 nesnesine yerleştirilmiştir. Bu işlemden sonra artık us1 nesnesi herhangi bir yeri göstermeyecektir. Sınıfın release isimli üye fonksiyonu nesnenin tuttuğu adresi geri dönüş değeri olarak verir. Ancak sınıfın içerisindeki göstericiye NULL adres atar. Yani release işleminden sonra artık nesne o dinamik nesneyi göstermez hale gelir. Örneğin: #include #include using namespace std; int main() { unique_ptr us(new string("ankara")); string *s; s = us.release(); // us artık dinamik nesneyi göstermiyor, s artık dinamik nesneyi gösteriyor cout << *s << endl; delete s; // delete etmek bizim sorumluluğumuzda return 0; } sınıfın reset üye fonksiyonu bizden yeni bir nesneyi parametre olarak alır. Eskisini delete eder. Artık nesne yeni dinamik nesneyi gösterir duruma gelir. Örneğin: #include #include using namespace std; int main() { unique_ptr us(new string("ankara")); us.reset(new string("izmir")); cout << *us << endl; return 0; } reset fonksiyonunu default argümanla çağırırsak nesnenin içerisindeki göstericiye null adres atanır. Tabii yine nesne daha önce gösterdiği dinamik nesneyi delete edecektir. Örneğin: unique_ptr us(new string("ankara")); us.reset(); // artık us bir nesneyi göstermiyor, eski alan delete edildi Nesneyi transfer etmek için release ile reset beraber kullanılmalıdır: unique_ptr us1(new string("ankara")); unique_ptr us2; us2.reset(us1.release()); // transfer etmenin normal yöntemi Tabii aslında bu işlem aşağıdakiyle işlevsel olarak tamamen eşdeğerdir: unique_ptr us1(new string("ankara")); unique_ptr us2; us2 = move(us1) // transfer etmenin normal yöntemi Sınıfın get üye fonksiyonu nesnenin tuttuğu adresi bize verir. get fonksiyonu sahipliği release gibi bırakmamaktadır. Dolayısıyla get kullanırken dikkat ediniz. Örneğin: #include #include using namespace std; int main() { unique_ptr us(new string("ankara")); string *s; s = us.get(); // us nesnenin adresini tutmaya devam ediyor cout << *us << endl; return 0; } Yukarıda da belirttiğimiz gibi unique_ptr sınıfının kopya yapıcı fonksiyonu yoktur ancak taşıma yapıcı fonksiyonu vardır. Benzer biçimde sınıfın kopya atama operatör fonksiyonu yoktur, ancak taşıma atama operatör fonksiyonu vardır. Örneğin: unique_ptr foo() { unique_ptr us{new string{"ankara"}}; return us; // geçerli, taşıma yapıcı fonksiyonu çağrılır } int main() { unique_ptr us; us = foo(); // geçerli, taşıma atama operatör fonksiyonu çağrılır cout << *us << endl; return 0; } Bu örnekte kopya yapıcı fonksiyonun ve kopya atama operatör fonksiyonunun değil, taşıma yapıcı fonksiyonunun ve taşıma atama operatör fonksiyonunun çağrıldığına dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- uniqe_ptr sınıfının "deleter" denilen ikinci bir şablon parametresi daha vardır. Deleter nesneyi yok ederken çağrılacak fonksiyonu belirtir. Bu şablon parametresi default durumda delete operatörü ile silme yapan bir sınıfı argüman olarak almıştır. Programcı isterse silme işlemini kendi belirlediği bir fonksiyonla ya da bir sınıfla yapabilir. Tabii böyle bir sınıf yazılırken sınıfın fonksiyon çağırma operatör fonksiyonunun yazılmış olması gerekir.Aşağıda deleter kullanımına ilişkin bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; void foo(int *pi) { cout << "foo called" << endl; delete pi; } int main() { unique_ptr a(new int, foo); *a = 10; cout << *a << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- unique_ptr nesnesini daha kolay bir biçimde yaratmak için make_unique isimli bir fonksiyon şablonu da bulundurulmuştur. Bu fonksiyon şablonu C++11 ile dile eklenen "variadic template" özelliği kullanılarak yazılmıştır. Bu fonksiyonun şablon parametresi yine unique_ptr nesnesinin tutacağı adresin türünü belirtmektedir. Fonksiyonun parametreleri ise doğrudan new operatörüne aktarılmaktadır. Yani yaratılacak unique_ptr nesnesinin türü T olmak üzere fonksyona geçirilen argümanlar doğrudan sanki new T(...) işleminde parantez içerine yazılacak argümanları belirtmektedir. Örneğin: unique_ptr us = make_unique(10, 'a'); Burada aslında make_unique fonksiyonu aşağıdaki ile eşdeğer işlemi yapmaktadır: unique_ptr us = unique_ptr(new string(10, 'a')); Bu işlemde bir noktaya dikkat ediniz: make_unique bir fonksiyon olduğu için fonksiyonların geri dönüş değerleri de eğer referans değilse prvalue olduğu için C++17 ve sonrasında zorunlu "copy elision" işlemi yapılacaktır. Dolayısıyla aslında us nesnesi için taşıma yapıcı fonksiyonu hiç çağrılmayacaktır. return ifadesindeki nesne doğrudan us üzerinde oluşturulacaktır. Başka bir deyişle C++17 ve sonrasında bu işlemin ekstra biz zaman kaybı yoktur. Tabii bu tür durumlarda programcılar auto tür belirleyicisi ile yazımı kısaltmaktadır: auto us = make_unique(10, 'a'); Tabii aşağıdaki gibi bir atama da geçerlidir: unique_ptr us{new string{"ankara"}}; cout << *us << endl; us = make_unique(10, 'a'); Burada make_unique öağrısının bir prvalue değeri oluşturduğuna dikkat ediniz. Dolayısıla buradaki atama işleminde kopya atama operatör fonksiyonu değil taşıma atama operatör fonksiyonu çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Diğer çok kullanılan bir akıllı gösterici sınıfı da shared_ptr isimli sınıftır. Bu sınıf da tıpkı unique_ptr sınıfı gibi şablon bir sınıftır. Dolayısıyla yine nesne yaratılırken adresi tutulacak olan nesnenin türününün açısal parantezler içerisinde belirtilmesi gerekir. Bu sınıf da başlık dosyasında bildirilmiştir. unique_ptr sınıfı gibi shared_ptr sınıfı da C++11 ile standart kütüphaneye eklenmiştir. shared_ptr sınıfı dinamik olarak yaratılmış bir nesnenin aynı anda birden fazla shared_ptr nesnesi tarafından gösterilebilmesini sağlamaktadır. Örneğin: shared_ptr ss1(new string{"ankara"}); shared_ptr ss2; ss2 = ss1; // unique_ptr'de geçersiz ancak shared_ptr'de geçerli cout << *ss1 << endl; // ankara cout << *ss2 << endl; // ankara (*ss1)[0] = 'x'; cout << *ss1 << endl; // xnkara cout << *ss2 << endl; // xnkara Bu örnekte ss1 nesnesi de ss2 nesnesi de dinamik olarak tahsis edilmiş olan aynı nesneyi göstermektedir. Anımsanacağı gibi unique_ptr sınıfında böyle bir durum oluşturulamamaktadır. Pekiyi akıllı göstericilerin en önemli kullanım gerekçeleri dinamik nesnenin otomatik delete edilmesiydi. Gerçekten de unique_ptr sınıfı kendi yıkıcı fonksiyonunda delete işlemini yapmaktadır. Pekiyi aynı dinamik nesneyi birden fazla shared_ptr nesnesi gösteriyorsa delete işlemi ne zaman ve nasıl yapılacaktır? İşte bu tür durumlarda otomatik silmenin sağlanması için "referans sayacı" tekniği kullanılmaktadır. Dinamik nesne için onu kaç shared_ptr nesnesinin gösterdiğini belirten bir referans sayacı oluşturulur. Bir shared_ptr nesnesi yaşamını kaybederken çağrılan yıkıcı fonksiyon bu referans sayacını bir eksiltir. Referans sayacı 0'a düştüğünde delete işlemi yapılır. Tabii kopya yapıcı fonksiyon ve kopya atama operatör fonksiyonlarında referans sayaçları artırılmaktadır. Ancak programcının bu ayrıntıları bilmesine gerek yoktur. Programcının tek bileceği şey dinamik tahsis edilen nesneyi gösteren hiçbir shared_ptr nesnesi kalmadığında o dinamik nesnenin otomatik olarak delete edileceğidir. Aslında bu referans sayacı tekniği bazı programlama dillerinde kullanılan "çöp toplama (garbage collection)" mekanizmaları tarafından da uygulanabilmektedir. Örneğin Python dilinde CPython yorumlayıcısı otomatik çöp toplama için böyle bir referans sayacı kullanmaktadır. Birden fazla sınıf nesnesinin "birleşme (aggregation)" yoluyla aynı nesneyi kullandığı durumlarda bu kullanılan nesnenin tam olarak programın neresinde silineceği programcıların kafasını karıştırabilmektedir. Böylesi durumlarda akla her zaman shared_ptr getirilmelidir. Çünkü shared_ptr sayesinde silme işlemi artık otomatikleştirilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- shared_ptr sınıfının unique_ptr sınıfına benzer elemanları vardır. Örneğin sınıfın get üye fonksiyonu yine sınıfın tuttuğu göstericiyi vermektedir. reset üye fonksiyonu yine nesnenin başka bir nesneyi göstermesini sağlamak için kullanılmaktadır. (Tabii bu durumda reset daha önce tuttuğu nesneyi hemen delete etmemekte yalnızca referans sayacını düşürmektedir.) Sınıfın unique isimli, üye fonksiyonu adresini tuttuğu nesnenin yalnızca kendisi tarafından gösterilip gösterilmediğini belirlemek için kullanılmaktadır. Ancak bu üye fonksiyon C++17'de deprecated yapılmış C++20'de kaldırılmıştır. Sınıfın use_count üye fonksiyonu nesnenin referans sayacaını bize vermektedir. Örneğin: shared_ptr ss1(new string{"ankara"}); shared_ptr ss2; cout << ss1.use_count() << endl; // 1 ss2 = ss1; cout << ss1.use_count() << endl; // 2 --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yinne shared_ptr sınıfı için de kolay nesne yaratmak için bir make_shared şablon fonksiyon bulundurulmuştur. make_shared fonksiyonu tamamen make_unique fonksiyonu gibi kullanılmaktadır. Ancak bu fonksiyon bir shared_ptr nesnesi oluşturmaktadır. Örneğin: auto ss1 = make_shared(10, 'a'); shared_ptr ss2 = ss1; cout << *ss1 << endl; cout << *ss2 << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; shared_ptr foo() { auto ss = make_shared("ankara"); return ss; } int main() { shared_ptr ss; ss = foo(); cout << ss.use_count() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz başından beri ekrana (yani stdout dosyasına) bir şeyler yazmak için cout değişkenini, klavyeden (yani stdin dosyasından) bir şeyler okuyabilmek için ise cin nesnesin kullandık. Pekiyi bu değişkenler nedir? İşte aslında cout değişkeni ostream denilen bir sınıf türünden, cin değişkeni ise istream denilen bir sınıf türünden global nesnelerdir. ostream sınıfı basic_ostream şablon sınıfının char açılımıdır. Benzer biçimde istream sınıfı da aslında basic_istream şablon sınıfının char için açılımıdır. Yani ostream ve istream aslında typedef isimleridir. Biz şablon sınıfları izleyen konularda ele alacağız. Bu nedenle bu sınıfların şablonluk özelliği üzerinde durmayacağız. C++'ın iostream sınıfları şablonluk özelliğini söz konusu etmezsek aşağıdaki biçimde bir türetme şemasına sahiptir. ios_base | | ios / \ / \ istream ostream \ / \ / \ / \ / iostream Buradan da görüldüğü gibi iostream sınıfı istream ve ostream sınıfından çoklu türetilmiştir. istream ve ostream sınıflarının ortak elemanları ios sınıfında toplanmıştır. basic_ios sınıfı da ios_base sınıfındn türetilmiştir. ostream sınıfının bir grup overload edilmiş << operatör fonksiyonu, istream sınıfının da overload edilmiş bir grup >> operatör fonksiyonu bulunmaktadır. cout nesnesi ostream sınıfı türünden olduğuna göre biz bu nesneyle yalnızca << operatör fonksiyonlarını, cin nesnesi de istream sınıfı türünden olduğuna göre biz bu nesneyle yalnızca >> operatör fonksiyonlarını kullanabiliriz. Şablonluk özelliği bir yana bırakılırsa bu sınıfların aşağıdaki gibi bir yapıya sahip olduğu söylenebilir: class ostream : public basic_ios { public: ostream &operator <<(int a); ostream &operator <<(long a); ostream &operator <<(double a); ostream &operator <<(char a); //... }; class istream : public basic_ios { public: istream &operator >>(int &a); istream &operator >>(long &a); istream &operator >>(double &a); istream &operator >>(char &a); //... }; Şimdi cout nesnesinin aşağıdaki gibi kullanıldığını düşünelim: cout << a << ", " << b << "\n"; Bunun eşdeğeri şöyledir: cout.operator <<(a).operator <<(", ").operator <<(b).operator <<("\n"); Bu operatör fonksiyonlarının nesnenin kendisine geri döndüğüne dikkat ediniz. Böylece örneğin cout.operator <<(a) gibi bir çağrıdan cout nesnesinin yine kendisi elde edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıda ostream sınıfının nasıl yazılmış olacbileceğine ilişkin bir örnek verilmiştir. Bu örneğin amacı ostream sınıfının orijinalini yazmak değildir. Yalnızca bu sınıfın nasıl yazılmış olabileceğine ilişkin bir ipucu vermektir: class myistream { public: myistream &operator >>(char &a); myistream &operator >>(int &a); myistream &operator >>(long &a); myistream &operator >>(double &a); myistream &operator >>(char *s); myistream &operator >>(string &s); //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class myostream { public: myostream &operator <<(char a); myostream &operator <<(int a); myostream &operator <<(long a); myostream &operator <<(double a); myostream &operator <<(const char *s); myostream &operator <<(string s); myostream &operator <<(void (*ps)(void)); //... }; void myendl() { printf("\n"); } myostream &myostream::operator <<(char a) { printf("%c", a); return *this; } myostream &myostream::operator <<(int a) { printf("%d", a); return *this; } myostream &myostream::operator <<(long a) { printf("%ld", a); return *this; } myostream &myostream::operator <<(double a) { printf("%f", a); return *this; } myostream &myostream::operator <<(const char *s) { printf("%s", s); return *this; } myostream &myostream::operator <<(string s) { printf("%s", s.c_str()); return *this; } myostream &myostream::operator <<(void (*ps)(void)) { ps(); return *this; } myostream mycout; int main() { int a = 123; mycout << "a = " << a << myendl; // mycout.op rator <<("a = ").operator <<(a).operator <<(myendl) return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Benzer biçimde aslında cin nesnesi de istream sınıfı türünden global bir nesnedir. istream sınıfının >> operatör fonksiyonları okuma işlemlerini yapmaktadır. Aşağıda bu sınıfın nasıl yazılmış olabileceğine ilişkin bir ipucu veriyoruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class myistream { public: myistream &operator >>(char &a); myistream &operator >>(int &a); myistream &operator >>(long &a); myistream &operator >>(double &a); myistream &operator >>(char *s); myistream &operator >>(string &s); //... }; myistream &myistream::operator >>(char &a) { scanf("%c", &a); return *this; } myistream &myistream::operator >>(int &a) { scanf("%d", &a); return *this; } myistream &myistream::operator >>(long &a) { scanf("%ld", &a); return *this; } myistream &myistream::operator >>(double &a) { scanf("%lf", &a); return *this; } myistream &myistream::operator >>(char *s) { scanf("%s", s); return *this; } myistream &myistream::operator >>(string &s) { char buf[4096]; scanf("%s", buf); s = buf; return *this; } myistream mycin; int main() { int a; double b; cout << "iki sayi giriniz:"; mycin >> a >> b; // mycin.operator >>(a).operator >>(b); cout << "a = " << a << ", b = " << b << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 85. Ders 22/07/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz şimdiye kadar bir sınıf yazdığımızda o sınıfın içerisindeki bilgileri ekrana (stdout dosyasına) yazdırmak için sınıfta disp isimli fonksiyonlar bulundurduk. Aslında C++'ta sınıf nesnesinin içerisindeki bilgilerin ekrana yazdırılması cout yoluyla yapılmalıdır. Çünkü o herkesin bildiği standart bir uygulamadır. Yani cout nesnesi ile biz nasıl int, long gibi temel türlere ilişkin bilgileri yazdırabiliyorsak kendi sınıfımıza ilişkin bilgileri yazdırabilmeliyiz. Kendi sınıflarımıza ilişkin nesneleri ekrana (stdout dosyasına) cout nesnesi yoluyla yazdırabilmemiz için mecburen bir global operatör fonksiyonu bulundırmamız gerekir. Örneğin: Sample s; cout << s; Burada cout << s işleminin iki eşdeğeri olabilir: cout.operator <<(s) ve operator <<(cout, s). Birinci eşdeğerliği biz sağlayamayız. Çünkü ostream sınıfına ekleme yapamayız. Ancak ikinci eşdeğerliği biz global operatör fonksiyonu ile sağlayabiliriz. Bu operatör fonksiyonun parametrik yapısı şöyle olacaktır: ostream &operator <<(ostream &os, const Sample &s); Görüldüğü gibi yazacağımız global operatör fonksiyonunun birinci parametresi ostream & türünden ikinci parametresi ise kendi sınıfımız türünden bir referans olmalıdır. Kombine edilebilirliği sağlamak için bu operatör fonksiyonun çağrısından bizim yine ostream nesnesinin kendisini elde etmemiz gerekir. Bu durumda fonksiyonun geri dönüş değeri ostream & olmalıdır. Biz de fonksiyondan birinci parametreyle geri dönebiliriz. Tabii bu global operatör fonksiyonun friend yapılması uygun olur. Çünkü bu fonksiyon sınıfın private veri elemanlarına erişmek isteyecektir. Özetle Smaple sınıfı için yazılacak global operatör fonksiyonu aşağıdaki gibi olabilir: class Sample { friend ostream &operator <<(ostream &os, const Sample &s); //... }; ostream &operator <<(ostream &os, const Sample &s) { //... return os; } Aşağıda Complex sınıfı için bu operatör fonksiyonun yazımını görüyorsunuz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // complex.hpp #ifndef COMPLEX_HPP_ #define COMPLEX_HPP_ #include namespace CSD { class Complex { public: Complex() = default; Complex(double real, double imag = 0); void disp() const; friend std::ostream &operator <<(std::ostream &os, const Complex &z); private: double m_real; double m_imag; }; } // complex.cpp #include #include #include "complex.hpp" using namespace std; namespace CSD { Complex::Complex(double real, double imag) : m_real{real}, m_imag{imag} {} ostream &operator <<(ostream &os, const Complex &z) { if (z.m_imag == 0) os << z.m_real; else { if (z.m_real != 0) { os << z.m_real; if (z.m_imag > 0) os << '+'; } if (abs(z.m_imag) != 1) os << z.m_imag; else if (z.m_imag < 0) os << '-'; os << 'i'; } return os; } } // app.cpp #include #include "complex.hpp" using namespace std; using namespace CSD; int main() { Complex z{3, 2}; cout << z << endl; // operator <<(cout, z) return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de String sınıfımız için bu operatör fonksiyonunu yazalım. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // string.hpp #ifndef STRING_HPP_ #define STRING_HPP_ #include #include namespace CSD { const int DEF_CAPACITY = 8; class String { public: using size_type = std::size_t; enum : size_type { npos = static_cast(- 1) }; // constructors String(); String(const char *str); String(size_type, char ch); String(const char *str, size_type n); String(const String &r); // copy constructor String(String &&r) noexcept; // move constructor ~String(); // getters size_type size() const { return m_size; } const char *c_str() const { return m_str; } size_type capacity() const { return m_capacity; } // utilities void reserve(size_type capacity); void append(char ch); void append(const char *str); void append(const char *str, size_type n); inline void append(String &r); bool insert(size_type index, size_type count, char ch); bool insert(size_type index, const char *str); bool insert(size_type index, const char *str, size_type count); inline bool insert(size_type index, String &r); bool erase(size_type index = 0, size_type count = npos); inline void clear(); void resize(size_type count); void resize(size_type count, char ch); void shrink_to_fit(); bool replace(size_type pos, size_type count, const char *str); bool replace(size_type pos, size_type count, String &s); size_type find(char ch, size_type pos = 0) const; size_type find(const char *str, size_type pos = 0) const; char &at(size_type pos) { return m_str[pos]; } char &front() { return m_str[0]; } char &back() { return m_str[m_size - 1]; } friend std::ostream &operator <<(std::ostream &os, const String &s); String &operator =(const String &r); String &operator =(String &&r) noexcept; private: char *m_str; size_type m_size; size_type m_capacity; }; inline void String::append(String &r) { append(r.m_str); } inline bool String::insert(size_type index, String &r) { return insert(index, r.m_str, r.m_size); } inline void String::clear() { m_str[0] = '\0'; m_size = 0; } } #endif // string.cpp #include #include #include "string.hpp" using namespace std; namespace CSD { String::String() { m_str = new char[DEF_CAPACITY]; m_str[0] = '\0'; m_size = 0; m_capacity = DEF_CAPACITY; } String::String(const char *str) { m_size = strlen(str); m_str = new char[m_size + DEF_CAPACITY]; strcpy(m_str, str); m_capacity = m_size + DEF_CAPACITY; } String::String(size_type n, char ch) { m_str = new char[n + DEF_CAPACITY]; m_str[n] = '\0'; memset(m_str, ch, n); m_size = n; m_capacity = n + DEF_CAPACITY; } String::String(const char *str, size_type n) { m_str = new char[n + DEF_CAPACITY]; strncpy(m_str, str, n); m_str[n] = '\0'; m_size = n; m_capacity = n + DEF_CAPACITY; } String::String(const String &r) { m_str = new char[r.m_size + DEF_CAPACITY]; strcpy(m_str, r.m_str); m_size = r.m_size; m_capacity = r.m_size + DEF_CAPACITY; } String::String(String &&r) noexcept { m_str = r.m_str; m_size = r.m_size; m_capacity = r.m_capacity; r.m_str = nullptr; } String::~String() { delete[] m_str; } void String::reserve(size_type capacity) { if (capacity <= m_capacity) return; char *newstr = new char[capacity]; strcpy(newstr, m_str); delete[] m_str; m_str = newstr; m_capacity = capacity; } void String::append(char ch) { size_type new_size = m_size + 1; if (new_size + 1 > m_capacity) reserve(new_size * 2); m_str[m_size++] = ch; m_str[m_size] = '\0'; } void String::append(const char *str) { size_type new_size = m_size + strlen(str); if (new_size + 1 > m_capacity) reserve(new_size * 2); strcat(m_str, str); m_size = new_size; } void String::append(const char *str, size_type n) { size_type new_size = m_size + n; if (new_size + 1 > m_capacity) reserve(new_size * 2); strncat(m_str, str, n); m_size = new_size; } bool String::insert(size_type index, size_type count, char ch) { if (index > m_size) return false; size_type new_size = m_size + count; if (new_size + 1 > m_capacity) reserve(new_size * 2); memmove(m_str + index + count, m_str + index, m_size - index); memset(m_str + index, ch, count); m_size = m_size + count; m_str[m_size] = '\0'; return true; } bool String::insert(size_type index, const char *str) { if (index > m_size) return false; size_type len_str = strlen(str); size_type new_size = m_size + len_str; if (new_size + 1 > m_capacity) reserve(new_size * 2); memmove(m_str + index + len_str, m_str + index, m_size - index); memcpy(m_str + index, str, len_str); m_size = m_size + len_str; m_str[m_size] = '\0'; return true; } bool String::insert(size_type index, const char *str, size_type count) { if (index > m_size) return false; size_type new_size = m_size + count; if (new_size + 1 > m_capacity) reserve(new_size * 2); memmove(m_str + index + count, m_str + index, m_size - index); memcpy(m_str + index, str, count); m_size = m_size + count; m_str[m_size] = '\0'; return true; } bool String::erase(size_type index, size_type count) { if (index > m_size) return false; if (count == npos) count = m_size - index; memmove(m_str + index, m_str + index + count, count); m_size = m_size - count; m_str[m_size] = '\0'; return true; } void String::resize(size_type count) { if (count > m_capacity) reserve(count + DEF_CAPACITY); if (count > m_size) memset(m_str + m_size, 0, count - m_size); m_size = count; m_str[m_size] = '\0'; } void String::resize(size_type count, char ch) { if (count > m_capacity) reserve(count + DEF_CAPACITY); if (count > m_size) memset(m_str + m_size, ch, count - m_size); m_size = count; m_str[m_size] = '\0'; } void String::shrink_to_fit() { char *newstr = new char[m_size + 1]; strcpy(newstr, m_str); delete[] m_str; m_str = newstr; m_capacity = m_size + 1; } bool String::replace(size_type pos, size_type count, const char *str) { if (!erase(pos, count)) return false; return insert(pos, str); } bool String::replace(size_type pos, size_type count, String &s) { if (!erase(pos, count)) return false; return insert(pos, s); } String::size_type String::find(char ch, size_type pos) const { for (size_type i = pos; i < m_size; ++i) if (m_str[i] == ch) return i; return npos; } String::size_type String::find(const char *str, size_type pos) const { char *result; if ((result = strstr(m_str + pos, str)) == nullptr) return npos; return static_cast(result - m_str); } ostream &operator <<(ostream &os, const String &s) { return os << '"' << s.m_str << '"' << ", size = " << s.m_size << ", capacity = " << s.m_capacity << endl; } String &String::operator =(const String &r) { if (this == &r) return *this; delete[] m_str; m_str = new char[r.m_capacity]; strcpy(m_str, r.m_str); m_size = r.m_size; m_capacity = r.m_capacity; return *this; } String &String::operator =(String &&r) noexcept { if (this == &r) return *this; swap(m_str, r.m_str); swap(m_size, r.m_size); swap(m_capacity, r.m_capacity); return *this; } } // app.cpp #include #include "string.hpp" using namespace std; using namespace CSD; int main() { String s{"ankara"}; cout << s << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Kendi sınıflarımızın cin ile okumasını sağlamak çoğu kez mümkün olmaz. Çünkü sınıf nesneleri bileşik türlerdir. Bunlar için değerlerin basit bir biçimde klavyeden (stdin dosyasından) alınması mümkün olmayabilir. Ancak biz yine de burada Sample sınıfı için bunun nasıl yapılacağının ipucunu vermek istiyoruz. class Sample { friend istream &operator >>(istream &is, Sample &r); //... }; istream &operator >>(istream &is, Sample &r) { //... return is; } Şimdi Complex sayı sınıfı için cin nesnesi ile okumaya basit bir örnek verelim: class Complex { public: Complex() = default; Complex(double real, double imag = 0); void disp() const; friend std::ostream &operator <<(std::ostream &os, const Complex &z); friend std::istream &operator >>(std::istream &is, Complex &z); private: double m_real; double m_imag; }; Yukarıda da belirttiğimiz gibi her türlü sınıf için okuma işlemi anlamlı olmayabilir. Okuma sırasında bir parse işleminin yapılması gerekebilir. Örneğin Complex sayı için aşağıdaki okumaların hepsini geçerli kabul edecekseniz pek çok kontrolü yapmaız gerekir: 2i+5 5 -5 i -3i +7 Bu tür karmaşık parse işlemleri için özel kütüphanelerden faydalanılabilmektedir. Fakat aşağıdaki örnekte okumayı gerçek ve sanal kısımların arasına boşluk karakterleri getirerek basit bir biçimde yapıyoruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // complex.hpp #ifndef COMPLEX_HPP_ #define COMPLEX_HPP_ #include #include namespace CSD { class Complex { public: Complex() = default; Complex(double real, double imag = 0); void disp() const; friend std::ostream &operator <<(std::ostream &os, const Complex &z); friend std::istream &operator >>(std::istream &is, Complex &z); private: double m_real; double m_imag; }; } #endif // complex.cpp #include #include #include "complex.hpp" using namespace std; namespace CSD { Complex::Complex(double real, double imag) : m_real{real}, m_imag{imag} {} ostream &operator <<(ostream &os, const Complex &z) { if (z.m_imag == 0) os << z.m_real; else { if (z.m_real != 0) { os << z.m_real; if (z.m_imag > 0) os << '+'; } if (abs(z.m_imag) != 1) os << z.m_imag; else if (z.m_imag < 0) os << '-'; os << 'i'; } return os; } istream &operator >>(istream &is, Complex &z) { return is >> z.m_real >> z.m_imag; } } // app.cpp #include #include "complex.hpp" using namespace std; using namespace CSD; int main() { Complex z; cin >> z; cout << z << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new ve delete operatörlerinde de bizim istediğimiz bir fonksiyonun çağrılmasını sağlayabiliriz. Böylece programcı tahsisat işlemlerinde araya girebilir. Ya da tahsisat işlemlerinin kendi istediği gibi yapılmasını sağlayabilir. Öncelikle new ve delete işlemlerinde aslında derleyicinin ne yaptığına bakalım. Aslında new ve delete işlemleri sırasında derleyici new ve delete işlemlerini yapan operatör fonksiyonlarını çağırmaktadır. Örneğin: pi = new int; Burada 1 int uzunluğunda dinamik bir alan tahsis edilmiştir. Derleyici bu new işlemini operator new isimli bir operatör fonksiyonunu çağırarak yapmaktadır. Yani yukarıdaki tahsisatın eşdeğeri aslında şöyledir: pi = (int *) operator new(sizeof(int)); Şimdi tahsisatı şöyle yapmış olalım: pi = new int[n]; Köşeli parantezli tahsisat yapıldığında derleyici operator new[] isimli operatör fonksiyonunu çağırmaktadır. Yani yukarıdaki işlemin eşdeğeri şöyledir: pi = (int *) operator new[](sizeof(int) * n); new işlemi yaptığımızda çağrılan operator new ya da operator new[] operatör fonksiyonları zaten standart kütüphane içerisinde bulunmaktadır. Görüldüğü gibi new bir operatör olsa da aslında tahsisat yine malloc gibi bir fonksiyon yoluyla yapılmaktadır. Aynı durum delete işlemi için de geçerlidir. Örneğin: delete pi; Derleyici köşeli parantezsiz delete işlemi sırasında operator delete isimli bir fonksiyonu çağırmaktadır. Yukarıdaki işlemin eşdeğeri şöyledir: operator delete(pi); delete işlemini şöyle yapmış olalım: delete[] pi; Köşeli parantezli delete işleminde de derleyici operator delete[] fonksiyonunu çağırmaktadır. Yani yukarıdaki işlemin eşdeğeri şöyledir. operator delete[](pi); operator delete ve operator delete[] fonksiyonları da yine standart kütüphanede yazılmış biçimde bulunmaktadır. Görüldüğü gibi aslında delete operatörü kullıldığında boşaltma işlemi tıpkı free gibi bir fonksiyonla yapılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new operatör fonksiyonlarının paramtrik yapısı aşağıdaki gibi olmalıdır: void *operator new(size_t size); void *operator new[](size_t size); Fonksiyonun normal ve köşeli parantezli biçimleri olduğunda dikkat ediniz. Normal olarak operator new fonksiyonları tahsisat başarısızsa bad_alloc isimli bir sınıfla exception fırtalmalıdır. delete işleminde çağrılacak operator delete fonksiyonun da parametrik yapısı şöyle olmalıdır: void operator delete(void *ptr) noexcept; void operator delete[](void *ptr) noexcept; Yukarıda da belirttiğimiz gibi standart kütüphanede new ve delete işlemleri sırasında çağırlan operator new ve operator delete fonksiyonları zaten bulunmaktadır. Ancak eğer programcı kendisi bu fonksiyonları kendisi yazarsa kütüphanedekiler değil de programcının yazdığı fonksiyonlar devreye girer. Eğer programcı bunları yazmasa kütüphanedekiler devreye girmektedir. operator delete fonksiyonlarındaki noexcept belirlyeicisi fonksiyonların exception fırlatmayacağı anlamına gelmektedir. noexcept belirleyicisi C++11 ile eklenmiştir. delete operatör fonksiyonlarında bu belirleyici kullanılmazsa bir sorun oluşmaz. new ve delete operatör fonksiyonları herhangi bir isim alanında bulunabilir. Arama niteliksiz isim arama kurallarına göre yapılmaktadır. Pekiyi programcı neden kendisi new ve deşete işlemlerinde çağrılacak operatör fonksiyonlarını yazmak istesin? Aslında programcıların bu operatör fonksiyonlarını kendilerinin yeniden yazması genellikle karşılaşılan bir durum değildir. Ancak özel bazı durumlarda programcı tahsisatın özel bir heap sisteminde yapılmasını isteyebilir. Aşağıda operator new ve operator delete fonksiyonlarının kullanımına bir örnek verilmiştir. Buradaki operatör fonksiyonları tahsisat işlemleri için standart C kütüphanesinin malloc ve free fonksiyonlarını çağırmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; void *operator new(size_t size) { void *ptr; if ((ptr = malloc(size)) == nullptr) throw bad_alloc(); return ptr; } void *operator new[](size_t size) { void *ptr; if ((ptr = malloc(size)) == nullptr) throw bad_alloc(); return ptr; } void operator delete(void *ptr) noexcept { free(ptr); } void operator delete[](void *ptr) noexcept { free(ptr); } int main() { int *pi; pi = new int[5] {1, 2, 3, 4, 5}; for (int i = 0; i < 5; ++i) cout << pi[i] << " "; cout << endl, delete[] pi; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------ new operatörlerinin "placement" denilen verisyonları da vardır. new operatörünün placement versiyonunda fonksiyon ek bir gösterici parametresi de alır. Bunların parametrik yapıları şöyledir: void *operator new(size_t size, void *ptr); void *operator new[](size_t size, void *ptr); Bu placement versiyonları kullanmak için new anahtar sözcüğünden sonra parabtezler içerisinde fonksiyonların ikinci parametrelerine geçirilecek adresler girilmelidir. Örneğin: pi = new(ptr) int; // new operatörünün placement versiyonu pi = new(ptr) int[n]; // new operatörünün placement versiyonu Standart kütüphanede bu placement versiyonlar da bulunmaktadır. Ancak bu placement versiyonlar aslında bir şey yapmayıp ikinci parametresiyle belirtilen adresle geri dönmektedir. Yani bu fonksiyonlar aşağıdaki gibi yazılmıştır: void *operator new(size_t size, void *ptr) { return ptr; } void *operator new[](size_t size, void *ptr) { return ptr; } Bu fonksiyonlarda NULL adres kontrolünün yapılıp bad_alloc ile exception fırlatılması gerekmemektedir. Standartlar bu placement evrsiyonların NULL adresle çağrılmasının tanımsız davranışa yol açtığını söylemektedir. new operatör fonksiyonlarının kütüphanedeki placement versiyonlarının çağrılması için başlık dosyasının include edilmesi gerekmektedir. (Normal versiyonlar için bu gerekmemektedir.) Pekiyi placement operator new fonksiyonunun parantez içerisinde verilen adresle geri dönmesinin ne anlamı olabilir? İşte new operatör fonksiyonlarının placement versiyonları aslında nesnelerin önceden tahsis edilmiş spesifik yerlerde yaratılmasını sağlamak için kullanılmaktadır. Böylesi durumlara bazen gereksinim duyulmaktadır. Örneğin: char buf[sizeof(Sample)]; Sample *ps; ps = new(buf)Sample{10, 20}; Burada buf yerel bir dizidir. new operatörünün placement biçimi kullanılmıştır. Bu durumda bu placement versiyon buf adresinin kendisine geri döncektir. Böylece yapıcı fonksiyona this göstericisi oalrak bu buf adresi geçirilecektir. Bu da nesnenin aslında buf içerisinde yaratılacağı anlamına gelir. Tabii bu örnekte bizim ps göstericisinin gösterdiği yeri delete operatörü ile aşağıdaki gibi delete etmememiz gerekir: delete ps; // dikkat! tanımsız davramış Heap'te new operatörü ile tahsis edilmeyen alanın delete operatörü ile serbest bırakılması tanımsız davranışa yol açacaktır. Pekiyi ya sınıfın yıkıcı fonksiyonu varsa ve biz onun çağrılmasını da istiyorsak ne yapmalıyız? delete operatörünün yıkıcı fonksiyonu çağırdığını anımsayınız. Biz delete operatör operatörünü kullanmayacağımıza göre böylesi bir durumda yıkıcı fonksiyon nasıl çağrılacaktır? Anımsanacağı gibi sınıfın yıkıcı fonksiyonları normal bir üye fonksiyon gibi çağrılabilmektedir. Dolayısıyla bu tür durumlarda yıkıcı fonksiyonu manuel bir biçimde çağırmak geekir. Örneğin: char buf[sizeof(Sample)]; Sample *ps; ps = new(buf)Sample{10, 20}; //.... ps->~Sample(); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} Sample() { cout << "destructor" << endl; } friend ostream &operator <<(ostream &os, const Sample &s); private: int m_a; int m_b; }; ostream &operator <<(ostream &os, const Sample &s) { os << s.m_a << ", " << s.m_b; return os; } int main() { char buf[sizeof(Sample)]; Sample *ps; ps = new(buf)Sample{10, 20}; cout << *ps << endl; ps->~Sample(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Mademki aslında new operatörü ile tahsisatlar aslında operator new fonksiyonlarıyla yapılmaktadır. O halde programcı operator new fonksiyonlarını tahsisat fonksiyonları olarak da doğrudan çağırabilir. Aynı durum operator delete operatörü için de geçerlidir. için de geçerlidir. Örneğin: int *pi; pi = static_cast(operator new[](sizeof(int) * 10)); //... operator delete[](pi); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { int *pi; pi = static_cast(operator new[](sizeof(int) * 10)); for (int i = 0; i < 10; ++i) pi[i] = i; for (int i = 0; i < 10; ++i) cout << pi[i] << endl; operator delete[](pi); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- T sınıfı türünden new operatöryle dinamik bir tahsisat yapıyor olalım: T *pt; pt = new T(...); Aslında bu işlemin eşdeğerini placment new operatöryle şöyle de oluşturabiliriz: T *pt; pt = static_cast(operator new(sizeof(T))); pt = new (pt) T(...); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} Sample() { cout << "destructor" << endl; } friend ostream &operator <<(ostream &os, const Sample &s); private: int m_a; int m_b; }; ostream &operator <<(ostream &os, const Sample &s) { os << s.m_a << ", " << s.m_b; return os; } int main() { Sample *ps; ps = static_cast(operator new(sizeof(Sample))); ps = new(ps)Sample(10, 20); cout << *ps << endl; ps->~Sample(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new ve delete operatörlerine ilişkin operatör fonksiyonları sınıfınm static üye fonksiyonu olarak da yazılabilmektedir. Böylece o sınıf türünden tahsisatlarda bu fonksiyonlar kullanılır. Örneğin: class Sample { public: static void *operator new(size_t size); void operator delete(void *ptr); //... private: int m_a; int m_b; }; Burada new ve delete operatörleri yalnızca Sample türünden tahsisatlarda kullanılacaktır. Diğer tahsisatlarda kütüphanede bulunan operator new ve operator delete fonksiyonları çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; void *operator new(size_t size) noexcept { cout << "global operator new" << endl; return malloc(size); } void operator delete(void *ptr) noexcept { cout << "global operator new" << endl; free(ptr); } class Sample { public: static void *operator new(size_t size); void operator delete(void *ptr); private: int m_a; int m_b; }; class Mample { //... private: int m_a; int m_b; }; void *Sample::operator new(size_t size) { cout << "static member operator new" << endl; return malloc(size); } void Sample::operator delete(void *ptr) noexcept { cout << "static member operator delete" << endl; free(ptr); } int main() { Sample *ps; Mample *pm; ps = new Sample(); pm = new Mample(); delete ps; delete pm; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- new operatör fonksiyonunun başarısızlık durumunda exception fırlatmayan tıpkı malloc fonksiyonunda olduğu gibi NULL adresle geri dönen biçimleri de vardır. Bunlara new operatör fonksiyonlarının nothrow versiyonları denilmektedir. Bu versiyonlar placement sentaksıyla kullanılmaktadır ve nothrow_t türünden bir ekstra parametreye sahiptir. nothrow_t türünden nothrow isimli global bir nesne vardır. (Bu nesnenin bildirimi dosyası içerisindedir.) Fonksiyonların parametrik yapısı şöyledir: void *operator new ( std::size_t count, const std::nothrow_t& tag ) noexcept; void *operator new[]( std::size_t count, const std::nothrow_t& tag ) noexcept; Bu new operatör fonksiyonlarının nothrow'lu versiyonları new operatöründe placement sentaksıyla new anahtar sözcüğünden sonra parantezler içerisinde nothrow yazarak kullanılmaktadır. Örneğin: pi = new (nothrow) int[size]; if (pi == nullptr { // hata ele alımı } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 86. Ders 24/07/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tür dönüştürmeleri açıkça (explicit) ya da otomatik olarak (implicit) biçimde yapılabilmektedir. C++ standartlarında kaynak türden hedef türe otomatik tür dönüştürmelerine "implicit conversion" denilmektedir. Otomatik tür dönüştürmeleri açık ya da örtük atama durumlarında derleyici tarafından yapılmaktadır. Böylesi açık ya da örtük atama durumlarında derleyici kaynak türü hedef türe dönüştürmeye çalışmaktadır. Standartlarda derleyicinin otomatik olarak hedef türü kaynak türe dönüştümesi için yaptığı işlemlere "implicit conversion sequence" denilmektedir. Otomatik dönüştürmeler şu durumlarda devreye girmektedir: 1) Açıkça ilkdeğer verme işlemlerinde 2) Fonksiyon çağırma sırasında argüman parametre atamalarında 3) return işlemlerinde 4) Exception throw işlemlerinde ve exception yakalama işlemlerinde 5) Açıkça = operatörü ile atama işlemlerinde Tabii aslında standartlara göre fonksiyon çağırması, return işlemi, exception throw işlemi ve exception yakalama işlemleri de ilkdeğer verme (initialization) anlamına gelmektedir. Bu tür dolaylı atamalara standartlarda "copy initialization" denilmektedir. Standartlara göre otomatik dönüştürme işlemleri (implicit conversion sequences) şunlardan biri biçiminde olabilir: 1) Standart Dönüştürme İşlemi (Standard Conversion Sequence) 2) Kullanıcı Tanımlı Dönüştürme İşlemi (User Defined Conversion Sequence) 3) Üç Noktalı Dönüştürme İşlemi (Ellipsis Conversion Sequence) Standart döönüştürme işlemi C'den de bildiğimiz temel türlerinm birbirlerine dönüştürülmesi ile ilgilidir. Üç noktalı dönüştürme fonksiyonun son parametresi ... iken onun için girilen argümanlara ilişkin dönüştürmedir. Bu durumla çok seyrek karşılaşılmaktadır. C++'ta şu dönüştürmelere kullanıcı tanımlı dönüştürmeler (user defined conversions) denilmektedir: 1) Sınıflardan temel türlere yapılan dönüştürmeler. Örneğin: 2) Temel türlerden sınıflara yapılan dönüştürmeler. Örneğin: 3) Bir sınıf türündne diğer bir sınıf türüne yapılan dönüştürmeler C++'ta ""kullanıcı tanımlı dönüştürmeler (user defined conversion)" "iki biçimde yapılabilmektedir: 1) Yapıcı fonksiyonun çağrılarak geçici sınıf nesnesinin oluşturulması yoluyla 2) Sınıf nesnesinin tür dönüştürme operatör fonksiyonlarıyla dönüştürülmesi yoluyla Birinci çeşit dönüştürmede kullanılan yapıcı fonksiyonlara standartlarda "converting constructor" denilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++ standartlarına göre bir sınıf nesnesi {...} ya da (...) ile tanımlanırsa buna ""doğrudan ilkdeğer verme (direct initialization)" denilmektedir. Ancak sınıf nesnesine = operatörü ile ilkdeğer verilirse ya da fonksiyon çağırma sırasında parametre değişkenlerine değer atanıyorsa ya da return ifadesiyle geçici nesneye değer atanıyorsa buna da "kopya ilkdeğer vermesi (copy initialization)" denilmektedir. Örneğin Number isimli aşağıdaki gibi bir sınıfımız olsun: class Number { public: Number() = default; Number(int val) : m_val(val) {} Number operator +(const Number &x) const; friend ostream &operator <<(ostream &os, const Number &s); private: int m_val; }; Burada aşağıdaki gibi bir tanımlamalar yapalım: Number x{10}; // direct initialization Number y = Number(10); // copy initialization Number z = 10; // copy initialization Burada ilk tanımalada verilen ilkdeğer "doğrudan ilkdeğer verme (direct initialization)" diğerlerinde ise "kopya ilkdeğer vermesi (copy initialization" söz konusudur. Standartlara göre doğrudan ilkdeğer verme durumlarında her zaman sınıfın en ygun yapıcı fonksiyonu tespit eidlip çağrılmaktadır. Yine C++'ta kopya ilkdeğer vermesinde eğer kaynak tür ve hedef tür aynı sınıf türündense (ikinci ilkdeğer verme örneği) yine sınıfın en uygun yapıcı fonksiyonu (bu kopya yapıcı fonksşyonlarından biri olacaktır) çağrılmaktadır. Ancak kopya ilkdeğer vermesinde kaynak türle hedef tür aynı tür değilse (üçüncü ilkdeğer vemr örneği) bu durumda önce ilkdeğer olarak verilen ifade ilgili sınıfın bir yapıcı fonksiyonu yoluyla o sınıf türüne dönüştürülür, sonra elde edilen geçici sınıf nesnesi nesnesi sanki doğrudan ilkdeğer veriliyormuş gibi hedef nesne için kullanılmaktadır. Yani örneğin: Number z = 10; Bu işlem C++ standartlarına göre aşağıdaki işlemle eşdeğerdir: Number z{Number(10)}; Tabii burada artık C++17 ile birlikte "copy elision" zorunlu hale geldiği için sonuçta C++17 sonrasında aşağıdaki ifade eşdeğer biçime gelmiştir: Number z = 10; Number z{10}; Yani Number z = 10 gibi bir işelm eskiden "önce 10'u Number türüne dönüştür, sonra Number sınıfının kopya yapıcı fonksiyonu yoluyla z nesnesine ilkdeğer ver" anlamına gelirken artık "copy elision" bu tür durumlarda zorunlu olduğu için Number z{10} ile eşdeğer hale gelmiştir. Aşağıdaki gibi bir foo fonksiyonunu çağırmaış olalım: void foo(Number x) { cout << x << endl; } //... foo(10); Burada 10 derleyici tarafından Number türüne dönüştürülecek sonra da x nesnesi initializae edilecektir. Standartlara göre bu işlemin eşdeğeri şöyledir: void foo(Number x) { cout << x << endl; } //... foo(Number(10)), Yine C++17 sonrasında bu durumda "copy elision" zorunlu olduğu için burada x parametre değişkeni doğrudan int parametreli yapıcı fonksiyon ile initialize edilecektir. Ancak eskiden önce geçici Number nesnesi yaratılıp sonra parametre değişkeni olan x için kopya yapıcı fonksiyonu çağrılıyordu. Buradan çıkacak sonuç şudur: Ne zaman biz bir sınıf türünden nesneye farklı türdne (örneğin türlerden) bir ifade ile atama yapmaya çalışsak ya da ilkdeğer vermeye çalışsak bu durumda derleyici önce bu ifadeyi hedef türe ilişkin sınıfın en uygun yapıcı fonksiyonu ile hedef türe dönüştürür sonra atamayı ya da ilkdeğerlemeyi yapar. C++17 ve sonrasında geçici sınıf nesneleri ile nesne yaratılırken "copy elision" zorunlu hale getirilmiştir. İşte bir ifadenin hedef sınıfın yapıcı fonksiyonu yoluyla sınıf türüne dönüştürülmesi işlemi "kullanıcı tanımlı dönüştürme (user defined conversion)" durumundadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi bir referansa ilkdeğer verilirken eğer verilen ilkdeğer referans ile aynı türden değilse ya referansın const olması ya da referansın sağ taraf değeri referansı olması gerekiyordu. Bu durumda referans türünden geçici bir nesne yaratılıyor ve bu geçici nesne referansa bind ediliyordu. Örneğin: const int &r = 10.2; Burada derleyici int türden bir geçici nesne yaratıp referansa o geçici nesneyi bind edecektir. İşte eğer referans bir sınıf türündense ve verilen ilkdeğer aynı sınıf türünden bir sol taraf değeri değilse derleyici yine kullanıcı tanımlı tür dönüştürmesi ile sınıfın en uygun yapıcı fonksiyonunu kullanarak verilen ilkdeğeri sınıf türüne dönüştürür. Buradan geçici bir nesne elde eder ve referansı bu geçici nesneye bind eder. Örneğin. const Number &x = 10; Burada derleyici önce 10 değerini Number sınıfına Number sınıfının en uygun yapıcı fonksiyonunu çağırarak dönüştürür, dönüştürme sonucunda elde edilen nesneyi de referansa bind eder. Yani bu işlemin eşdeğeri şöyledir: const Number &x = Number(10); Tabii burada artık ilkdeğer vermenin doğrudan yapıması ile kopya ilkdeğer vermesi arasında bir fark yoktur. Yani aşağıdaki ile yukarıdaki ildeğer vermeler eşdeğerdir: const Number &x{10}; Tabii aynı durum sağ taraf değeri referansları için de söz konusudur: Number &&r = 10; Aşağdıaki gibi bir ifade söz konusu olsun: Number result, x{10}; result = x + 20; // result = x.operator =(20) Number sınıfının iki Number nesnesini toplaayan + operatör fonksiyonu vardır. Bir Number ile bir int değeri toplayan bir operatör fonksiyonu yoktur. O halde bu x + 20 işlemi geçerli midir? x + 20 işleminin eşdeğeri x.operator +(20) biçimindedir. Buradaki 20 değeri + operatör fonksiyonun referams parametresine atanacaktır. İşte burada yukarıdaki örnekteki gibi bir durum oluşacak 20 değeri Number türüne dönüştürülüp referansa bind edilecektir. Yani işlem geçerlidir. Aşağıda çeşitli denemeler yapabilmeniz için örnek bir sınıf oluşturulmuştur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Number { public: Number() = default; Number(int val); ~Number(); // gereksiz, deneme amaçlı Number(const Number &r); // gereksiz, deneme amaçlı Number(const Number &&r); // gereksiz, deneme amaçlı Number operator =(const Number &r); // gereksiz, deneme amaçlı Number operator +(const Number &x) const; friend ostream &operator <<(ostream &os, const Number &s); private: int m_val; }; Number::Number(int val) { cout << "int constructor" << endl; m_val = val; } Number::~Number() { cout << "destructor" << endl; } Number::Number(const Number &r) { cout << "copy constructor" << endl; m_val = r.m_val; } Number::Number(const Number &&r) { cout << "move constructor" << endl; m_val = r.m_val; } Number Number::operator =(const Number &r) { m_val = r.m_val; cout << "copy assignment operator" << endl; return *this; } Number Number::operator +(const Number &x) const { Number result; result.m_val = m_val + x.m_val; return result; } ostream &operator <<(ostream &os, const Number &s) { return os << s.m_val; } int main() { Number result, x{10}; result = x + 20; // result = x.operator +(20); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Kullanıcı tanımlı tür dönüştürmesinin ikinci biçimi "tür dönüştürme operatör fonksiyonları yoluyla" yapılan dönüştürmelerdir. Tür dönüştürme operatör fonksiyonları bir sınıf türünden nesneyi temel türlere ya da başka sınıf türlerine dönüştürmek için kullanılmaktadır. Tür dönüştürme operatör fonksiyonlarının genel biçimi şöyledir: operator (); Tür dönüştürme operatör fonksiyonlarında geri dönüş değerinin türü belirtilmez zaten operatör anahtar sözcüğünün sağındaki tür aynı zamanda fonksiyonun geri dönüş değerinin türüdür Tür dönüştürme operatör fonksiyonları üye fonksiyon olarak yazılmak zorundadır ve parametreye sahip olamazlar. Tür dönüştürme operatör fonksiyonlarının const üye fonksiyon yapılması iyi bir tekniktir. Örneğin: class Number { public: Number() = default; Number(int val); Number operator +(const Number &x) const; operator int() const { return m_val; } friend ostream &operator <<(ostream &os, const Number &s); private: int m_val; }; Burada operator int fonksiyonu Number türünden nesneyi int türüne dönüştürme iddiasındadır. Örneğin: Number x{10}; int val; val = x; // val = x.operator int() Burada val = x işleminde derleyici x değerini int türüne dönüştürmeye çalışacal bunun için Number sınıfının tür dönüştürme operatör fonksiyonunu kullanacaktır. Tabii tür dönüştürme operatör fonksiyonları da nomal bir fonksiyon gibi çağrılabilir. Örneğin: val = x.operator int(); Görüldüğü gibi bu sayede int gereken her yerde biz Number nesnesini de kullanabiliriz. Aşağıdaki örneğe dikkat ediniz: double result; Number x{10}; result = 3.14 + x; Burada result = 3.14 + x işlemi ya bir global operatör fonksiyonu yoluyla (böyle bir fonksiyon yazmadık) ya da Number nesnesinin int türüne dönüştürülmesi yoluyla yapılacaktır. Gerçekten de burada x nesnesi önce operator int fonksiyonu yoluyla int türüne dönüştürülür sonra double değer ile int değer toplanır. Buradaki alternatif fonksiyonlar arasında "overload resolution" işlemlerini izleyen paragraflarda ele alacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Tür dönüştürme operatör fonksiyonlarına C++11 ile birlikte "explicit" özelliğ de eklenmiştir (bu özellik C#'ta başından beri vardı). Eğer tür dönüştürme operatör fonksiyonunun başına explicit anahtar sözcüğü getirilirse bu dönüştürme fonksiyonu yalnızca açıkça tür dönüştürme operatörü yoluyla çağrılmaktadır. Örneğin: class Number { public: Number() = default; Number(int val); Number operator +(const Number &x) const; explicit operator int() const { return m_val; } friend ostream &operator <<(ostream &os, const Number &s); private: int m_val; }; Burada opeartor int fonksiyonu explicit yapılmıştır. Artık aşağıdaki gibi implicit dönüştürmede çağrılamayacaktır: Number x{10}; int val; val = x; // geçersiz! operator int fonksiyonu çağrılamaz. explicit tür dönüştürme operatör fonksiyonları ancak tür dönüştürme operatörleriyle çağrılabilir. Örneğin: Number x{10}; int val; val = (int)x; // geçerli Tabii buradaki dönüştürme static_cast operatöryle de yapılabilirdi. Sınıfta aynı türe dönüştürme yapan hem normal hem de explicit tür dönüştürme operatör fonksiyonu birlikte bulunamaz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıf içerisinde farklı türlere dönüştürme yapan birden fazla tür dönüştürme operatörü bir arada bulunabilir. Örneğin class Number { public: //... operator int() const; operator long() const; operator double() const; }; Pekiyi bu durumda overload resolution işleminde hangi operatör fonksiyonu çağrılacaktır? İşte bu durumda aday ve uygun olan tür dönüştürme opeartör fonksiyonları arasında hedef türe dönüştürme kalitesine bakılarak en uygun tür dönüştürme operatör fonksiyonu belirlenmektedir. Örneğin: Number x{10}; double d; d = x; // operator doube fonksiyonu seçilir Burada double türüne dönüştürme yapan operator double fonksiyonun daha iyi dönüştürme yaptığı (exact match) kabul edilmektedir. Pekiyi kod aşağıdaki gibi olsaydı ne olurdu? float f; Number x{10}; f = x; Burada int->float, long->float, double-> float dönüştürmelerinin hepsi standart dönüştürmedir. Dolayısıyla hiçbiri diğerinden daha iyi değildir. Atama işlmi ambiguity nedeniyle error ile sonuçlanacaktır. Şimdi de aşağıdaki koda bakalım: class Number { public: //... operator int() const; operator long() const; operator float() const; }; //... double d; Number x{10}; d = x; Burada int->double, long->double, float->double dönüştürmelerinde float->double dönüştürmesi "floating point promotion" sınıfına girdiği için daha iyi bir dönüştürmedir. Dolayısıyla float türüne dönüştürme yapan operatör fonksiyonu seçilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 87. Ders 29/07/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Özellikle operatör fonksiyonlarında kullanıcı tanımlı dönüştürmeler söz konusu olduğunda çeşitli "ambiguity" hatalarıyla karşılaşılabilmektedir. Örneğin Number tütünden bir nesne ile int türünden bir değeri toplayacak olalım: Number x{10}, result; result = x + 20; Bu işlemin iki biçimde de yapılabileceğini varsayalım: 1) x nesnesinin int türüne dönüştürülmesiyle ve iki int değerin toplanmasıyla. 2) 20'nin Number türüne dönüştürülmesiyle ve iki Number nesnesinin toplanmasıyla. Number sınıfının hem int türüne dönüştürme yapan tür dönüştürmesi olsun hem de int değeri Number türüne dönüştürmekte kullanılabilecek bir yapıcı fonksiyonu (converting constructor) bulunuyor olsun. Örneğin Number sınıfı aşağıdaki gibi olabilir: class Number { public: Number() = default; Number(int val); Number operator +(const Number &x) const; operator int() const; friend ostream &operator <<(ostream &os, const Number &s); private: int m_val; }; İşte x + 20 işleminde aslında bir "overload resolution" süreci işletilmektedir. Öncelikle burada result atamasının konuyla hiçbir ilgisiin olmadığını belirtelim. Çünkü derleyici hiçbir zaman sonraki operatörleri dikkate almamaktadır. Burada x'in int türüne dönüştürülmesi de 20'nin Number türüne dönüştürülmesi de "kullanıcı tanımlı dönüştürme (user defined conversion)" grubundadır. Anımsanacağı gibi dönüştürme kaliteleri iyiden kötüye doğru şöyleydi: - Exact match - Integer promotion ya da floating point promotion - Standard conversion - User defined conversion C++ standartlarına göre iki operand'lı operatör işlemlerinde aday fonksiyonlar (candidate functions) soldaki operand'ın sınıfı içerisinden ve global düzeydeki aynı isimli operatör fonksiyonlarından ve "doğal operatör fonksiyonlarından (buna standartlarda "built-in candidate functions" denilmektedir) seçilmektedir. Standartlara göre ovlerload resolution sürecine "sanki temel türler üzerinde işlem yapan operatör fonksiyonları da varmış gibi" bu doğal operatör fonksiyonları da sokulmaktadır. Yani örneğin T bir temel türü belirtmek üzere sanki aşağıdaki gibi global operatör fonksiyonlarının bulunduğu varsayılmaktadır: T operator +(T a, T b); T operator -(T a, T b); T operator *(T a, T b); T operator /(T a, T b); .... Benzer biçimde temel türler için de tek operarand'lı doğal (built-in) operatör fonksşyonlarının bulunduğu varsayılmaktadır. Burada diğer tüm ili operand'lı operatörler için de tek operand'lı operatörler için de aynı durum söz konusudur. Şimdi yeniden örneğimize dönelim: result = x + 20; Bu durumda aday ve uygun fonksiyonlar şunlar olacaktır: 1) İki Number nesnesini toplayan + operatör fonksiyonu 2) iki int değeri toplayan doğal (built-in) operatör fonksiyonu. Şimdi bu operatör fonksiyonlarının parametrelerine bakalım. Bu bağlamda üye fonksiyonların birinci parametrelerinin aynı sınıf türünden bir referans olduğu olduğu da varsayılmaktadır: 1) int operator +(int, int); 2) Number operator +(const Number &r, const Number &n); Burada bir noktaya dikkatinizi çekmek istiyoruz: Aslında doğal (built-in) operatör fonksiyonları diye fonksiyonlar yoktur. Yani örneğin iki int değerin toplanması bir operatör fonksiyonu yoluyla yapılmamaktadır. Burada yalnızca "overload resolution" süreci için sanki böyle bir doğal operatör fonksiyonları varmış gibi bir işlem yürütülmektedir. Programcı iki temel tür üzerinde işlem yapabilecek bir operatör fonksiyonu yazamamaktadır. Yukardaki örnekte x + 20 işleminde x ve 20 sanki argüman gibi yukarıdaki fonksiyonların parametreleriyle eşleşmektedir. Yani birinci fonksiyon için eşleşme şöyle yapılmaktadır: x ---> int 20 ---> int İkinci fonksiyon için ise eşleştirme şöyle yapılmaktadır: x ---> r 20 ---> n İşte burada artık tamamen daha önce görmüş olduğumuz "overload resolution" kuralları işlemektedir. Anımsanacağı gibi "en uygun fonksiyon (the best viable function) tüm argüman parametre dönüştürmeleri diğerlerinden daha iyi olan ya da daha kötü olmayan" fonksiyondur. x --> const Number & dönüştürmesi x ---> int dönüştürmesinden daha iyidir. 20 ---> int dönüştürmesi ise s0 ---> const Number & dönüştürmesinden daha iyidir. O halde burada en uygun fonksiyon yoktur. Yukarıdaki x + 20 işleminin geçersiz olmasının kısa açıklamasını da şöyle yapabiliriz: "x'in int türüne dönüştürmesi de 20'nin Number'a dönüştürülmesi de kullanıcı tanımlı dönüştürmelerdir. Bunların hiçbiri diğerinden iyi değildir. Tabii bu tür ambiguity oluşturan ifadelerde tür dönüştürmeleriyle bu ambiguity durumunu ortadan kaldırabiliriz. Örneğin. result = x + 20; // geçersiz, ambiguity result = static_cast(x) + 20; // geçerli, iki int toplanıyor result = x + Number(20); // geçerli, artık en uygun fonksiyon iki Number nesnesini toplayan operatör fonksiyonu Pekiyi yukarıdaki örnekte Number sınıfında Number ile int değeri toplayan bir operatör fonksiyonu da olsaydı ne olurdu? Bu durumda aday ve uygun olan fonksiyonlar üç tane olacaktır: 1) int operator +(int, int); 2) Number operator +(const Number &r, const Number &n); 3) Number operator +(const Number &r, int val); Burada artık en uygun fonksiyon Number ile int değeri toplayan operatör fonksiyonu olacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Number { public: Number() = default; Number(int val); Number operator +(const Number &x) const; Number operator +(int val) const; operator int() const { return m_val; } friend ostream &operator <<(ostream &os, const Number &s); private: int m_val; }; Number::Number(int val) { m_val = val; } Number Number::operator +(const Number &x) const { Number result; result.m_val = m_val + x.m_val; return result; } Number Number::operator +(int val) const { Number result; result.m_val = m_val; return result; } ostream &operator <<(ostream &os, const Number &s) { return os << s.m_val; } int main() { Number x{10}; Number result; result = x + Number(20); cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Exception mekanizması (exception handling) özellikle nesne yönelimli programlama dillerinde karşılaşılan bir hata kontrol mekanizmasıdır. C gibi klasik prosedürel dillerde hata kontrolleri programcı açısından yorucu olabilmektedir. Programcı başarısız olabilecek her fonksiyonun başarısını kontrol etmek durumunda kalır. İç içe fonksiyonlarda iç bir fonksiyonda hata oluştuğunda programcının içerden dışa doğru başarıszlıkla geri dönmesigerekir. Bu da programın çok kontrollü bir biçimde oluşturulmasına yol açar. Çokça yapılan kontroller okunabilirliği azaltmaktadır. İşte exception mekanizması bu sorunları çözmek amacıyla nesne yönelimli dillere sokulmuştur. Exception mekanizmasının sağladığı avantajlar şunlardır: - Programın daha az kontrollü bir biçimde oluşturulmasını sağlamak ve okunabilirliği artırmak. - Kod ile hata ele alımını biribirinden ayırmak - Tam bir hata kontrolü sağlamak - İç içe fonksiyon çağırmalarında iç fonksşyonlarda oluşan hatalaın daha kolay ele alınmasını sağlamak. - Bir hata oluştuğunda hatanın nedenini de hatayı ele alacak kişiye bildirmek. Örneğin new operatörünü ele alalım. Biz C'de malloc fonksiyonu ile her tahsisat yaptığımızda tahsisatın başarısını kontrol ederiz. Ancak new operatörü ile tahsisat yaptığımızda operator new fonksiyonu eğer tahsisat yapılamazsa exception fırlatmaktadır. Böylece biz her new işleminde kontrol yapmayız. Exception mekanizması kod ile hata ele alımını birbirinden ayırmaktadır. Exception mekanizması sayesinde hatalar toplu biçimde programın belli bir yerinde ele alınabilmektedir. C++'ta bazı özel durumlarda hata kontrolü exception mekanizması olmadan tam bir biçimde yapılamamaktadır. Örneğin türemiş sınıfın yapısı fonksiyonu çağırdığı sırada taban sınıfın yaıcı fonksiyonu içerisinde oluşan hatanın türemiş sınıf tarafından ele alımı normal koşullarda düzgün bir biçimde sağlanamamaktadır. Bazı kütüphane fonksiyonları bazı durumlarda hata oluşturabilmektedir. Ancak bu hatalar fonksiyonun geri dönüş değerine yansıtılamayabilmektedir. Bir hata oluştuğunda hatanın oluştuğu yerde hatanın kaynağına yönelik bilgi aktarımı exception mekanizmasında çok daha kolay sağlanmaktadır. Ancak exception mekanizmasını derleyici sağlarken koda aslında kodda açık bir biçimde bulunmayan pek çok öğe de eklemektedir. Dolayısıyla bu mekanizma belli bir maliyetle oluşturulmaktadır. Üstelik programcı hiç exception mekanizmasını kullanmasa bile "derleyici programcı bu mekanizmayı kullanabilir" diye yine bazı hazırlıklar yapmaktadır. C++ derleyicilerinde "ben exception mekanizmasını kullanmayacağım" biçiminde seçenekler de bulunabilmektedir. Böylece exception mekanizmasının maliyeti ortadan kaldırılabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 88. Ders 31/07/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta exception mekanizması için try, catch ve throw anahtar sözcükleri kullanılmaktadır. try anahtar sözcüğünü bir blok izelemek zorundadır. Buna "try bloğu" denir. try bloğu tek başına bulunamaz. try bloğunu bir ya da birden fazla catch bloğu izlemek zorundadır. Örneğin: try { // try bloğu } catch ( [değişken_ismi]) { //... } catch ( [değişken_ismi]) { //... } catch ( [değişken_ismi]) { //... } ... catch parantezlerinin içerisinde "catch parametresi" denilen bir parametre bildirimi bulunur. catch parametresi bir tane olmak zorundadır. catch bölümü tek parametreye sahip bir fonksiyon gibi de düşünülebilir. Bir try bloğu aynı parametre türüne ilişkin birden fazla catch bölümüne sahip olamaz. Yani catch parametrelerinin türlerinin farklı olması gerekir. catch parametrelerinde yalnıca tür de belirtilebilir. Örneğin: int main() { try { //... } catch (int) { //... } catch (long) { //... } return 0; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Program "akış bakımından" try bloğunun içerisindeyken bir exception oluşursa akış bir goto işlemi gibi tek hamlede try bloğunun uygun catch bloğuna aktarılır. O catch bloğu çalıştırılır ve akış catch bloklarının sonundan devam eder. Yani catch blokları exception oluştuğunda hatanın ele alınacağı yerleri belirtmektedir. Exception'ı asıl oluşturan deyim throw deyimidir. throw deyiminin genel biçimi şöyledir: throw [ifade]; Programın akışı throw deyimini gördüğünde akış son girilen try bloğunun throw anahtar sözcüğünün yanındaki ifadenin türü ile aynı olan catch bloğuna bir goto işlemi gibi aktarılmaktadır. Bundan sonra akışın aktarıldığı catch bloğu çalıştırılır. Diğer catch blokları atlanır. Programın akışı catch bloklarının sonundan devam eder. throw deyimi ile adeta uygun catch bloğuna bir goto işlemş yapılmaktadır. Dolayısıyla akış bir daha throw işleminin yapıldığı yere dönmez. Eğer programın akışı try bloğuna girdikten sonra hiç throw işlemi oluşmazsa akış try bloğunu bitirir, tüm catch blokları atlanır, akış catch bloklarının sonundan devam eder. Yani catch blokları "exception oluşursa akışın aktarılacağı yeri" belirtmektedir. Exception oluşmazsa catch blokları bir etki oluşturmamaktadır. Pekiyi throw anahtar sözcüğünün yanındaki ifadenin anlamı nedir? İşte bu ifade exception oluştuğunda catch parametresine aktarılmaktadır. throw işlemini bu bakımdan bri fonksiyon çağırma işlemine benzetebilirsiniz. aşağıdaki örnekte foo fonksiyonu bar fonksiyonunu, bar fonksiyonu da tar fonksiyonunu çağırmıştır. tar fonksiyonu içerisinde parametre değeri kontrol edilmiş eğer parametre negatif ise exception throw edilmiştir. Bu örneği exception oluşmayacak biçimde pozitif argümanla da çağırıp deneyiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void tar(int a) { cout << "tar begins..." << endl; if (a < 0) throw 10; cout << "tar ends..." << endl; } void bar(int a) { cout << "bar begins..." << endl; tar(a); cout << "bar ends..." << endl; } void foo(int a) { cout << "foo begins..." << endl; bar(a); cout << "foo ends..." << endl; } int main() { cout << "main begins..." << endl; try { foo(-10); } catch (int a) { cout << "exception caught: int a = " << a << endl; } catch (long a) { cout << "exception caught: long a = " << a << endl; } cout << "main ends..." << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- throw deyimi ile bir exception fırlatıldığında bu exception'ı yakalayacak uygun türde bir catch bloğunun olması gerekir. Eğer böyle bir catch bloğu yoksa ya da programcı bir try bloğu içerisinde bile değilse derleyici tarafından std::terminate isimli fonksiyon çağrılır. Bu terminate fonksiyonu da kendi içerisinde std::abort isimli fonksiyonu çağırmaktadır. İşte abort fonksiyonu da programı "abnormal biçimde" sonlsonlandırmaktadır. Burada "neden exception yakalanmadığında std::terminate fonksiyonunun değil programcının istediği bir fonksiyonun çağrılmasının sağlanmasıdır. std::set_terminate isimli fonksiyon ile programcı ele alınmayan exception'lar için kendi fonksiyonunun çağrılmasını sağlayabilmektedir. Tabii std::terminate yerine programcının kendi belirlediği fonksiyon çağrılsa bile programcının artık birtakım işlemleri yaptıktan sonra programı sonlandırması gerekir. Zira artık dönülecek bir yer yoktur. set_terminate fonksiyonunun prototipi şöyledir: #include std::terminate_handler set_terminate(std::terminate_handler f ) noexcept; typedef void (*terminate_handler)(); Fonksiyon geri dönüş değeri void olan parametresi bulunmayan bir fonksiyonun adresini parametre olarak alır. Geri dönüş değeri olarak da önceki fonksiyonun adresini verir. Eğer programcı set ettiği fonksiyon çağrıldığında programı sonlandırmazsa bu durum "tanımsız davranışa" yol açmaktadır. throw ifadesiyle catch poarametresinin türünün tam olarak uyuşması gerekir. Overload resolution kuralları burada işletilmemektedir. (Örneğin throw ifadesi char türdense bu int parametreli bir catch bloğu tarafından yakalanamaz.) throw işlemi yapıldığında throw ifadesinin türü ile tamamen aynı türden olan bir catch bloğu aranmaktadır. Burada "overload resolution" kuralları işletilmemektedir. Yionani örneğin char türünden bir throw işlemş yapıldığında oluşan exception'ı int parametreli bir catch bloğu yakalayamaz. Ancak char parametreli bir catch bloğu yakalayabilir. Başka bir deyişle burada "exact match" aranmaktadır. Ancak izleyen paragraflarda bunun bazı istisnaları üzerinde duracağız. Aşağıda exception'ın yakalanmamasından dolayı oluşan duruma bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; void my_terminate_handler(); void foo(int a); void bar(int a); int main() { cout << "main begins..." << endl; set_terminate(my_terminate_handler); try { foo(-5); } catch (int a) { cout << "int exception caught: " << a << endl; } catch (long a) { cout << "long exception caught: " << a << endl; } catch (double a) { cout << "double exception caught: " << a << endl; } cout << "main ends..." << endl; return 0; } void my_terminate_handler() { cout << "my_terminate handler called..." << endl; exit(EXIT_FAILURE); } void foo(int a) { cout << "foo begins..." << endl; bar(a); cout << "foo ends..." << endl; } void bar(int a) { cout << "bar begins..." << endl; if (a < 0) throw 'a'; cout << "bar ends..." << endl; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında throw işlemi temel türlerle değil sınıf türleriyle yapılır. Çünkü sınıflar bilgi tutabilirler. Her sınıf farklı tür belirttiğine göre farklı sınıf türlerine ilişkin catch blokları bulundurulabilmektedir. Tipik olarak programcı throw işlemini yapmadan önce bir sınıf türünden nesne yaratır. Bu nesnenin içini oluşan problemlemli durumları betimleyen bilgilerle doldurur. Sonra bu sınıf ile throw eder. Bu exception'ı yakalayn kişi de oluşan exception hakkında bilgileri bu nesneden elde eder. Örneğin: if (a < 0) { InvalidArgument ia("Argument shall not be negative!", 123); throw ia; } Ancak programcılar genellikle yukarıdakş iki satır yerine tek satırla geçici nesne yaratarak throw işlemini yaparlar. Örneğin: if (a < 0) throw InvalidArgument("Argument shall not be negative!", 123); Aşağıdaki örnekte InvalidArgument isimli bir exception sınıfı yazılmıştır. throw işlemi sırasında bu sınıf türünden geçici bir nesne yaratılmış ve bu nesneyle throw edilmiştir. try bloğunda da aynı isimli bir sınıfla exception yakalanmış ve hata bilgileri stderr dosyasına yazdırılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class InvalidArgument { public: InvalidArgument(const char *msg, int errcode) : m_msg(msg), m_errcode(errcode) {} const string &msg() const { return m_msg; } int errcode() const { return m_errcode; } private: string m_msg; int m_errcode; }; void foo(int a); void bar(int a); int main() { cout << "main begins..." << endl; try { foo(-5); } catch (const InvalidArgument &e) { cerr << e.msg() << '(' << e.errcode() << ')' << endl; } cout << "main ends..." << endl; return 0; } void foo(int a) { cout << "foo begins..." << endl; bar(a); cout << "foo ends..." << endl; } void bar(int a) { cout << "bar begins..." << endl; if (a < 0) { InvalidArgument ia("Argument shall not be negative!", 123); throw ia; } cout << "bar ends..." << endl; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- throw işleminin bazı ayrıntıları vardır. Şimdi bu ayrıntılar üzerinde duralım. 1) throw ifadesinin catch parametresine aktarılması doğrudan değil derleyici tarafından yaratılan bir geçici nesne yoluyla yapılmaktadır. Örneğin: throw ifade; Burada derleyici önce "ifade" türünden bir geçici nesne yaratır Sanki throw ifadesi bu geçici nesneye verilen ilkdeğer gibi ele alınmaktadır. Standartlar derleyici tarafından yaratılan bu geçici nesneye ilkdeğer vermenin "copy initializayion" biçiminde yapıldığını belirtmektedir. Yine standartlar her ne kadar bu nesne bir geçici nesne gibiyse de bu nesnenin "lvalue" belirttiğini söylemektedir. (Geçici nesnelerin normal olarak RValue belirttiğini anımsayınız.) throw işlemi ile derleyici tarafından yaratılan bu geçici nesneye "exception nesnesi (exception object)" de denilmektedir. Eğer throw ifadesi bir sınıf türündense exception nesnesi de aynı sınıf türünden olacaktır. Bu durumda exception nesnesi için ilgili sınıfın kopya yapıcı fonksiyonu ya da taşıma yapıcı fonksiyonu çağrılacaktır. Tabii daha önce görmüş olduğumuz"copy elision" kuralları buradaki exception ensnesi için de geçerli olacaktır. Örneğin: T et{...}; ... throw et; Buradaki işlem aslında şununla eşdeğerdir: T temp = et; // exception nesnesi sınıf türündense kopya yapıcı fonksiyonu devreye girer Eğer throw ifadesi geçici bir sınıf nesnesi ise C++17 ile birlikte "copy elision" işleminin zorunlu olarak yapılacağını anımsayınız. Örneğin: throw T(...); // C++17 ile birlikte geçici nesne için "copy elision" zorunlu Burada artık C++17 ile birlikte derleyici doğrudan exception nesnesini bu geçici nesne için çağrılacak olan yapıcı fonksiyonla yaratacaktır. Yani bu işlemde C++17 ile birlikte önce geçici nesne için yapıcı fonksiyon çağrılıp sonra derleyicinin yarattığı exception nesnesi için kopya yapıcı fonksiyonu ya da taşıma yapıcı fonksiyonu çalıştırılmayacaktır. Doğrudan geçici nesne yerine exception nesnesi yaratılcaktır. Ayrıca C++11 ile birlikte try bloğu içerisindeki yerel bir sınıf nesnesi ile throw işlemi yapıldığında NRVO işlemi derleyicinin isteğine bağlı bir biçimde gerçekleştirilebilmektedir. Yani bu durumda yerel değişken exception nesnesi gibi yaratılıp kopya yapıcı fonksiyonu çağrılmayabilir. Örneğin: try { T t{...} //... if (ifade) throw t; // t nesnesi doğrudan exception nesnesi gibi yaratılabilir //... } catch (const T &e) { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 89. Ders 05/08/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 2) Derleyicinin oluşturduğu exception nesnesi catch parametresine sanki ilkdeğer veriliyormuş gibi atanmaktadır. Bu atama işlemi fonksiyona parametre aktarımı gibi "copy initialization" biçiminde yapılmaktadır. Örneğin: T t{...}; ... throw t; //... catch (T e) { //... } Burada aslında şu biçimde işlemler yapılmaktadır: T temp = t; T e = et; Burada hiç "copy elision" yapaılmazsa catch parametresi için de kopya yapıcı fonksiyonu çalıştırılacaktır. Yine C++11 ile birlikte exception nesnesinden catch parametresine aktarım sırasında isteğe bağlı (NRVO) bir copy elision uyguşanab,leceği belirtilmiştir. Standartlara göre eğer catch parametresi exception nesnesi ile aynı sınıf türünden bir nesne ise sanki ctahc parametresi aynı sınıf türünden bir referansmış gibi ele alınabilmektedir. Böylece catch parametresi için kopya yapıcı fonksiyonu çağrılmayabilmektedir. Tabii catch parametresindeki bu varsayılan referans exception nesnesini belirtmektedir. Daha önceden defalarca belirttiğimiz gibi C++'ta her zaman yapıcı fonksiyonlarla yıkıcı fonksiyonlar ters sırada çağrılmaktadır. Yukarıda açıkladığımız durumların hepsinde eğer sınıfların yıkıcı fonksiyonları varsa yapıcı fonksiyonlara göre ters sırada çağrılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki gibi bir exception sınıfı olsun: class MyException { public: MyException() { cout << "default constructor..." << endl; } MyException(const MyException &r) { cout << "copy constructor..." << endl; } MyException(const MyException &&r) { cout << "move constructor..." << endl; } ~MyException() { cout << "destructor..." << endl; } }; Biz de aşağıdaki gibi bir exception oluşturalım: int main() { MyException me; try { //... throw me; //... } catch (MyException e) { cout << "exception caught..." << endl; } return 0; } Burada önce me nesnesi için default yapı fonksiyon çağrılacaktır. Sonra throw işlemi gerçekleştiğinde derleyici MyException türünden geçici bir exception nesnesi oluşturacaktır. me nesnesinden bu exception nesnesine ilkdeğer verme kopya yapıcı fonksiyonu yoluyla yapılacaktır. Bu exception nesnesinden de catch parametresine ilkdeğer verme yine kopya yapıcı fonksiyonu yoluyla gerçekleştirilecektir. Microsoft C++ derleyicilerinde program çalıştırıldığında ekranda şunlar gözükmektedir: default constructor... copy constructor... copy constructor... exception caught... destructor... destructor... destructor... Ancak yukarıda da belirttiğimiz gibi C++11 ve sonrasında aslında derleyici isterse catch parametresini sanki bir referansmış gibi de varsayıp catch parametresi için kopya yapıcı fonksiyonu çağırmayabilir. Tabii eğer geçici bir nesneyle throw işlemi yapılırsa C++17 ve sonrasında exception nesnesi için taşıma yapıcı fonksiyonu ya da kopya yapıcı fonksiyonu çağrılmayacaktır. Örneğin: int main() { try { //... throw MyException(); //... } catch (MyException e) { cout << "exception caught..." << endl; } return 0; } Burada artık exception nesnesi için taşıma ya da kopya yapıcı fonksiyonu çağrılmayacaktır. Exception nesnesi doğrudan geçici nesnede belirtilen yapıcı fonksiyonla (örneğimizde default yapıcı fonksiyon) yaratılacaktır. Microsft derleyicilerinde şöyle bir çıktı elde edilmiştir: default constructor... copy constructor... exception caught... destructor... destructor... --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class MyException { public: MyException() { cout << "default constructor..." << endl; } MyException(const MyException &r) { cout << "copy constructor..." << endl; } MyException(const MyException &&r) { cout << "move constructor..." << endl; } ~MyException() { cout << "destructor..." << endl; } }; int main() { try { //... throw MyException(); //... } catch (MyException e) { cout << "exception caught..." << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesiyle throw işlemi yaparken az sayıda yapıcı fonksiyonun ve dolayısıyla yıkıcı fonksiyonun çağrılmasını nasıl sağlayabiliriz. İşte bunu sağlamak için iki yöntem tercih edilebilmektedir. Birinci yöntemde throw ifadesi bir geçici nesne olarak yaratılır. Böylece exception nesnesi için copy elision uygulanır. catch parametresi de aynı sınıf türünden bir referans olur. catch parametresi bir referans olduğu için catch parametresine aktarımda yapıcı fonksiyon çağrılmayacaktır. Örneğin: try { ///... throw MyException(); //... } } catch (MyException &e) { cout << "exception caught..." << endl; } Burada C++17 ve sonrasında toplamda bir tane yapıcı fonksiyon bir tane de yıkıcı fonksiyon çağrılacaktır. İkinci yöntem C++17 öncesinde "copy elision" işleminin zorunlu olmadığı zamanlarda kullanılıyordu. Bu yöntemde throw işleminde exception sınıfı türünden dinamik bir nesne yaratılmakta ve throw işlemi bu nesne türünden adresle yapılmaktadır. Bu durumda exception nesnesi bir gösterici olacaktır. Tabii catch parametresinin de exception sınıfı türünden bir gösterici olması gerekmektedir. Bu yöntemde dinamik nesnenin silinmesi tipik olarak exception yakalandıktan sonra catch bloğunun sonunda yapılmaktadır. Örneğin: try { //... throw new MyException(); //... } catch (MyException *pe) { cout << "exception caught..." << endl; delete pe; } Bu yöntemi Microsoft MFC kütüphensinde yoğun olarak kullanmıştır. Ancak artık C++17 ve sonrasında yukarıda açıkladığımız birinci yöntem daha etkin bir yöntemdir. Tabii her throw nesnesinin geçici yaratılması mümkün olmayabilir. Bu durumda bu teknik kullanılabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Türemiş sınıf türünden yapılan bir throw işlemi taban sınıf türünden catch bloklarıyla yakalanabilir. Bu durum hem sınıf nesneleri için hem sınıf türünden göstericiler ve referanslar için de geçerlidir. Örneğin BException sınıfı AException sınıfından türetilmiş olsun: AException BException Aşağıdaki gibi bir try-catch bloğu bulunuyor olsun: try { //... throw BException(); //... } catch (AException ae) { // BException temp = B(); AException ae = temp; //... } Burada derleyicinin yarattığı geçici exception nesnesi "copy elision" uygulandığı durumda default yapıcı fonksiyonla yaratılacaktır. Daha sonra ise bu exception nesnesi ile ilkdeğer verilerek catch parametresi yaratılacaktır. Bu durumda BException nesnesinin AException kısmı catch parametresine kopyalanmış olacaktır. Yani bu işlem aşağıdaki ile şdeğer hale gelecektir: BException temp = BException(); // C++17 ve sonrasında copy elision uygulanacak AException ae = temp; // AException sınıfının kopya yapıcı fonksiyonu çağrılacak Tabii yukarıda da belirttiğimiz gibi genellikle yapıcı fonksiyonların daha az çağrılması için catch parametresinde referans ya da gösterici kullanılmaktadır: try { //... throw BException(); //... } catch (AException &ae) { //... } Bu işlemin eşdeğeri de şöyledir: BException temp = BException(); // C++17 ve sonrasında copy elision uygulanacak AException &ae = temp; // Kopya yapıcı fonksiyon çağrılmayacak --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Derleyici catch bloklarına yukarıdan aşağıya sırasıyla bakmaktadır. Bu nedenle taban sınıfa ve türemiş sınıfa ilişkin catch blokları birlikte bulundurulduğunda. Bunların sırası önemli olmaktadır. Eğer taban sınıfa ilişkin catch bloğu türemiş sınıfa ilişkin catch bloğundan daha yukarıda bulundurulursa exception'ların hepsi taban sınıfa ilişkin catch bloğu tarafından yakalanacağı için türemiş sınıfa ilişkin catch bloğu bulundurmanın bir anlamı kalmayacaktır. Zaten C++ standartlarında taban sınıfa ilişkin catch bloğunun türemiş sınıfa ilişkin catch bloğunun altında bulundurulması zorunlu tutulmuştur. Örneğin: try { //... throw BException(); //.... } catch (AException &ae) { // geçersiz! //... } catch (BException &be) { //... } Burada türemiş sınıf türünden de taban sınıf türünden de exception'ları yukarıdaki catch bloğu yakalayacaktır. Dolayısıyla aşağıdaki catch bloğunun bulundurulmasının bir anlamı yoktur. Bu tür durumlarda taban sınıfa ilişkin catch bloklarının türemiş sınıfa ilişkin catch bloklarının altında bulundurulması gerekmektedir. Örneğin: try { //... throw BException(); //.... } catch (BException &be) { // geçerli, olması gereken durum //... } catch (AException &ae) { //... } Burada artık türemiş sınıf türünden yapılan throw işlemi yukarıdaki catch bloğu tarafından, taban sınıf türünden yapılan throw işlemi ise aşağıdaki catch bloğu tarafından yakalanacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında kütüphanelerde genel olarak exception sınıfları birbirinden bağımsız biçimde değil bir türetme şeması içerisinde bulundurulmkatadır. C++'ın standart exception sınıfları da bu biçimde bir türetme şeması oluşturmaktadır. Exception sınıflarının türetme şeması içerisinde birbirilerinden türetilerek bulundurulmasının temel nedeni bir grup exception'ın az sayıda catch bloğu tarafından yakalanmasını sağlamak içindir. Örneğin foo fonksiyonunun kendi içerisinde 10 farklı exception sınıfıyla throw edebildiğini varsayalım. Bu durum dokümantasyonda belirtilmiş olsun. O halde biz bu foo fonksiyonunu çağırırken try bloğu kullanmamız ve 10 farklı catch bloğu oluşturmamız gerekir: try { foo(); } catch (E1 &r) { //... } catch (E2 &r) { //... } catch (E3 &r) { //... } ... catch (E10 &r) { //... } İşte eğer bu 10 farklı exception sınıfı taban bir exception sınıfından türetilmiş olsaydı tek bir catch bloğu yoluyla foo içerisinde oluşabilecek exception'ları ele alabilirdik. Örneğin: E E1 E2 E3 E4 E5 E6 E7 E8 E9 E10 try { foo(); } catch (E &r) { //... } Tabii burada biz exception'ı yakaladığımızda bunun hangi exception olduğunu yani sorunun ne olduğunu doğrudan anlayamayız. İşte bu tür durumlarda çokbiçimlilikten faydalanılabilmektedir. Örneğin: class E { public: virtual string msg() const; //... }; //... try { foo(); } catch (E &r) { cout << r.msg() << endl; } Burada hatanın nedeni taban E sınıfındaki sanal msg fonksiyonu ile elde edilmektedir. Çokbiçimli mekanizmadan dolayı msg fonksiyonu çağrıldığında nesnenin dinamik türüne ilişkin (yani throw edilen türe ilişkin) msg fonksiyonu çağrılacak ve hata yazısı uygun biçimde rapor edilebilecektir. Bazen programcı bazı kritik exception'lar için özel işlemler yapmak isteyebilir. Ancak geri kalanları genel biçimde işlemek isteyebilir. Bu durumda türemiş sınıf ve taban sınıf catch blokları bir arada bulundurulmaktadır. Örneğin: try { foo(); } catch (E1 &r) { //... } catch (E2 &r) { //... } catch (E &r) { cout << r.msg() << endl; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Exception mekanizmasında "ellipsis (...)" parametreli özel bir catch bloğu da kullanılabilmektedir. Bu catch bloğu tüm exception'ları yakalamaktadır (yani bunun "catch all" gibi bir anlamı vardır). Ancak eğer ellipsis parametreli catch bloğu bulundurulacaksa catch bloklarının sonunda bulundurulması zorunludur. Böylece eğer exception daha yukarıdaki catch blokları tarafından yakalanmamışsa kesinlikle ellipsis parametreli catch bloğu tarafından yakalanacaktır. Tabii exception ellipsis parametreli catch bloğu tarafından yakalandığında biz artık exception nesnesini elde edemeyiz. Ancak bu catch bloğu exception hiyerarşisi içinde olmayan bir grup farklı exception'ı tek blok ile yakalama işlevini yerine getirmektedir. Örneğin: try { foo(); } catch (AException &ae) { //... } catch (BException &be) { //... } catch (...) { //... } Burada AException ve BException sışındaki tüm exception'lar ellipsis parametreli exception bloğu tarafından yakalanacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 90. Ders 07/08/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Programın akışı birden fazla kez try blouğuna girmiş olabilir. Bu durumda bir exception oluştuğunda (yani throw işlemi yapıldığında) try bloklarının catch blokları son girilenden ilk girilene doğru (yani içeriden dışarıya doğru) el ealınmaktadır. Akış oluşan exception'ı yakalayabilecek ilk catch bloğuna aktarılmaktadır. Eğer exception'ı yakalayabilecek hçbir catch bloğu bulunamazsa daha önceden de belirttiğimiz gibi std::terminate fonksiyonuy çağrılmaktadır. Bu fonksiyon da std::abort fonksiyonunu çağıracak ve program sonlandırılacaktır. Aşağıdaki örnekte programın akışı birden fazla kez try bloğuna gçrmiştir. tar fonksiyonunun içerisinde exception oluştuğunda önce bar fonksiyonu içerisindeki catch blokları taranacak, bu catch bloklarındna hiçbiri exception'ı yakalayamadığı için bu kez foo fonksiyonun içerisindeki catch blokları taranacaktır. foo fonksiyonu içerisinde exception'ı yakalayabilecek bir catch bloğu bulunduğu için programın akışı o catch bloğuna aktarılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class AException { //... }; class BException { //... }; class CException { //... }; void foo(int a); void bar(int a); void tar(int a); int main() { cout << "main begins..." << endl; try { //... foo(-10); //... } catch (...) { cout << "exception caught in main (...)" << endl; } cout << "main ends..." << endl; return 0; } void foo(int a) { cout << "foo begins..." << endl; try { //... bar(a); //... } catch (AException &ae) { cout << "exception caught in foo (AException)" << endl; } cout << "foo ends..." << endl; } void bar(int a) { cout << "bar begins..." << endl; try { //... tar(a); //... } catch (BException &be) { cout << "exception caught in bar(BException)" << endl; } catch (CException &ce) { cout << "exception caught in bar (BException)" << endl; } cout << "bar ends..." << endl; } void tar(int a) { cout << "tar begins..." << endl; if (a < 0) throw AException(); //... cout << "tar ends..." << endl; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta özel bir throw deyimi de yanında ifade olmadan kullanılan throw deyimidir. Buna "rethow" denilmektedir. Örneğin: throw; Bu biçimdeki throw deyimleri ancak catch bloğu içerisinde kullanılabilir. Bu biçimdeki throw deyimleri "exception'ın orijinali hangi nesne ile fırlatılmışsa aynı nesne ile onu yeniden fırlatma" anlamına gelmektedir. Yani rethrow işlemi sırasında exception nesnesi yok edilmez ve exception aynı nesneyle yeniden fırlatılmış gibi bir durum oluşur. Örneğin: try { foo(-10); } catch (exception &r) { //... throw; } Burada exception yakalandığında birtakım işlemler yapılıp rethow uygulanmıştır. Bu rethow işlemi orijinal exception nesnesi ne ise onunla yeniden throw etmek anlamına gelir. Böylece exception bu try bloğunu akş bakımından içeren başka bir catch bloğu tarafından yeniden yakalanabilecektir. Rethrow işlemi "adeta exception'ı yakalayıp yakalamamış gibi işlemlerin devam etmesini sağlamaktadır. Tabii rethow işleminde yeniden fırlatılan exception'ı yakalayabilecek bir catch bloğu bulunamazsa yine derleyici tarafından std::terminate fonksiyonu çağrılmaktadır. Aşağıdaki örnekte tar fonksiyonu içerisinde oluşan exception önce bar fonksiyonu içerisindeki catch bloğu tarafından yakalanmış ve rethrow işlemi uygulanmıştır. Bu durumda exception ikinci kez main içerisindeki ellipis parametreli catch bloğu tarafından yakalanacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class AException { //... }; class BException { //... }; class CException { //... }; void foo(int a); void bar(int a); void tar(int a); int main() { cout << "main begins..." << endl; try { //... foo(-10); //... } catch (...) { cout << "exception caught in main (...)" << endl; } cout << "main ends..." << endl; return 0; } void foo(int a) { cout << "foo begins..." << endl; try { //... bar(a); //... } catch (AException &ae) { cout << "exception caught in foo (AException)" << endl; } cout << "foo ends..." << endl; } void bar(int a) { cout << "bar begins..." << endl; try { //... tar(a); //... } catch (BException &be) { cout << "exception caught in bar(BException)" << endl; } catch (CException &ce) { cout << "exception caught in bar (BException)" << endl; } cout << "bar ends..." << endl; } void tar(int a) { cout << "tar begins..." << endl; if (a < 0) throw AException(); //... cout << "tar ends..." << endl; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Akış bir fonksiyonda ilerlerken bir throw işlemi oluşursa akış tamamen başka bir yere aktarılabilmektedir. Bu durum o fonksiyon içerisinde yaratılan sınıf nesneleri için önemlidir. Çünkü throw işlemi yapılmadan önce çeşitli sınıf nesneleri yaratılmış olabilir. Bu sınıf nesnelerinin yapıcı fonksiyonları içerisinde çeşitli tahsisat işlemleri yapılmış olabilir. Bu durumda akışın başka bir yere gitmesi ve o yerel sınıf nesneleri ile erişimin kesilmesi sızıntılara yol açabilmektedir. İşte C++'ta bu durumu ortadan kaldırmak için "stack unwinding" denilen bir mekanizma bulunmaktadır. Bu mekanizmaya göre bir throw işlemi gerçekleştiğinde o throw işlemini yakalayan catch bloğuna ilişkin try bloğuna girildikten sonra yaratılmış olan bütün yerel sınıf nesneleri için ters sırada yıkıcı fonksiyonlar çağrılmaktadır. Böylece yerel sınıf nesneleri içerisinde tahsis edilmiş olan kaynaklar throw işlemi sırasında başarılı bir biçimde boşaltılmış olur. Tabii bu mekanizmasının derleyici tarafından sağlanmasının birtakım maliyetleri vardır. Bu nedenle bazı kritik uygulamalarda programcılar exception mekanizmasını hiç kullanmak istemeyebilirler. Stack unwinding mekanizmasında throw işlemi oluştuğunda yalnızca try bloğuna girildikten sonra yaratılmış ve yapıcı fonksiyonu tam olarak çalıştırılmış yerel ve parametre nesneleri için yıkıcı fonksiyonlar ters sırada çalıştırılmaktadır. new ile dinamik bir biçimde yaratılmış olan sınıf nesneleri için yıkıcı fonksiyonlar çağrılmamaktadır. Dinamik olarak yaratılmış olan nesnelerin boşaltımından tamamen programcı sorumludur. Aşağıdaki örnek "stack unwinding" mekanizmasını açıklamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample(int a) : m_a(a) { cout << "Sample constructor: " << m_a << endl; } ~Sample() { cout << "Sample destructor: " << m_a << endl; } private: int m_a; }; void foo(int a); void bar(int a); void tar(int a); Sample a(10); int main() { Sample b(20); try { Sample c(30); //... foo(-10); //... } catch (...) { cout << "exception caught in main (...)" << endl; } return 0; } void foo(int a) { Sample d(40), e(50); bar(a); } void bar(int a) { Sample f(60); tar(a); } void tar(int a) { Sample g(70); if (a < 0) throw 0; //... } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- throw işlemi gerçekleştiğinde eğer exception hiçbir catch bloğu tarafından yakalanamamışsa (bu durumda std::terminate fonksiyonun çağrıldığını anımsayınız) stack unwinding uygulanıp uygulanmayacağı derleyicileri yazanların isteğine bırakılmıştır. Yani exception yakalanmamışsa stack unwinding hiç uygulanmayabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir yapıcı fonksiyonun içerisinde exception oluşursa ne olacaktır? Örneğin: Sample s; Burada Sample sınıfının yapıcı fonksiyonu içrisinde aşağıdaki noktada bir exception oluşmuş olsun: Sample::Sample(...) { //... throw MyException(); //... } Bu durumda bu Sample nesnesi için yıkıcı fonksiyon çağrılacak mıdır? İşte C++'ta bu durumlar için şöyle bir genel kural vardır: Stack unwinding sırasında yalnızca yapıcı fonksiyonları tam olarak çalıştırılmış olan sınıf nesneleri için yıkıcı fonksiyonlar çağrılmaktadır. Dolayısıyla yukarıdaki örnekte Sample nesnesinin yapıcı fonksiyonu tam olarak çalıştırılmamış olduğundan dolayı stack unwinding sırasında yıkıcı fonksiyonu da çağrılmayacktır. Tabii bu durumu bilen programcının throw işlemine kadar yapılan tahsisatları kendisinin serbest bırakması gerekir. Yapıcı fonksiyonlar içerisinde exception daha tuhaf yerlerde de oluşabilir. Ancak yukarıda da belirttiğimiz gibi genel kural stack unwinding sırasında yapıcı fonksiyonları tam olarak çalıştırılmış olan sınıf nesneleri için yıkıcı fonksiyonların çağrılmasıdır. Örneğin B sınıfının A sınıfından türetildiğini ve sınıfın X ve Y sınıfları türünden m_x ve m_y veri elemanlarının bulunduğunu varsayalım. Yani B sınıfı şöyle bildirilmiş olsun: class B : public A { public: //... private: X m_x; Y m_y; }; B sınıfının yapıcı fonksiyonu aşağıdaki gibi olsun (temsili kod): B::B(...) : A(...), m_x(...), m_y(...) { //... } Şimdi B sınıfı türünden bir nesne yaratmış olalım: B b(....); Burada çeşitli exception'ların oluştuğu durumdaki stack unwinding mekanizmasını gözden geçirelim: 1) Eğer A taban sınıfının yapıcı fonksiyonu içerisinde exception oluşursa hiçbir yıkıcı fonksiyon çağrılmaz. Çünkü bu durumda yapıcı fonksiyonu tamamen çalıştırılmış olan herhangi bir nesne yoktur. 2) m_x nesnesi için çağrılan X sınıfının yapıcı fonksiyonu içeisinde exception oluşursa yalnızca b nesnesinin taban sınıf kısmı için A sınıfının yıkıcı fonksiyonu çağrılacaktır. 3) Eğer m_y nesnesi için çağrılan Y sınıfının yapıcı fonksiyonu içerisinde exception oluşursa bu durumda önce m_x için b nesnesinin taban kısmı için yıkıcı fonksiyonlar ters sırada çağrılacaktır. 4) Eğer B sınıfının yapıcı fonksiyonu içerisinde exception oluşursa sırasıyla m_y nesnesi için, m_x nesnesi için ve b nesnesinin taban kısmı için ilgili sınıfların yıkıcı fonksiyonları çalıştırılacaktır. Aşağıda bu duruma bir örnek verilmiştir. Bu örnek üzerinde oynamalar yaparak hangi yapıcı fonksiyonun içerisinde exception oluştuğunda hangi nesneler için yıkıcı fonksiyonların çalıştırıldığını kontrol edebilirsiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class X { public: X() { cout << "X constructor" << endl; } ~X() { cout << "X destructor" << endl; } }; class Y { public: Y() { cout << "Y constructor" << endl; throw 0; } ~Y() { cout << "Y destructor" << endl; } }; class A { public: A() { cout << "A constructor" << endl; } ~A() { cout << "A destructor" << endl; } }; class B : public A { public: B() : A(), m_x(), m_y() { cout << "B constructor" << endl; } ~B() { cout << "B destructor" << endl; } private: X m_x; Y m_y; }; int main() { try { B b; } catch (...) { //... } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Dinamik olarak tahsis edilen sınıf nesnelerinin yapıcı fonksiyonları içerisinde exception oluşursa tahsis edilmiş olan heap alanı nasıl free hale getirilecektir? Örneğin Sample sınıfı türünden new operatörü ile aşağıdaki gibi bir dinamik nesne tahsis ettiğimizi düşünelim: ps = new Sample(); Burada bilindiği gibi önce Sample nesnesi için operator new fonksiyonu ile heap'te dinamik bir alan tahsis edilecek sonra bu alan için yapıcı fonksiyon çağrılacak, ondan sonra ps göstericisine atama yapılacaktır. Burada Sample sınıfının yapıcı fonksiyonunda exception oluşursa bu exception bir catch bloğu tarafından yakalandığında programcının bu dinamik alanı free hale getirme olanağı yoktur. Çünkü daha ps göstericisine bile atama yapılmamıştır. İşte C++ standartlarına göre dinamik olarak tahsis edilen sınıf nesnesinin yapıcı fonksiyonunda exception oluşursa operator new fonksiyonu tarafından tahsis edilmiş olan dinamik alan derleyicinin kendisi tarafından operator delete fonksiyonu ile free hale getirilmektedir. Yani programcının bu özel durum için kaygılanması gerekmemektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Stack unwinding süreci içerisinde içerisinde yıkıcı fonksiyonlarda oluşabilecek exception'lar programı tamamen kontrol dışı bir noktaya getirebilmektedir. Bu nedenle C++'ta stack unwinding süreci içerisinde yıkıcı fonksiyonlarda exception oluştuğunda derleyici tarafından std::terminate fonksiyonu çağrılarak program sonlandırılmaktadır. Tabii burada kastettiğimiz şey exception'ın yıkıcı fonksiyondan dışarıya fırlatılmasıdır. Yoksa yıkıcı fonksiyonun kendi içerisinde exception ele alınırsa bu durum soruna yol açmamaktadır. Aşağıda bu duruma bir örnek verilmiştir. Bu örnekte stack unwinding sırasında yıkıcı fonksiyonda exception oluşturulmuştur. Bu durumda std::teriminate ve dolayısıyla std::abort ile ptogram sonlandırılacaktır. Örnekteki noexcept(false) belirleyicisi izleyen paragraflarda ele alınacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample() {} ~Sample() noexcept(false); }; void foo(int a); Sample::~Sample() noexcept(false) { foo(-10); //... } void foo(int a) { if (a < 0) throw 0; } int main() { try { Sample s; throw 0; } catch (...) { //... } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında programcının throw etmek için kendi exception sınıflarını yazmasına çoğu kez gerek yoktur. Çünkü C++'ın standart kütüphanesinde zaten çeşitli sorunlu durumlar için oluşturulmuş olan basit hazır exception sınıfları bulunmaktadır. Standart kütüphanedeki bu exception sınıfları bir türetme şeması içerisinde oluşturulmuştur ve bunların hepsi en tepedeki "exception" isimli bir sınıftan türetilmiştir. Standart kütüphanedeki önemli exception sınıflarının hiyerarşisi şöyledir: exception ├── logic_error │ ├── invalid_argument │ ├── domain_error │ ├── length_error │ └── out_of_range └── runtime_error ├── range_error ├── overflow_error ├── underflow_error ├── system_error Burada exception sınıfının bildirimi başlık dosyasında diğer sınıfların bildirimelri ise başlık dosyasına bulunmaktadır. Tepedeki exception sınıfından temelde iki önemli kol ayrılmaktadır: logic_error ve runtime_error. logic_error sınıfı programdaki birtakım ön koşulların ihlal edilmesi durumu için düşünülmüştür. runtime_error ise programın çalışması sırasında karşılaşılan sorunlar için oluşturulmuştur. Bu logic_error ve runtime_error sınıflarından da sınıflar türetilmiştir. logic_error sınıfından türetilen invalid_argument isimli sınıf tipik olarak programcılar tarafından bir fonksiyonun argümanının yanlış bir biçimde geçilmesi durumu için kullanılmaktadır. length_error exception'ı bir olgunun uzunluğunun uygunsuz olması durumları için düşünülmüştür. out_of_range ise belli bir aralıkta olması gereken değerin o aralıkta olmaması durumlarında kullanılmaktadır. Örneğin bir tarihteki ay değeri 1 ile 12 arasında olmalıdır. Ancak bu ay değeri bu değerlerin dışında bir eğer olarak girilmişse bu exception kullanılabilir. runtime_error sınıfından türetilen sınıflar genel olarak standart kütüphanenin kendi öğeleri tarafından kullanılmaktadır. Örneğin bir fonksiyon bir taşma durumu tespit ettiğinde overflow_error ile throw işlemi yapabilir. Programcı isterse kendisi de bu exception sınıflarından sınıflar türetebilir. En tepedeki exception sınıfının what isimli sanal const char * türüyle geri dönen (yani bir yazının adresiyle geri dönen) bir üye fonksiyonu vardır. Bu sanal fonksiyon türemiş sınıflarda override edilmiştir. Fonksiyonun prototipi şöyledir: virtual const char* what() const noexcept; Bu tepedeki exception sınıfının bir default yapıcı fonksiyonu bir de kopya yapıcı fonksiyonu bulunmaktadır. Ayrıca sınıf bir de kopya atama operatör fonksiyonuna sahiptir. Bu sınıfın bildirimi aşağıdaki gibidir: #include class exception { public: exception() noexcept; exception(const exception&) noexcept; exception& operator=(const exception&) noexcept; virtual ~exception(); virtual const char* what() const noexcept; }; Bu taban exception sınıfının what fonksiyonunun saf sanal olmadığına dikkat ediniz. Genellikle standartlarda belirtilmemiş olsa da derleyiciler bu sınıfın içerisinde const char * türünden bir private gösterici tutulmaktadır. what fonksiyonu da bu göstericinin değeriyle geri dönmektedir. Ancak standartlar bu gerçekleştirimin ayrıntıları hakkında bir şey söylememektedir. Taban exception sınıfından türetilmiş olan yukarıda bazılarını ele aldığımız sınıfların hepsinin aşağıdaki gibi iki yapıcı fonksiyonu vardır: explicit xxx_error(const string& what_arg); explicit xxx_error(const char* what_arg); Bu sınıflar what fonksiyonlarında tipik olarak parametresiyle aldıkları bu yazıyı geri döndürürler. Örneğin: void foo(int a) { if (a < 0) throw invalid_argument("argument shall not be negative!"); // ... } //.. try { foo(val); //... } catch (exception &e) { cout << e.what() << endl; } Burada invalid_argument sınıfı türünden nesne yaratılırken ona bir yazı argüman verilmiştir. İşte override edilen what sanal fonksiyonu bu yazıyı vermektedir. Dolayısıyla biz farklı türlerden exception nesnelerini taban sınıf olan exception parametreli catch bloğu ile yakalabiliriz ve hatayı rapor edebiliriz. Aşağıda standart exception sınıflarının kullanımına basit bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; void foo(int a) { if (a < 0) throw invalid_argument("argument must be positive or zero!"); cout << "Ok" << endl; } int main() { try { foo(1000000); } catch (exception &e) { cout << e.what() << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart exception sınıfları aslında C++'ın kendi standart kütüphanesi tarafından da kullanılmaktadır. Örneğin new operatör tahsisatı yapamazsa bad_alloc isimli exception sınıfından türetilmiş olan bir sınıf nesnesi ile throw eder. Ya da örneğin string sınıfında string içerisindeki belli indekste bulunan karaktere erişme işlemi at fonksiyonuyla yapılıyorsa bu at fonksiyonu erişilen indeks geçersizse out_of_range türüyle throw etmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { string s{"ankara"}; try { auto c1 = s.at(4); cout << c1 << endl; auto c2 = s.at(100); } catch (out_of_range &e) { cout << e.what() << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda da belirttiğimiz gibi programcı C++'ın standart excetpion sınıflarını hiç kullanmayıp kendi exception sınıflarını da yazabilir. Aslında Qt gibi, MFC gibi pek çok yaygın sınıf kütüphanesi hiç C++'ın standart exception sınıflarını kullanmadan kendi exception sınıflarını tanımlayarak throw işlemini bunlarla yapmaktadır. Tabii programcı C++'ın standart exception sınıflarını kullanırken o sınıflardan türetme yaparak da kendi exception sınıflarını oluşturabilir. Aşağıda örnekte logic_error sınıfınından NegativeError isimli bir exception sınıfı türetilmiş ve kodda bu sınıf kullanılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include using namespace std; class NegativeError : public logic_error { public: NegativeError(const char *msg, int errcode) : logic_error(msg), m_errcode(errcode) {} const char *what() const noexcept override { m_errtext = string(logic_error::what()) + "errno: " + to_string(m_errcode); return m_errtext.c_str(); } private: mutable string m_errtext; int m_errcode; }; void foo(int a) { if (a < 0) throw invalid_argument("argument must be positive or zero!"); if (a > 1000) throw NegativeError("argument too big!", 123); cout << "Ok" << endl; } int main() { try { foo(1000000); } catch (exception &e) { cout << e.what() << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın standart exception sınıflarındaki what fonksiyonunun geri dönüş değerinin C tarzı bir string olması şüpheli bir tasarımdır. Yukarıdaki örnekte olduğu gibi bu what fonksiyonundna yaşayan bir C string'i oluşturmak zahmetlidir. Burada tasarımda what fonksiyonunun string nesnesine geri dönmesi daha uygun gibi gözükmektedir. Ancak exception sınıflarının throw etmeyen kopya yapıcı fonksiyonu kullanmak istemesi tasarımın böyle yapılmasına yol açmıştır. (string sınıfı kendi içerisinde bellek tahsisatı yaptığı için bad_alloc ile throw işlemi oluşturabilmektedir. Ancak yine de tasarım daha değişik de yapılabilirdi.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta "exception belirlemesi (exception specification)" denilen özellik bir fonksiyonun hangi exception'larla dışarıya throw yapabileceğini belirtmek kullanılıyordu. Örneğin: void foo(int a) throw(std::invalid_argument) { //... } Buarada foo fonksiyonu yalnızca dışarıya invalid_argument sınıfı ile throw yapabilmektedir. Bu sayede bu fonksiyonu çağıracak kişiler hangi bloklarını oluşturucaklarını hiç dokğmantasyona bakmadan prototipten anlayabiliyordu. Eğer söz konusu fonksiyon exception belirlemesinde verdiği sözü tutmazsa (yani yukarıdaki örnekte foo fonksiyonu dışarıya başka bir exception sınıfı ile throw işlemi yaparsa) derleyici tarafından std::unexpected isimli fonksiyon çağrılıyordu. Bu fonksiyon da std::terminate fonksiyonunu çağırmaktaydı. Böylece program abort fonksiyonu ile sonlandırılıyordu. Örneğin: void foo(int a) throw() { //... } Buradaki exception belirlemesi "foo fonksiyonunun hiçbir exception ile throw işlemi yapmayacağı" anlamına gelmektedir. Her ne kadar klasik C++'taböyle bir mekanizma varsa da programcılar bu mekanizmayı kullanmada isteksiz olmuşlardır. Zaten bazı derleyiciler bu mekanizmayı hiç desteklelemiştir. Örneğin Microsoft'un C++ derleyicileri bu sentaksı desteklemekle tamamen görmezlik gelmektedir. Java programlama dilinde exception belirlemesi çok yoğun kullanılmaktadır. Hatta bu dilde exception belirlemesine sahip olan metotlar çağrılırken belirlemede belirtilen sınıfları içeren catch bloklarının bulundurulması zorunludur. Exception belirlemeleri ilk standartlardan beri bulunan bir özellikse de zamanla "faydasının zararından az olduğu" fikri belirginleşmiştir. Bu nedenle C++11'de "deprecated" yapılmış ve C++17'de de tamamen kaldırılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte eski exception belirlemesi deprecated yapılmakla birlikte kısıtlı bir exception belirlemesi sentaksı da dile eklenmiştir. Fonksiyonun parametre parantezinden sonra kullanılan noexcept belirleyicisi "fonksiyonun dışarıya herhangi bir türle throw etmeyeceği" anlamına gelmektedir. Örneğin: void foo(int a) noexcept { //... } Eğer noexcept belirleyicisi kullanıldığı halde fonksiyonun dışına throw işlemi yapılırsa bu durumda derleyici tarafından std::unexpected isimli fonksiyon çağrılır. Bu fonksiyon da kendi içerisinde std::terminate fonksiyonunu, std::terminate fonksiyonu da anımsanacağı gibi std::abort fonksiyonu çağırmaktadır: unexpected --> terminate --> abort Yani kısaca biz noexcept ile verdiğimiz sözü tutmazsak programımız abort ile sonlandırılır. noexcept anahtar sözcüğü hem prototipte hem de tanımlama sırasında bulundurulmak zorundadır. Ancak overload bakımından fonksiyonun imzasını değiştirmez. (Yani aynı isimli ve aynı parametrik yapıya sahip noexcept belirleyici olan ve olmayan aynı faaliyet alanında iki fonksiyon bir arada bulunamaz.) Aşağıdaki örnekte foo fonksiyonu noexcept belirleyicisi ile dışarıya throw yapmayacağı sözünü verdiği halde dışarıya throw işlemi yapmaktadır. Bu durumda program abort fonksiyonu ile sonlandırılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; void foo(int a) noexcept { if (a < 0) throw invalid_argument("argument must not be negative or zero!"); cout << "foo" << endl; } int main() { try { foo(-1); } catch (invalid_argument &e) { cout << e.what() << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- noexcept belirleyici parantezli bir sentaksla da kullanılabilmekteir. Bu durumda parantezler içerisinde bool türden true ya da false değeri veren ifadeler bulunabilir. Tabii bu ifadelerin derleme aşamsında sabit ifadesi biçiminde ele alınabilmesi gerekmektedir. noexcept(true) fonksiyonun exception throw etmeyeceği, noexcept(false) ise fonksiyonun exception throw edebileceği anlamına gelmektedir. Örneğin: void foo() noexcept(true); Buradaki true fonksiyonun dışarıya exception fırlatmayacağı anlamına gelir. Zaten yalnızca parantezsiz noexcept kullanımı da aynı anlama gelmektedir. Dolayısıyla aşağıdaki iki bildirim eşdeğerdir: void foo() noexcept; void foo() noexcept(true); noexcept parametresi false ise bu durum fonksiyonun dışarıya exception throw edebileceği anlamına gelmektedir. Dolayısıyla aşağıdaki iki bildirim de eşdeğerdir: void foo() noexcept(false); void foo(); Pekiyi parantezli noexcept belirleyicine ne gerek vardır? İşte bazen bazı sabit ifadeleriyle duruma göre noexcept durumu ayarlanabilmektedir. Özellikle şablonlarla birlikte bu parantezli biçimin gerekli kullanımları söz konusu olabilmektedir. Bir üye fonksiyon hem const ise hem de noexcept belirleyicisine sahip ise bildirimde parametre parantezlerinden sonra önce const sonra noexcept belirleyicilerinin getirilmesi zorunludur. Bu sıra ters olamaz. Örneğin: class Sample { public: void foo() const noexcept; // geçerli void foo() noexcept const; // geçersiz!.. //... }; C++11 ie birlikte sınıfların yıkıcı fonksiyonları programcı noexcept belirlyecisini kullanmasa bile noexcept kabul edilmektedir. Örneğin: class Sample { public: Sample(); ~Sample(); // Yıkıcı fonksiyon dışarıya exception fırlatamaz,noexcept kullanılmamış olsa bile noexcept kullanılmış gibi işlem görmektedir //... }; Eğer yıkıcı fonksiyon içerisinde programcı doışarıya throw işlemi yapacaksa (bu durum tavsiye edilmemektedir) bu durumda açıkça noexcept(false) belirleyicisini kullanması gerekir. Örneğin: class Sample { public: Smaple(); ~Sample() noexcept(false); // yıkıcı fonksiyon dışarıya exception fırlatabilir //... }; noexcept belirleyicisi için derleyici derleme zamanında herhangi bir kontrol yapmak zorunda değildir. Çünkü noexcept bir fonksiyonun exception fırlatabilmesi çok dolaylı bir biçimde mümkğn olabilmektedir. Halbuki Java'da bu tür durumlarda exception fırlatabilecek bir fonksiyonun zaten try-catch içerisinde çağrılması ya da onu çağıran fonksiyonda exception belirlemesinin yapılması gerekmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyon çağrıldığında fonksiyon içerisinde bir exception oluşursa akış throw işlemi ile birlikte fonksiyondan aniden çıkacaktır. Bu durumda fonksiyon içerisinde o ona kadar yapılmış olan tahsisatlar boşaltılamayabilir. Bu duruma fonksiyonun "exception güvenliliği (exception safety)" denilmektedir. Örneğin: try { foo(); // foo'da exception oluşursa sızıntı oluşur mu? } catch (exception &e) { //... } Normal olarak fonkisyonlar eğer kendi içlerinde tahsisatlar yapmışlarsa throw uygulanmadan önce yapılmış olan bu tahsisatların bu fonksiyonlar tarafından geri alınması gerekmektedir. Böylece fonksiyonlar exception güvenli olurlar. Örneğin: void foo() { int *pi; //... pi = new int[n] if (ifade) throw runtime_error(); // dikkat, bellek sızıntısı! //... delete[] pi; } Burada foo fonksiyonu exception güvenli değildir. Çünkü throw işlemi yapılmadan önce tahsis ettiği kaynağı geri bırakmamıştır. Stack unwinding mekanizmasından dolayı fonksiyonlar içerisinde yaratılmış olan sınıf nesneleri exception güvenliliği bozmamaktadır. Örneğin: void foo() { Sample s; Mample m; //... if (ifade) throw runtime_error(); // sorun yok //... } Burada Sample ve Mample sınıfının yapıcı fonksiyonları içerisinde çeşitli tahsisatların yapılmış olduğunu düşünelim. Stack unwinding mekanizması yoluyla throw işlemi yapıldığında bu nesneler için yıkıcı fonksiyonlar çağrılacağından ve bu yıkıcı fonksiyonlarda kaynaklar geri bırakılacağından herhangi bir sızıntı oluşmayacaktır. O halde fonksiyonlar içerisinde dinamik tahsisatların yapıldığı durumda bu dinamik tahsisatların throw işlemi sırasında otomatik geri bırakılması unique_ptr sınıfı ile mümkün hale getirilebilir. Örneğin: void foo() { unique_ptr pi; //... pi = unique_ptr(new int[n]); if (ifade) throw runtime_error(); // dikkat, bellek sızıntısı! //... } Aşağıdaki kodla unique_ptr kullanımını test edebilirsiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; class Sample { public: Sample() { cout << "constructor" << endl; m_pi = new int[100]; } ~Sample() { cout << "destructor" << endl; delete[] m_pi; } //... private: int *m_pi; }; void foo(int a) { unique_ptr ps; ps = unique_ptr(new Sample());; if (a < 0) throw invalid_argument("argument must not be negative or zero!"); cout << "foo" << endl; } int main() { try { foo(-1); } catch (invalid_argument &e) { cout << e.what() << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf nesnesi, sınıf türünden gösterici ya da referans yoluyla sınıfın statik olmayan bir üye fonksiyonu çağrılmış olsun. C++'ta bu üye fonksiyon içerisinde dışarıya bir exception oluştuğunda üye fonksiyonun çağrıldığı nesnenin durumuna yönelik dört exception garanti düzeyinden söz edilmektedir: No-throw Guarantee, Strong Guarantee, Basic Guarantee ve No Guarantee. Bunları en iyiden kötüye doğru tek tek gözden geçirelim: 1) No Throw Guarantee: Burada üye fonksiyon zaten dışarıya throw işlemi yapmayacağına ilişkin söz vermiştir. Dolayısıyla bir exception fırlatmayacaktır. O zaman olabilecek en iyi durum budur. Böyle fonksiyonların programcılar tarafından noexcept belirleyicisi ile belirtilmesi iyi bir tekniktir. Örneğin: class Sample { //... void foo() noexcept; }; Burada artık biz bu fonksiyonu zaten try-cath içerisinde çağırmak zorunda da değiliz. Çünkü fonksiyon zaten dışarıya throw işlemi yapmayacaktır. Dolayısıyla da bir sızıntı durumu söz konusu olmayacaktır. 2) Strong Guarantee: Burada bir üye fonksiyon çağrıldığında eğer üye fonksiyonun içerisinde exception oluşursa akış fonksiyondan çıktığında nesnenin durumu (yani onun içerisindeki veri elemanlarının değerleri) bu üye fonksiyon çağrılmadan önceki değerlerde kalır. Başka bir deyişle üye fonksiyonda exception oluşması nesne üzerinde hiçbir etki yaratmamaktadır. Nesnenin durumu tamamen exception'a yol açmış olan bu üye fonksiyonun çağrılmasından önceki durumla aynıdır. Örneğin: Sample s; //... try { s.foo(); } catch (...) { s'in durumu foo'nun çağrılmadan önceki durumu ile tamamen aynıdır. //... } Tabii "strong guarantee" oluşturmak hem zahmetli hem de zordur. C++'ın standart kütüphanesindeki pek çok sınıfın üye fonksiyonu "strong guarantee" oluşturmaktadır. Örneğin vektörde biz push_back yaptığımızda bir exception oluşursa vector nesnemiz push_back yapmadan önceki durumla aynı durumda kalır. Yani push_back fonksiyonu bize "strong guarantee" vermektedir. vector sınıfındaki push_back fonksiyonun bile strong guarantee verebilmek için oldukça ince işlemleri yapması gerekmektedir. Örneğin biz Samplı sınıf nesnelerinden oluşan bir vector nesnesi tanımlamış olalım: vector v; //... Burada bir Sample nesnesini push_back ile vektöre eklediğimizi düşünelim: v.push_back(Sample()); push_back fonksiyonu size() == capacity() durumunda yeni bir alan tahsis edip eski alandaki nesneleri yeni alana kopya yapıcı fonksiyonu kopyalayacaktır. Sample sınıfının kopya yapıcı fonksiyonunda bir exceprion oluşsa bile bu push_back fonksiyonunu yazanlar durumu ele almış ve nesneyi orijinal durumda bırakmayı başarmışlardır. Yukarıda da belirttiğimiz gibi üye fonksiyona "strong guarantee" vermek kolay olmayabilir. Programcının her ifadenin uç durumlarda excetion fırlatıp fırlatmadığını dikkate alması, eğer exception fırlatma potansiyelinde olan ifadeler varsa bu exception'ı yakalayıp nesneyi orijinal konuma getirmesi ve yeniden aynı exception ile rethrow işlemi yapması gerekir. Örneğin: void foo() { // kaynak tahsisatı yapılıyor olsun try { // exception'a yol açabilecek başka işlemler } catch (...) { // kaynaklar boşaltılıyor, nesne eski haline getiriliyor throw; } //... } 3) Basic Guarantee: Burada ilgili üye fonksiyon içerisinde bir exception oluştuğunda "bellek sızıntısı" ya da "kaynak sızıntısı" olmayacağı garanti edilir. Ancak sınıf nesnesinin durumunun bu üye fonksiyon çağrılmadna önceki durumda olması garanti edilmemektedir. Exception oluşturğunda nesnenin durumu geçerlidir, nesne yıkıcı fonksiyon ile kaynaklarını boşaltabilir durumdadır. Örneğin: Sample s; //... try { s.foo(); } catch (...) { //... } Burada eğer "basic guarantee" söz konusu ise foo fonksiyonunun içersinde exception oluşursa herhangi bir sızıntı olmamalıdır. Ayrıca buradaki s nesnesi foo fonksiyonu çağrılmadan önceki durumunda olmayabilir ancak "destruct" edilebilir bir durumda olmalıdır. Yani "destructor" çağrıldığında nesne geri bırakım işlemlerini yapabilmelidir. Basic guarantee sağlayabilmek için programcı koduna dikkat etmelidir. Bir problem karşısında geri bırakımı yapıp rethrow işlemi uygulayabilir. Örneğin: void foo() { // kaynak tahsisatı yapılıyor olsun try { // exception'a yol açabilecek başka işlemler } catch (...) { // kaynaklar boşaltılıyor throw; } //... } Basic guarantee oluşturmak için dinamik bellek tahsisatlarında bellek sızıntısına karşı "smart pointer" sınıfları da kullanılabilir. Örneğin: void foo(int a) { //... unique_ptr pi(new int[10]); unique_ptr pi(new char[100]); // artık burada exception oluşursa stack unwinding sırasında pi tahsisatı boşaltılacak // ... } 4) No Guarantee: Böyle üye fonksiyonlarda exception oluşursa bellek sızıntısı (memory leak) ya da kaynak sızıntısı (resource leak) oluşabilir. Şüphesiz böyle fonksiyonların yazılmaması gerekir. Tabii bazen böyle fonksiyonların bir biçimde yazılması da söz konusu olabilmektedir. Örneğin: void Sample::Sample() { int *p1, *p2; //... p1 = new int[n]; p2 = new int[n]; // dikkat burada bad_alloc exception'ı oluşursa sızıntı da oluşur //... } Buradaki foo fonksiyonu hiçbir exception garantisi verememektedir. Kendi içerisinde sızıntılar bırakıp nesnenin kaynaklarını düzgün bir biçimde geri bırakamayabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Daha önce yazmış olduğumuz String sınıfı aslında exception güvenliliği bakımından sorunluydu. O sınıfın atama operatör fonksiyonları herhangi bir exception garantisi vermiyordu. Ancak diğer fonksiyonlarda bir problem yoktu. Eski atama operatör fonksiyonları şöyle yazIlmıştı: String &String::operator =(const String &r) { if (this == &r) return *this; delete[] m_str; m_str = new char[r.m_capacity]; strcpy(m_str, r.m_str); m_size = r.m_size; m_capacity = r.m_capacity; return *this; } Bu atama operatör fonksiyonu bu haliyle exception güveli değildir. Yani "basic guarantee" bile vermemektedir. Burada new işlemi başarısız olursa sızıntı oluşmaz ancak nesne kararlı bir durumda kalmamaktadır. Yani nesne destruct edilemez durumdadır. Anımsanacağı gibi "strong guarantee" nesnenin exception öncesindeki durum ile aynı durumda bırakılması anlamına gelmekteydi. Yukarıdaki atama operatör fonksiyonunu "strong guarantee" verecek biçimde şöyle düzenleyebiliriz: String &String::operator =(const String &r) { if (this == &r) return *this; char *temp = new char[r.m_capacity]; delete[] m_str; m_str = temp; strcpy(m_str, r.m_str); m_size = r.m_size; m_capacity = r.m_capacity; return *this; } Burada artık new işlemi sırasında bir exception oluşursa nesne orijinal durumunda kalmaktadır. Strong guarantee vermenin bu tür durumlardaki klasik yöntemlerinden biri "copy and swap idiom" denilen kalıbın uygulanmasıdır. Bu kalıpta önce yerel bir nesne kopya yapıcı fonksiyonu ile yaratılır, sonra yerel nesne ile asıl nesnenin veri elemanları yer değiştirilir. Bunun için genellikle sınıfta swap isimli bir üye fonksiyon bulundurulur. Örneğin String sınıfı için bu swap fonksiyonu şöyle olabilir: void String::swap(String &r) noexcept { std::swap(m_str, r.m_str); std::swap(m_size, r.m_size); std::swap(m_capacity, r.m_capacity); } Burada swap fonksiyonu içerisinde çağırdığımız swap fonksiyonları standart kütüphenedeki şablon tabanlı swap fonksiyonlarıdır. Bu fonksiyonlar no-throw garantisi vermektedir. (Başka bir deyişle bu fonksiyonlar noexcept belirleyicine sahiptir.) Bu durumda String sınıfı için "copy and swap idiom" şöyle uygulanabilir: String &String::operator =(const String &r) { String temp(r); // exception safe swap(temp); return *this; } String &String::operator =(const char *str) { String temp(str); // exception safe swap(temp); return *this; } String &String::operator =(String &&r) noexcept { swap(r); return *this; } Pekiyi bu fonksiyonlar neden "strong guarantee" vermektedir. Adım adım inceleyelim: 1) Eğer yerel nesne üzerindeki kopya yapıcı fonksiyonunda exception oluşursa henüz asıl nesnede bir değişiklik yapılmadığı için "strong guarantee" bozulmaz. 2) swap işlemi asıl nesneyle yerel nesnenin elemanlarını yer değiştirmektedir. Dolayısıyla swap zaten noexcept bir fonksiyondur. Yani "no-throw guarantee" vermektedir. 3) return *this işleminde de artık exception oluşturacak bir durum yoktur. Aşağıdaki kullanıma dikkat ediniz. int main() { String s{"ankara"}; String k; cout << s << endl; try { k = s; } catch (...) { //... } cout << k << endl; return 0; } Burada k = s işleminde bir exception oluşsa bile arık k'da hiçbir değişiklik olmayacaktır. Yani biz k'nın önceki hali ne ise onu öyle kullanmaya devam edebiliriz. Yukarıdaki örnekte biz String sınıfının atama operatör fonksiyonlarına "copy and swap" idiom yerine ilk örnekte de yaptığımız gibi bir temp göstericisi kullanarak strong uarantee verebilirdik. Ancak "copy and swap idiom" daha genel bir kullanım olanağı sağlamaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Özetle siz bir global fonksiyon ya da üye fonksiyon yazarken satır satır hangi noktalarda exception oluşabileceğini dikkate almalısınız. Mümkünse fonksiyonlarınıza "strong guarantee" vermeye çalışmalısınız. Mümkün değilse ya da efektif değilse o zaman "basic guarantee" vermeye çalışabilirsiniz. Zaten throw etmeyecek fonksiyonlarda mutlaka noexcept belirleyicisini kullanmalısınız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 93. Ders 19/08/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'a C++11 ile "kullanıcı tanımlı sabitler (user defined literals)" ismiyle yeni bir operatör fonksiyonu daha eklenmiştir. Aslında biz bu operatör fonksiyonlarından string sınıfını anlattığımız konuda kısaca bahsetmiştik. Kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonları sayesinde bir sabitin sonuna belli ekler getirerek onların belli türlerden sabit belirtmesi sağlanabilmektedir. Örneğin: string s; //... s = "ankara"s; Burada iki tırnak ifadesi normalde işleme sokulduğunda const char * türünden olacaktır. Ancak bu iki tırnak ifadesinin sonuna onunla yapışık biçimde getirilen 's' harfi bu iki tırnak ifadesinin string sınıfı türünden bir sabit olmasını sağlamaktadır. Pekiyi bu nasıl sağlanmaktadır? Aslında bir sabite bir ek getirildiğinde ismine operator "" denilen bir operatör fonksiyonu çağrılmaktadır. Bu operatör fonksiyonu üye fonksiyon biçiminde tanımlanamaz global fonksiyon biçiminde isim alanlarının içerisinde tanımlanmak zorundadır. Kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonlarının genel biçimi şöyledir: operator ""_sonek(T t) { //... } Görüldüğü gibi genel biçimde önce operator anahtar sözcüğü sonra içi boş iki tırnak ifadesi ("" biçiminde) sonra da alt tire ve programcının belirlediği bir sonek bulunmaktadır. Buradaki sonekten önce bir alt tire karakteri olduğuna dikkat ediniz. Bu alt tire karakterinden sonraki ilk karakter eğer büyük harf değilse "" ifadesinden sonra gelen sonek bu iki tırnak ifadesi ile bitişik de yazılabilir ayrı da yazılabilir. Ancak eğer alt tireden sonra getirilen sonekin ilk karakteri büyük harf ise "" ile sonek arasında boşluk bırakılmamalıdır. (Bu kuralın nedeni bir alt tire ve sonraki ilk harf büyük harf olan isimlerin "reserved" yapılmasından kaynaklanmaktadır.) Kullanıcı tanımlı sabitleri belirten operatör fonksiyonlarının parametreleri yalnızca aşağıda belirtilen türlerden olabilir: const char * unsigned long long int long double char wchar_t char8_t char16_t char32_t const char*, std::size_t const wchar_t*, std::size_t onst char8_t*, std::size_t const char16_t*, std::size_t const char32_t*, std::size_t Örneğin: T operator "" _abc(unsigned long long int); // geçerli T operator ""_abc(unsigned long long int); // geçerli T operator ""_Abc(unsigned long long int); // geçerli T operator "" _Abc(unsigned long long int); // geçersiz, alt tireden sonr ailk harf büyük harf ise bitişik yazılma zorunluluğu vardır! T operator "" _abc(int); // geçersiz, parametre int türden olamaz! T operator ""abc(int); // geçersiz, "" ifadesinden sonra alt tire karakterinin bulunması gerekir. Örneğin: class Sample { public: Sample(int val) : m_val(val) {} int val() const { return m_val;} private: int m_val; }; Sample operator ""_s(unsigned long long int val) { return Sample(val); } Burada sonunda "_s" olan tamsayı belirten sabitler Sample sınıfı türünden sabit gibi değerlendirilecektir. Aslında derleyici bu biçimde bir sabit gördüğünde niteliksiz isim arama kurallarına göre operator ""_s isimli operatör fonksiyonunu aramaktadır. Eğer bulursa alt tirenin solundaki değeri bu operatör fonksiyonuna argüman yaparak bu operatör fonksiyonunu çalıştırmaktadır. Yani örneğin 123_s biçimindeki bir sabit aslında operator ""_s(123) biçiminde bir fonksiyon çağrısı belirtmektedir. Kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonlarının parametrelerinin unsigned long long türünden olması size biraz tuhaf gelebilir. Anımsanacağı gibi -123 gibi bir ifade bir sabit belirtmemektedir. Buradaki '-' sembolü bir operatör belirtmektedir. -123 ifadesinde sabit olan kısım 123'tür. Dolayısıyla tamsayı sabitleri asla negatif olamaz. Bu nedenle tasarımda en yüksek işaretsiz tamsayı türü kullanılmıştır. Kullanıcı tanımlı sabitler eğer iki tırnaklı ifadeyle çağrılacaksa ayrıca operatör fonksiyonuna bir uzunluk parametresi daha geçilmelidir. Ancak eğer fonksiyon bir şablon biçiminde yazılmışsa bu durumda tür parametresi olmayan bir şablon parametresi de kullanılabilmektedir. Örneğin: Sample operator ""_s(const char *str, size_t size) { //... } //... Sample x; x = "123"_s; Burada operatör fonksiyonuna "123" yazısı ve aynı zamanda bu string'in uzunluğu da geçirilmektedir. Tabii yine buradaki iki tırnaklı string'in sonunda null karakter bulunmaktadır. Bu uzunluk parametresi programcı tarafından gerektiğinde kullanılabilmektedir. Böylece programcı string'in uzunluğuna ilişkin işlemler yapmak isterse bu uzunluğu bu parametreden elde edebilmektedir. Tabii kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonları da overload edilebilir. Bu operatör fonksiyonlarının isimleri operator anahtar sözcüğü "" sembolü ve _senek'ten oluşmaktadır. Örneğin: Sample operator ""_s(unsigned long long int val) { //... } Sample operator ""_s(char val) { //... } Sample operator ""_s(const char *str, size_t size) { //... } Görüldüğü gibi bu operatör fonksiyonlarının hepsi aynı faaliyet alanında bulunabilmektedir. Çünkü bunların isimleri aynı olsa da parametrik yapıları farklıdır. Yukarıdaki genel biçimden de görüleceği gibi bu operatör fonksiyonlarında "" ifadesinden sonra sonek alt tire ile başlatılmalıdır. Ancak standartlara göre alt tire ile başlamayan sonekler bulunabilir. Fakat bu sonekler standart kütüphaneye özgüdür. Örneğin standart kütüphanedeki string sınıfının sabiti belirtilirken hiç alt tire olmadan 's' soneki kullanılmıştır: string s = "ankara"s; // alt tire olmadan sonek getirme yalnızca standart kütüphaneye özgüdür Kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonları constexpr fonksiyon biçiminde de tanımlanabilir. Tabii fonksiyonun bu durumda zorunlu olmasa da derleme zamanında çağrılabilmesi sağlanmalıdır. Eğer fonksiyon bir sınıfla geri döndürüleceke sınıfın yapıcı fonksiyonalrının da constexpr olması uygun olur. Kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonlarının parametreleri ve geri dönüş değerleri temel türlere ilişkin olabilir. Yani bu fonksiyonların bir sınıf ile ilişkili olması gerekmemektedir. Örneğin: double operator ""_sr(long double val) { return sqrt(val); } double operator ""_sr(unsigned long long val) { return sqrt(val); } //... double d; d = 10_sr; cout << d << endl; // 3.16228 Kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonlarında aday fonksiyonlar yalnızca sabite ilişkin parametre türünden seçilmektedir. Örneğin: double operator ""_sr(long double val) { return sqrt(val); } Burada 10_sr biçiminde bir sabit kullanıldığında yukarıdaki operatör fonksiyonu aday fonksiyon durumunda değildir. Buradaki aday fonksiyon yalnızca unsigned long long parametresine ilişkin fonksiyondur. Dolayısıyla sabitte nokta yoksa aday fonksiyon long double parametreli olan, nokta varsa aday fonksiyon unsigned long long parametreli olan fonksiyondur. Mevcut C++ standartlarında birkaç yerde kullanıcı tanımlı sabitler kullanılmıştır. Örneğin yukarıda da belirttiğimiz gibi "anakara"s biçiminde bir sabit string sınıfı türünden sabit oluşturmaktadır. (Yani başlık dosyasında böyle bir operatör fonksiyonu bulunmaktadır.) Benzer biçimde C++11 ile birlikte karmaşık sayılar üzerinde işlem yapan sınıfı ile ilgili "i", "if" ve "il" sonekli kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonları oluşturulmuştur. "i" soneki complex türünden, "if" soneki complex türünden ve "il" soneki de complex türünden sabit oluşturmaktadır. Örneğin: complex z; z = 3i; cout << z.real() << '+' << z.imag() << 'i' << endl; // 0+3i Kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonları kütüphanesinde de çokça kullanılmıştır. Aşağıda daha önce yazmış olduğumuz Complex sayı sınıfına _i sonekli kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonları eklenmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // complex.hpp #ifndef COMPLEX_HPP_ #define COMPLEX_HPP_ #include #include namespace CSD { class Complex { public: Complex() = default; constexpr Complex(double real, double imag) : m_real{real}, m_imag{imag} {} void disp() const; friend std::ostream &operator <<(std::ostream &os, const Complex &z); friend std::istream &operator >>(std::istream &is, Complex &z); private: double m_real; double m_imag; }; constexpr Complex operator ""_i(unsigned long long imag) { return Complex(0, imag); } constexpr Complex operator ""_i(long double imag) { return Complex(0, imag); } } #endif // complex.cpp #include #include #include "complex.hpp" using namespace std; namespace CSD { ostream &operator <<(ostream &os, const Complex &z) { if (z.m_imag == 0) os << z.m_real; else { if (z.m_real != 0) { os << z.m_real; if (z.m_imag > 0) os << '+'; } if (abs(z.m_imag) != 1) os << z.m_imag; else if (z.m_imag < 0) os << '-'; os << 'i'; } return os; } istream &operator >>(istream &is, Complex &z) { return is >> z.m_real >> z.m_imag; } } // app.cpp #include #include "complex.hpp" using namespace std; using namespace CSD; int main() { cout << 3_i << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıda daha önce yazmış olduğumuz Rect sınıfına için kullanıcı tanımlı sabitlere ilişkin operatör fonksiyonu yazılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // rect.hpp #ifndef RECT_HPP_ #define RECT_HPP_ #include "point.hpp" #include "size.hpp" namespace CSD { class Rect { public: constexpr Rect(int x, int y, int width, int height) : m_pos(x, y), m_size(width, height) {} constexpr Rect(const Point &pos, const Size &size) : m_pos(pos), m_size(size) {} constexpr Point pos() const { return m_pos; } constexpr Size size() const { return m_size; } constexpr int x() const { return m_pos.x(); } constexpr int y() const { return m_pos.x(); } constexpr int width() const { return m_size.width(); } constexpr int height() const { return m_size.height(); } constexpr Point bottom_right() const { return m_pos.add(m_size.width(), m_size.height()); } bool contains(const Point &pt) const; bool contains(int x, int y) const; bool contains(const Rect &rect) const; void move(int x, int y) { m_pos.move(x, y); } void move(const Point &pt) { m_pos.move(pt); } private: Point m_pos; Size m_size; }; Rect operator ""_r(const char *str, std::size_t size); } #endif // rect.cpp #include #include #include #include #include "rect.hpp" using namespace std; namespace CSD { bool Rect::contains(const Point &pt) const { return pt.x() > m_pos.x() && pt.x() < m_pos.x() + m_size.width() && pt.y() > m_pos.y() && pt.y() < m_pos.y() + m_size.height(); } bool Rect::contains(int x, int y) const { return x > m_pos.x() && x < m_pos.x() + m_size.width() && y > m_pos.y() && y < m_pos.y() + m_size.height(); } bool Rect::contains(const Rect &rect) const { return contains(rect.m_pos) && contains(bottom_right()); } Rect operator ""_r(const char *str, std::size_t size) { char *rect_str; char *tok; int x, y, width, height; rect_str = new char[size + 1]; strcpy(rect_str, str); if ((tok = strtok(rect_str, " ")) == nullptr) throw invalid_argument("invalid rect"); x = atoi(tok); if ((tok = strtok(nullptr, " ")) == nullptr) throw invalid_argument("invalid rect"); y = atoi(tok); if ((tok = strtok(nullptr, " ")) == nullptr) throw invalid_argument("invalid rect"); width = atoi(tok); if ((tok = strtok(nullptr, " ")) == nullptr) throw invalid_argument("invalid rect"); height = atoi(tok); delete[] rect_str; return Rect(x, y, width, height); } } // app.cpp #include #include "rect.hpp" using namespace std; using namespace CSD; int main() { Rect rect = "10 10 20 20"_r; cout << rect.x() << ", " << rect.y() << ", " << rect.width() << ", " << rect.height() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 94. Ders 21/08/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Kursumuzun bu bölümünde "şablon (template)" işlemleri üzerinde duracağız. Şablonlar konusu C++'ta ana fikir olarak basit ancak ayrıntı bağlamında karmaşık olan bir konudur. Biz kursumuzda belli bir derinlikte bu şablon işlemlerini ele alacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bazı fonksiyonların değişik türler için içi aynı olacak biçimde yeniden yazılması gerekebilmektedir. Örneğin int bir dizinin en büyük elemanına geri dönen bir fonksiyon yazmak isteyelim: int getmax(const int *pa, size_t size) { int max; max = pa[0]; for (size_t i = 1; i < size; ++i) if (pa[i] > max) max = pa[i]; return max; } Şimdi biz double bir dizinin en büyük elemanını elde etmek istesek aynı fonksiyondan double için bir tane daha yazmak zorundayız: double getmax(const double *pa, size_t size) { double max; max = pa[0]; for (size_t i = 1; i < size; ++i) if (pa[i] > max) max = pa[i]; return max; } Bu kez de long bir dizinin en büyük elemanını elde etmek isteyelim. Bu durumda içi aynı olan ancak parametresi long * türünden olan yeni bir fonksiyon daha yazmamız gerekir: long getmax(const long *pa, size_t size) { long max; max = pa[0]; for (size_t i = 1; i < size; ++i) if (pa[i] > max) max = pa[i]; return max; } Görüldüğü gibi burada üç fonksiyonun da içi tamamen ayndır. Yalnızca tür farklılığı yüzünden programcı yeniden aynı fonksiyondan yazmak zorunda kalmıştır. Tabii aslında bazen tek bir fonksiyon farklı türlerele çalışabilir hale de getirilebilir. Ancak bu tür fonksiyonlar "kullanımı zor" ve "yavaş" olma eğilimindedir. Örneğin: void *getmax_general(const void *pa, size_t width, size_t size, int (*cmp)(const void *, const void *)) { const void *maxaddr = pa; const char *pc = reinterpret_cast(pa); for (size_t i = 1; i < size; ++i) if (cmp(pc + i * width, maxaddr) > 0) maxaddr = pc + i * width; return const_cast(maxaddr); } İşte şablon (template) "içi aynı olan fakat parametrik türleri" farklı olan fonksiyonların ve sınıfların kullanılan her tür için yazılmasını kolaylaştırmak amacıyla oluşturulmuş bir mekanizmadır. Şablon mekanizmasının Java ve C# gibi dillerdeki mantıksal benzerine "generic" denilmektedir. Bugün artık statik tür sistemine sahip yeni programlama dillerinin neredeyse hepsinde mantıksal olarak C++'takine benzer bir şablon mekanizması vardır. Tabii bu dillerin şablon mekanizmaları sentaks ve işleyiş bakımından C++'takinden farklıdır. Ancak ana tema benzerdir. Biz İngilizce "template" sözcüğünün Türkçe karşılığı olarak "şablon" sözcüğünü kullanıyoruz. İngilizce "template" sözcüğü "templıt" ya da "templeyt" biçiminde okunabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int getmax(const int *pa, size_t size) { int max; max = pa[0]; for (size_t i = 1; i < size; ++i) if (pa[i] > max) max = pa[i]; return max; } double getmax(const double *pa, size_t size) { double max; max = pa[0]; for (size_t i = 1; i < size; ++i) if (pa[i] > max) max = pa[i]; return max; } long getmax(const long *pa, size_t size) { long max; max = pa[0]; for (size_t i = 1; i < size; ++i) if (pa[i] > max) max = pa[i]; return max; } void *getmax_general(const void *pa, size_t width, size_t size, int (*cmp)(const void *, const void *)) { const void *maxaddr = pa; const char *pc = reinterpret_cast(pa); for (size_t i = 1; i < size; ++i) if (cmp(pc + i * width, maxaddr) > 0) maxaddr = pc + i * width; return const_cast(maxaddr); } int mycmp(const void *pv1, const void *pv2); int main() { int a[] = {5, 3, 78, 23, 12}; double b[] = {4, 6.7, 2.8, 7.3, 5.2}; long c[] = {3400, 3000, 1200, 270, 9000}; int maxi; double maxd; long maxl; int *pi; maxi = getmax(a, 5); cout << maxi << endl; maxd = getmax(b, 5); cout << maxd << endl; maxl = getmax(c, 5); cout << maxl << endl; pi = reinterpret_cast(getmax_general(a, sizeof(int), 5, mycmp)); cout << *pi << endl; return 0; } int mycmp(const void *pv1, const void *pv2) { const int *pi1 = reinterpret_cast(pv1); const int *pi2 = reinterpret_cast(pv2); if (*pi1 > *pi2) return 1; if (*pi1 < *pi2) return -1; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şablon mekanizması ikiye ayrılmaktadır: 1) Fonksiyon Şablonları (Function Templates) 2) Sınıf Şablonları (Class Templates) Fonksiyon şablonları tek bir fonksiyonun şablon olarak yazılması anlamına gelir. Sınıf şablonları ise bir sınıfın tamamının şablon olarak yazılması anlamına gelir. Biz önce fonksiyon şablonları üzerinde sonra da sınıf şablonları üzerinde duracağız. C++'ta standart öncesinde "şablon fonksiyon" teriminin mi yoksa "fonksiyon şablonu" teriminin mi, "şablon sınıf" teriminin mi yoksa "sınıf şablonu" teriminin mi kullanılacağı belirsizdi. Farklı yazarlar farklı terimleri kullanabiliyordu. Bu terim karmaşası ilk standart olan C++98'de giderilmeye çalışımıştır. Ancak bu bakımdan yine de bu standarta da bazı çelişkiler vardır. C++03 ile bu terim tamamen oturtulmuştur. Artık bu terimler resmi olarak "fonksiyon şablonu (function template)" ve "sınıf şablonu (class template)" biçiminde ifade edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyon şablonlarında fonksiyon bildirimi bir şablon bildirimi ile başlatılır. Şablon bildirimi template anahtar sözcüğünden sonra açısal parantezler içerisinde şablon parametreleri belirtilerek yapılmaktadır. Şablon parametreleri aslında tür belirten (type specifier) parametrelerdir (tür belirtmeyen şablon parametreleri de olabilmektedir). Tür belirten şablon parametrelerinden önce "class" ya da "typename" anahtar sözcükleri getirilir. Bu iki anahtar sözcük arasında hiçbir farklılık yoktur. Bir fonksiyonun genel biçimi şöyledir: template <şablon_tür_parametre_listesi> fonksiyonun_geri_dönüş değerinin_türü fonksiyon_ismi(parametre_bildirimi) { //... } Burada şablon tür parametreleri class ya da typename anahtar sözcüklerinden sonra bir değişken ismi biçiminde olmalıdır. Örneğin: template T getmax(const T *p, size_t size) { //... } Burada T şablon şablon parametresidir. Şablon parametreleri herhangi bir biçimde isimlendirilebilir. Ancak genellikle programcılar tek karakterli T, K gibi büyük harf isimlendirmeyi tercih etmektedir. Şablon parametrelerinde önce class anahtar sözcüğü yerine typename anahtar sözcüğü de kullanılabilirdi. Örneğin: template T getmax(const T *p, size_t size) { //... } Standart öncesinde uzunca bir süre şablon parametresinden önce yalnızca class anahtar sözcüğü kullanılabiliyordu. Sonra typename anahtar sözcüğü de daha okunabilir olduğu gerekçesiyle dile eklendi. Bugün her iki anahtar sözcük de geçerli biçimde kullanılabilmektedir. Ancak biz kursumuzda daha çok typename anahtar sözcüğünü kullanacağız. Fonksiyon şablonundaki şablon parametreleri birden fazla olabilir. Örneğin: template void foo(T a, K b) { //... } Bazı programcılar şablon bildirimi ile fonksiyon bildirimini aynı satırda yazmayı tercih etmektedir. Örneğin: template void foo(T a, K b) { //... } Burada yine her şablon parametresinden önce class ya da typename anahtar sözcüğü bulundurulmak zorundadır. Fonksiyon şablonlarında şablon parametreleri yukarıda da belirttiğimiz gibi tür belirten biz sözcük (type specifier) gibi fonksiyonun içerisinde ve parametrik yapısında kullanılabilir. Örneğin: template T getmax(const T *pa, size_t size) { T max = pa[0]; for (size_t i = 1; i < size; ++i) if (max < pa[i]) max = pa[i]; return max; } Burada fonksiyon T türüne dayalı bir biçimde yazılmıştır. Yani türden bağımsız genel bir fonksiyon yazımı söz konusudur. Bir fonksiyon şablonu çağrıldığında derleyici önce fonksiyon şablonundaki şablon parametrelerinin o çağrıma göre gerçekte hangi tür olduğunu tespit etmeye çalışır. Bu sürece standartlarda "template argument deduction" denilmektedir. Eğer bu tespit başarılı bir biçimde yapılırsa derleyici fonksiyonunu temel alarak ilgili tür için o fonksiyonu yazar. Bu sürece de "function template instantiation" denilmektedir. Örneğin yukarıdaki fonksiyonu şöyle çağrımış olalım: int a[5] = {3, 6, 2, 9, 1}; int max; //... max = getmax(a, 5); Derleyici böylesi bir çağrımda önce T türünün gerçek türünü belirlemeye çalışacaktır. Burada a ifadesi int * türündendir. Fonksiyon şablonunun buna karşı gelen parametresi ise const T * türündendir. Yani aşağıdaki gibi bir ilkdeğer verme söz konusudur: const T * = int *; Burada bu ilkdeğer verme işleminin geçerli olabilmesi için T türünün int olması gerekir. O halde derleyici T türünü int kabul ederek bu fonksiyondan bir tane yazacaktır. İşte bu sürece yukarıda da belirttiğimiz gibi İngilizce "instantiation" denilmektedir: int getmax(const int *pa, size_t size) { int max = pa[0]; for (size_t i = 1; i < size; ++i) if (max < pa[i]) max = pa[i]; return max; } Biz derleyicinin fonksiyon şablonuna bakarak ilgili tür için o şablon için fonksiyon yazılması için İngilizce "function template instantiation" yerine Türkçe "fonksiyon şablonun açılması" terimini kullanacağız. Fonksiyon şablonu bir kez programcı tarafındna yazılmaktadır. Derleyici ise her farklı tür için açım yapmaktadır. Şüphesiz derleyici belli bir tür için yalnızca tek bir fonksiyon açmaktadır. Örneğin biz yukkarıdaki germa fonksiyonunu ikinci kez yine int bir dizinin adresiyle çağırsak derleyici bu fonksiyondan yeniden yazmaz. Çünk zaten daha önce int türü için bir açım yapmıştır. Şimdi biz yukarıdaki fonksiyon şablonunu aşağıdaki gibi de çağırmış olalım: double b[5] = {3.4, 6.2, 1.9, 9.32, 12.1}; double max; //... max = getmax(b, 5); Bu kez derleyici fonksiyon şablonunun T parametresinin double olması gerektiğini düünecek ve aynı fonksiyondan T yerine double koyarak yazacaktır. Aşağıdaki örnekte getmax fonksiyon şablonunun örnek kullanımını görüyorsunuz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; template T getmax(const T *pa, size_t size) { T max; max = pa[0]; for (size_t i = 1; i < size; ++i) if (pa[i] > max) max = pa[i]; return max; } int main() { int a[] = {5, 3, 78, 23, 12}; double b[] = {4, 6.7, 2.8, 7.3, 5.2}; long c[] = {3400, 3000, 1200, 270, 9000}; int maxi; double maxd; long maxl; maxi = getmax(a, 5); cout << maxi << endl; maxd = getmax(b, 5); cout << maxd << endl; maxl = getmax(c, 5); cout << maxl << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çağrılma ifadesinden fonksiyon şablonunun şablon parametresinin türünün tespit edilmesinin (template argument deduction) bazı ayrıntıları vardır. Biz bu ayrıntılara daha sonra değineceğiz. Şimdi bir sayının mutlak değerine geri dönen bir fonksiyon şablonu yazalım: template T abs(T val) { if (val < 0) return -val; return val; } Burada yine fonksiyon bir T türüne dayalı biçimde yazılmıştır. Biz abs fonksiyonunu hangi türden argüman vererek çağırsak T o türden olacaktır ve derleyici o türden yeni abs fonksiyonu yazacaktır ("instantiate" edecektir). --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; template T abs(T val) { if (val < 0) return -val; return val; } int main() { int a; double b; long c; a = abs(-10); cout << a << endl; b = abs(-3.14); cout << a << endl; c = abs(-123L); cout << c << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Örneğin iki nesnenin içerisindeki değeri yer değiştiren myswap isimli bir fonksiyon yazalım (zaten standart kütüphanede swap isminde böyle bir fonksiyon vardır): template void myswap(T &a, T &b) { T temp = a; a = b; b = temp; } Şimdi fonksiyonumuzu şöyle çağırmış olalım: int a = 10, b = 20; //... myswap(a, b); Burada derleyici önce şablon parametresinin gerçek türünü tespit etmeye çalışacaktır. Aşağıdaki gibi ilkdeğer vermeler söz konusudur: T & = int T & = int Bu durumda T türü int olarak tespit edilecektir. O halde derleyici bu şablona bakarak aşağıdaki gibi bir fonksiyon yazacaktır: void myswap(int &a, int &b) { int temp = a; a = b; b = temp; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; template void myswap(T &a, T &b) { T temp = a; a = b; b = temp; } int main() { int a = 10, b = 20; double x = 3.14, y = 6.28; myswap(a, b); cout << "a = " << a << ", b = " << b << endl; myswap(x, y); cout << "x = " << x << ", y = " << y << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Derleyici çağrılma ifadesindeki argümanalara bakarak şablon parametrelerini tutarlı bir biçimde belirleyemezse bu durum error oluşturacaktır. Örneğin: template void disp(T a, T b) { cout << a << ", " << b << endl; } Biz bu fonksiyonu şöyle çağırmış olalım: int a = 10; double b = 3.14; disp(a, b); // error! Burada bir tutarsızlık vardır. Birinci argümana bakıldığında T şablon parametresi int olmalıdır, ancak ikinci argümana bakıldığında T şablon parametresi double olmalıdır. Derleyici T şablon parametresinin türünü tutarlı bir biçimde belirleyemediğinden dolayı çağrı geçersizdir. Şimdi, aynı fonksiyonu şöyle yazmış olalım: template void disp(T a, K b) { cout << a << ", " << b << endl; } Burada artık iki şablon parametresi olduğuna dikkat ediniz. Dolayısıyla aşağıdaki çağrı artık geçerlidir: int a = 10; double b = 3.14; disp(a, b); // geçerli, T = int, K = double Bu çağrıda derleyici T türünü int, K türünü double olarak tespit edecektir ("deduce" edecektir). Tabii T ve K parametreleri aynı türü de temsil edebilir. Örneğin: disp(10, 20); Burada hem T hem de K int türü olarak tespit edilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; template void disp(T a, K b) { cout << a << ", " << b << endl; } int main() { int a = 10; double b = 3.14; disp(a, b); // geçerli, T = int, K = double disp(10, 20); // geçerli, T = int, K = int return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şablon bildirimleri derleyici tarafından her derleme işleminde görülmelidir. Bu nedenle fonksiyon şablonları kütüphaneler içerisine yerleştirilemez. Onların tipik olarak başlık dosyalarına yerleştirilmesi uygun olur. Böylece derleyici derleme işlemi sırasında onların kodunu görebilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // util.hpp #ifndef UTIL_HPP_ #define UTIL_HPP_ namespace CSD { template T abs(T val) { if (val < 0) return -val; return val; } template void swap(T &a, T &b) { T temp = a; a = b; b = temp; } template T getmax(const T *pa, size_t size) { T max; max = pa[0]; for (size_t i = 1; i < size; ++i) if (pa[i] > max) max = pa[i]; return max; } } #endif // app.cpp #include #include "util.hpp" using namespace std; int main() { int a = 10, b = 20; int x[] = {1, 6, 34, 12, 21}; CSD::swap(a, b); auto result = CSD::getmax(x, 5); //... return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın standart kütüphanesi tamamen şablon esasına dayandırılmıştır. Orada pek çok hazır fonksiyon ve sınıf şablonu bulunmaktadır. Bu hazır fonksiyon şablonları ve sınıf şablonları "iteratör (iterator)" denilen bir kavram kullanılarak gerçekleştirilmiştir. İteratör "bir gösterici gibi davranan gerçek bir gösterici ya da sınıftır". Standart kütüphanedeki fonksiyon şablonları bir diziyi parametre olarak alacakları zaman onun başlangıç ve bitiş bitiş iteratörlerini bizden isterler. Eğer dizi normal bir dizi ise başlangıç iteratörü dizinin ilk elemanının adresi, bitiş iteratörü ise dizinin son elemanından sonraki elamanın adresidir. Örneğin aşağıdaki gibi bir dizi söz konusu olsun: int a[] = {10, 12, 11, 4, 9}; Bu dizinin başlangıç ve bitiş iteratörleri şöyledir: 10 32 11 4 9 ? ^ ^ Başlangıç iteratörü Bitiş iteratörü Bu iteratörler a ve a + 5 ifadesiyle oluşturulabilir. Bu örneğimizde iteratör gerçek birer gösterici durumundadır. Ancak yukarıda da belirttiğimiz gibi iteratörler göstericileri taklit eden sınıflar biçiminde de oluşturulabilmektedir. Örneğin başlık dosyası içerisinde sort isimli fonksiyon şablonu aşağıdaki parametrik yapıya uygun tanımlanmıştır: template void sort(RandomIt first, RandomIt last); Burada fonksiyonun bir tane şablon parametresi vardır. Fonksiyon bir diziyi sort etme iddiasındadır. Bizden dizinin başlangıcına ve bitişine ilişkin iki tane iteratörü ister. Eğer biz bu fonksiyona gerçek bir dizi vereceksek başlangıç iteratörü dizinin başlangıç adresi, bitiş iteratörü ise dizinin son elemanından sonraki elemanın adresidir. Biz bu fonksiyonu aşağıdaki gibi kullanabiliriz: int a[10] = {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; sort(a, a + 10); Burada dikkat edilmesi gereken nokta fonksiyonun "dizinin başlangıç adresini ve uzunluğunu değil, başlangıç adresini ve son elemandan sonraki adresi" parametre olarak istemesidir. Biz bu iki adrese "başlangıç ve bitiş iteratörleri" diyoruz. Bu çağrıda RandomIt isimli şablon parametresi int * olarak tespit edilecektir. Yani derleyici RandomIt yerine int * türünü yerleştirerek fonksiyonu açacaktır: void sort(int *first, int *last) { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int a[10] = {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; sort(a, a + 10); for (int x : a) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart kütaphanedeki iteratör kavramı bizim kütüphanedeki fonksiyon şablonlarını hem normal dizilerle hem de C++'ın standart veri yapılarıyla (bunlara "container" da denilmektedir) kullanmamıza olanak sağlamaktadır. Bu nedenle biz burada "dizi" kavramı yerine genel bir kavram oluşturmak için "dizilim" kavramını tercih edeceğiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 95. Ders 4/09/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Örneğin standart kütüphanedeki find isimli fonksiyon şablonunun parametrik yapısı şöyledir: template InputIt find(InputIterator first, InputIterator last, const T &value); find fonksiyonu bir dizilim içerisinde bir elemanı sıralı (sequential) biçimde arar. Eğer elemaı dizilimde bulursa onun iteratörünü bize verir. Fonksiyon bizden dizilimin başlangıç ve bitiş iteratörlerini ve aranacak değeri parametre olarak almaktadır. Eğer biz fonksiyonu normal dizilerle kullanıyorsak başlangıç iteratörü dizinin ilk elemanının adresi, bitiş iteratörü ise son elemandan sonraki elemanın adresi olur. Örneğin biz int bir dizi içerisindeki belli bir elemanı bu find fonksiyonu ile şöyle arayabiliriz: int a[10] = {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; int *pi; pi = find(a, a + 10, 61); cout << *pi << endl; // 61 Burada find fonksiyonu eğer elemanı bulursa onun iteratörünü bize verecektir. Normal dizilerde iteratör bir adres durumundadır. Dolaısıyla biz find fonksiyonunu normal dizilerle kullanırsak fonksiyon bize başarı durumunda bulunan elemanın dizi içerisindeki adresini verecektir. Normal olarak adrese geri dönen fonksiyolar başarısız olduğunda NULL adrese geri dönmektedir. Ancak iteratör kullanımı söz konusu olduğunda başarısız olan fonksiyonlar "bitiş iteratörüne" geri dönerler. Anımsanacağı gibi dizilerde bitiş iteratörleri son elemandna sonraki elemanın adresini belirtmektedir. O halde yukarıdaki örnekte find fonksiyonunun başarısı şöyle kontrol edilebilir: //... pi = find(a, a + 10, 61); if (pi == a + 10) { // cannot find } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int a[10] = {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; int *pi; pi = find(a, a + 10, 100); if (pi == a + 10) cout << "cannot find!.." << endl; else cout << "found: " << *pi << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi C++'ın standart kütüphanesindeki fonksiyon şablonları neden iteratör kavramı kullanılarak gerçekleştirilmiştir? İşte bunun nedeni bu fonksiyonların hem normal dizilerle hem de "container" denilen veri yapılarını temsil eden vector gibi, array gibi, list gibi sınıflarla kullanımını sağlamktır. Örneğin biz daha önce vector sınıfını ve array sınıfını görmüştük. Genel olarak C++'ın "container" denilen vari yapılarını gerçekleştiren sınıf şablonlarının begin isimli üye fonksiyonları dizilimin başlangıç itertaörünü end isimli üye fonksiyonları ise bitiş iteratörünü vermektedir. Bu sınıfların iteratörlerinin gerçek bir göstetrici mi yoksa bir sınıf mı olduğunu bizim bilmemiz gerekmemektedir. Bu sınıfların iteratörlerinin türleri sınıf içerisinde iterator ismiyle typedef edilmiştir. Bu durumda örneğin biz aynı find fonksiyonunu vector sınıfı ile aşağıdaki gibi kullanabiliriz: vector v {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; vector::iterator iter; iter = find(v.begin(), v.end(), 100); if (iter != v.end()) cout << "found: " << *iter << endl; else cout << "cannot find!.." << endl; Burada find fonksiyonunun aşağıdaki gibi çağrıldığına dikkat ediniz: iter = find(v.begin(), v.end(), 100); find fonksiyonu her zaman "dizlimin başlangıç ve bitiş iteratörlerini" parametre olarak almaktadır. Biz de fonksiyona bu iteratörleri v.begin() ve v.end() biçiminde geçtik. find fonksiyonu geri dönüş değeri olarak bir iteratör vermektedir. Biz de geri dönüş değerini aşağıdaki bigi tanımladığımız bir iteratör nesnesine atadık: vector::iterator iter; //... iter = find(v.begin(), v.end(), 100); Burada vector::iterator türünün ne olduğunu bilmemize gerek yoktur. Bu tür ya gerçekten bir adres türünü belirtir ya da bir gösterici gibi davranan sınıf türünü belirtir. Fonksiyonun başarısını aşağıdaki gibi kontrol ettik: if (iter != v.end()) cout << "found: " << *iter << endl; else cout << "cannot find!.." << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { vector v {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; vector::iterator iter; iter = find(v.begin(), v.end(), 100); if (iter != v.end()) cout << "found: " << *iter << endl; else cout << "cannot find!.." << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şİmdi de sort fonksiyonunu vector sınıfyla kullanalım: vector v {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; sort(v.begin(), v.end()); for (vector::size_type i = 0; i < v.size(); ++i) cout << v[i] << " "; cout << endl; Gördüğünüz gibi biz sort fonksiyonunu hem normal dizilerle hem de vector sınıfı ile kullabilmekteyiz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { vector v {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; sort(v.begin(), v.end()); for (vector::size_type i = 0; i < v.size(); ++i) cout << v[i] << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Mademki iteratörler gerçek bir gösterici ya da göstericiyi taklit eden birer sınıf belirtmektedir. O halde bir "container" nesnesinin ilk başlangıç iteratörünü alıp onu sürekli artırarak o "container" nesnesinin tüm elemanlarına erişebiliriz. Bu işlem tipik olarak aşağıaki gibi bir döngü yoluyla yapılabilmektedir: vector v {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; for (vector::iterator iter = v.begin(); iter != v.end(); ++iter) cout << *iter << " "; cout << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { vector v {4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; sort(v.begin(), v.end()); for (vector::iterator iter = v.begin(); iter != v.end(); ++iter) cout << *iter << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de find isimli fonksiyon şablonunu biz yazalım. Aslında tek yapacağımız başlangıç iteratöründen bitiş iteratörüne kadar dizilim içerisinde ilgili değeri aramaktır. Basit bir gerçekleştirim şöyle olabilir: template InputIt myfind(InputIt first, InputIt last, const T &value) { for (; first != last; ++first) if (*first == value) break; return first; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; template InputIt myfind(InputIt first, InputIt last, const T &value) { for (; first != last; ++first) if (*first == value) break; return first; } int main() { vector v{4, 76, 23, 12, 9, 61, 23, 7, 43, 33}; vector::iterator iter; iter = find(v.begin(), v.end(), 61.0); if (iter == v.end()) cout << "cannot find!.." << endl; else { for (; iter != v.end(); ++iter) cout << *iter << " "; cout << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Stnadart kütüphanedeki fonksiyon şablonlarında iteratörler üzerinde bazı işlemler uygulanmaktadır. Bu durumda ilgili fonkisyonun kullanılabilmesi için elimizdeki iteratörün o fonksiyonun uyguladığı birtakım işlemleri destekliyor olması gerekir. Örneğin yukarıda yazmış olduğumuz find fonksiyonuna dikkat ediniz: template InputIt myfind(InputIterator first, InputIterator last, const T &value) { for (; first != last; ++first) if (*first == value) break; return first; } Bu fonksiyonda biz yalnızca ilgili iteratörü ++ operatöryle bir artırıp * operatörüyle o iteratörün gösterdiği nesneye eriştik. Yani ğer eimizdeki iteratör ++ operatör fonksiyonunu destekliyorsa ve içeriğine * ile erişiliebiliyorsa bu iteratör yazdığımız find fonksiyonunda kullanılabilir. İşte C++ standartlarında fonksiyonlarla kullanılabilecek iteratörlerin hangi özelliklere sahip olduğu "iteratör grupları" ile belirlenmiştir. Tipik olarak iteratörler şu gruplara ayrılmaktadır: 1) Input İtearatörleri (Input Iterators): Eğer bir iteratör ++ operatörüyle artırılabiliyorsa * operatörün gösterdiği elemana erişilebiliyorsa böyle iteratörlere input iteratörleri" denilmektedir. 2) Output İteratörleri (Output Iterators): Eğer bir iteratör ++ operatörüyle artırılabiliyorsa * operatörün gösterdiği elemana erişilip oraya yeni bir değer yazılabiliyorsa bu tür iteratorlere "Output İteratörleri" denilmektedir. İnput iteratörleri ile output iteratörleri arasındaki tek fark birinin okumaya izin verebilmesi diğerinin de yazmaya izin verebilmesidir. Tabii bir iteratör hem input iteratör hem de output iteratör gibi kullanılabilecek yetemeğe sahip olabilir. 3) Forward İteratörler (Forward Iterators): Bir iteratör ++ operatöryle sonuna kadar ilerletilebiliyorsa genel olarak böyle iteratörlere "forward iteratörler" de denilmektedir. 4) Bidirectional İteratörler (Bidirectional Iteratos): Bir iteratör hem ++ hem de -- operatörünü destekliyorsa böyle iteratörlere "bidirectional iterator" denilmektedir. 5) Random Acces İteratörler (Random Access Iterators): Bir iterator bir tamsayıyla toplanıp çıkartılıyorsa ve köşeli parantez operatörünü destekliyorsa bu tür iteratörlere "random access iteratörler" denilmektedir. Random access iteratörler bidirectional iteratörleri kapsamakta ve bidirectional iteratörler de forward iteratörleri kapsamaktadır. Standartlarda her fonksiyonun hangi tür iteratörlerle kullanılabileceği belirtilmiştir. Göstericiler random access iteratör gibi ele alınmaktadır. Ayrıca standartlarda container sınıfların iteratörlerinin (yani begin ve end üye fonksiyonlarıyla elde edilen ietaörlerin) türleri de belirtilmiştir. Örneğin vector sınıfının iteratörleri "random access" iteratörlerdir. find fonksiyonu "input iteratöre" gereksinim duyduğuna göre ve "random access iteratörler" "input iteratörleri" kapsadığına göre biz vector iteratörlerini find fonksiyonunda kullanabiliriz. sort fonksiyonun prototipine dikkat ediniz: template void sort(RandomAccessIterator first, RandomAccessIterator last); Burada bu prototipten sort fonksiyonun "random access iteratör" istediği anlaşılmaktadır. Biz bu sort fonksiyonunu vector sınıfı ile kullanabiliriz. Çünkü vector sınıfının iteratörleri "random access" iteratörlerdir. Ancak biz bu fonksiyonu list sınıfı ile kullanamayız. Çünkü list sınıfının iteratörleeri "birectional" iteratörlerdir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- başlık dosyasındaki copy isimli fonksiyon şablonu genel kopyalama yapmakta kullanılmaktadır. Fonksiyonun prototipi şöyledir: template constexpr OutputIterator copy(InputIterator first, InputIterator last, OutputIterator result); Görüldüğü gibi copy fonksiyonuun iki şablon parametresi vardır. Bu şablon parametreleri "input ve output" iteratör kategorilerine ilişkin iteratörbelirtmektedir. Fonksiyonun ilk iki parametresi kaynak dizilimin başlangıç ve bitil iteratörlerini, üçüncü parametresi ise hedef dizilimin başlangıç iteratörünü almaktadır. İteratör türlerine bakıldığında buradaki copy fonksiyonunun kaynak ve hedef dizilimdeki iteratörleri ++ ile operatörü ile artırarak kopyalama yaptığı anlaşılmaktadır. copy fonksiyonu hedef dizilimde kopyalama sonrasındaki kalınan yere ilişkin iteratör ile geri dönmektedir. copy fonksiyonunu normal bir dizi ile aşağıdaki gibi kullanabiliriz: int x[5] = {10, 20, 30, 40, 50}, y[5]; copy(x, x + 5, y); Bir vector nesnesi ile aşağıdaki gibi kullanabiliriz: vector a{4, 76, 23, 12, 21}; vector b; b.resize(5); copy(a.begin(), a.end(), b.begin()); for (vector::size_type i = 0; i < b.size(); ++i) cout << b[i] << " "; cout << endl; Tabii kopyalamanın yapılabilmesi için hedef dizilimin kopyalanacak miktarda elemana sahip olması gerekir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { vector a{4, 76, 23, 12, 21}; vector b; b.resize(5); copy(a.begin(), a.end(), b.begin()); for (vector::size_type i = 0; i < b.size(); ++i) cout << b[i] << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de copy fonksiyonunu biz kendimiz yazalım. Basit bir yazım şöyle olabilir: template constexpr OutputIterator mycopy(InputIterator first, InputIterator last, OutputIterator result) { for (; first != last; ++first) *result++ = *first; return result; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; template constexpr OutputIterator mycopy(InputIterator first, InputIterator last, OutputIterator result) { for (; first != last; ++first) *result++ = *first; return result; } int main() { vector a{4, 76, 23, 12, 21}; vector b; b.resize(5); mycopy(a.begin(), a.end(), b.begin()); for (vector::size_type i = 0; i < b.size(); ++i) cout << b[i] << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyon şablonu çağrılırken şablon türleri açısal parantezler içerisinde açıkça (explicit) da belirtilebilmektedir. Bu durumda şablon parametreleri argümanlardan hareketle akıl yürütme (deduction) belirlenmez. Programcının açısal parantez içerisinde belirttiği türler dikkate alınır. Örneğib: template void foo(T a) { cout << a << endl; } Burada biz fonksiyonu aşağıdaki gibi çağırmı olalım: foo(123); Bu durumda derleyici T türünün int olduğunu belirleyecektir. Ancak biz bu türü açıkça açısal parantezler içerisinde aşağıdaki gibi de kendimiz belirtebiliriz: foo(123); Burada 123 int türden olmasına karşın proramcı T türünün açıkça double olduğunu belirtmiştir. Artık T türü double olacak biçimde açık yapılacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; template void foo(T a) { cout << a << endl; } int main() { foo(10); // T = int foo(1.2); // T = double foo(100); // T = double return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Açısal parantezler içerisinde şablon parametreleri bazen mecburen belirtilmektedir. Örneğin fonksiyon şablonunun şablon parametresine ilişkin bir parametre değişkeni olmayabilir. Bu durumda derleyici şablon parametresini otomatik belirleyemez. Örneğin: template void foo() { T a; //... } Burada foo fonksiyonun şablon parametresi cinsinden bir parametre değişkeni olmadığı için derleyici akıl yürütme yoluyla böyle bir belirleme yapamayacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; template void foo() { T a(); cout << a << endl; } int main() { foo(); // error! foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Eğer şablon parametresi fonksiyonun geri dönüş değerinde kullanılmışsa ancak parametrelerde kullanılmamışsa yine açıkça belirtme zorunlu hale gelmektedir. Örneğin: template R add(T a, T b) { return a + b; } Biz add fonksiyonuna argüman girdiğimizde derleyici T şablon parametresinin türünü otomatik belirleyebilir ancak R şablon parametresinin türünü otomatik belirleyemez. Bu durumda bizim açıkça belirleme yapmamız gerekir. Örneğin: double result; result = add(10, 20); cout << result << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; template T foo() { cout << "foo" << endl; return T(); } int main() { int result; result = foo(); cout << result; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Programcı ilk n tane şablon parametresini açıkça belirtip geri kalanlarını derleyicinin tespit etmesini de isteyebilir. Aşağıdaki örnekte R şablon parametresi açıkça belirtilmiş ancak T ve K şablon şablon parametresi derleyici tarafından belirlenmiştir: template R add(T a, K b) { return a + b; } //... double result; result = add(10, 20.2); Burada R şablon parametresi açıkça double olarak belirtilmiştir. Ancak T ve K şablon parametreleri otomatik olarak tespit edilmektedir. Böylece tespit edilen şablon parametreleri şöyledir: R ---> double (açıkça belirtilmiş) T ---> int (argümandan belirlenmiştir) K ---> double (argümandan belirlenmiştir> Burada bir noktayı bir daha vurgulamak istiyoruz. Bu biçimde şablon parametrelerinin yalnızca ilk n tanesi açıkça belirtilebilir. Örneğin aşağıdaki gibi bir sentaks geçerlidi değildir: result = add(10, 20.2); // geçersiz! böyle bir sentaks yok --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; template R add(T a, K b) { return a + b; } int main() { double result; result = add(10, 20.2); cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 96. Ders 17/09/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sablon parametreleri default değerler de alabilmektedir. Eğer bir şablon parametresi otomatik belirlenemiyorsa verilen default değerler kullanılmaktadır. Örneğin: template T foo() { return T(); } Burada fonksiyonu T türünü belirtmeden aşağıdaki gibi çağırmış olalım: auto result = foo(); // geçerli T => double olarak belirlenecek Şablon parametresi olan T açıkça belirtilmediği için default değer olan double kullanılacaktır. Yani yukarıdaki çağrı aşağıdakiyle tamamen eşdeğerdir: auto result = foo(); Aslında default tür ile çağrım yaparken açısal parantezlerin içini boş da bırakabilirdik: auto result = foo<>(); // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; template K foo(T a) { cout << a << endl; return K(); } int main() { int result; result = foo(10.2); // T = double, K = int cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şablon parametresi için belirtilen default değer "ilgili şablon parametresinin türü tespit edilememişse" kullanılmaktadır. Eğer şablon parametresinin türü açıkça ya da argümandan hareketle tespit edilmişse belirtilen bu default tür etkili olmamaktadır. Örneğin: template void foo(T a) { cout << a << endl; } //... foo(3.14); // T => double Burada T şablon parametresi argümandan hareketle double olarak belirlenebilecektir. Bu nedenle default değer olan int kullanılmayacaktır. Aynı işlem açısal parantezlerin içi boş bırakılarak da yapılabilirdi foo<>(3.14); // T => double --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir şablon parametresi için default tür belirtilirken onun solundaki şablon parametrelerinden faydalanılabilir. Örneğin: template void foo(T a) { K p = &a; cout << *p << endl; } Burada T türü hangi türse K türü o türden bir adres türünü temsil etmektedir. Örneğin: foo(3.14); // T => double, K => double * Örneğin: template > void foo(T a) { K v; //... } //... foo(123); // T => int, K => vector --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyon şablonu ile aynı isimli normal fonksiyon bir fonksiyon aynı faaliyet alanında bulunabilir. Bu durumda overload resolution işleminde normal fonksiyon eğer argüman türleri tam olarak uyum sağlıyorsa tercih edilmektedir. Ancak argüman türleri tam olarak uyum sağlamıyorsa fonksiyon şablonu tercih edilmektedir. Örneğin: void foo(int a) { cout << "foo(int)" << endl; } template void foo(T a) { cout << "template foo" << endl; } //... Burada foo fonksiyonunu aşağıdaki gibi çağırmış olalım: foo(123); // şablon olmayan int parametreli foo çağrılır Görünüşe göre her iki foo fonksiyonu da aday ve uygun fonksiyonlardır. Ancak bu durumda şablon olmayan fonksiyon tercih edilecektir. Fakat int dışında tüm türler için şablon açımı (template instantiation) yapılır. Örneğin: foo(3.14); // şablon açımı yapılacak, yani çağrı foo(3.14) ile eşdeğer Tabii yukarıdaki örnekte programcı açısal paramtezlerle açıkça şablon açımının yapılmasını isteyebilir. Örneğin: foo(123); // artık şablon açımı yapılacak, çağrı foo(123) ile eşdeğer Bazen fonksiyon şablonu uygun (viable) fonksiyon olmaktan çıkabilir. Bu durumda normal fonksiyon tercih edilecektir. Örneğin: void foo(int a, double b) { cout << "foo(int, double)" << endl; } template void foo(T a, T b) { cout << "template foo" << endl; } //... foo(123, 123L); // fonksiyon şablonu olan foo artık uygun (viable) fonksiyon değil, normal foo seçilir --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; void foo(int a, double b) { cout << "foo(int, double)" << endl; } template void foo(T a, T b) { cout << "template foo" << endl; } int main() { foo(123, 12L); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyon şablonu tanımlandığında tüm hatalar henüz fonksiyon şablonu görüldüğünde tespit edilememektedir. Hataların büyük kısmı fonksiyon şablonunun parametre türü tespit edildikten sonra açım sırasında (instantiation) tesit edilebilmektedir. Örneğin: template void foo(T a) { a.foo(); a.bar(10, 20, 30); cout << a << ednl; } Burada derleyici bu fonksiyon şablonunu gördüğünde foo ve bar hakkında hiçbir bilgi edinmediği halde bu tanımlamayı error olarak değerlendiremez. Çünkü fonksiyonun gövdesindeki kod T türüne bağlı olarak geçerli olabileceği gibi geçersiz olabilir. Örneğin fu foo fonksiyonu aşağıdaki gibi çağrılırsa açım sırasında error oluşacaktır: foo(123); // açım sırasında error Burada T türü intg olarak belirlenecektir. Ancak int türünün foo ve bar isimli üye fonksiyonları yoktur. Tabii burada foo fonksiyonu bir sınıf türüyle çağrılırsa o sınıfın da foo ve bar isimli uygun parametreleri üye fonksiyonları varsa açım sırasında bir error oluşmayacaktır. İşte C++ derleyicileri tipik olarak fonksiyon şablonlarıyla ilgili kontrolleri üç aşamada yapmaktadır: 1) Fonksiyon şablonunun tanımlanması sırasında yapılan kontroller: Derleyici fonksiyon şablonunu gördüğünde şablon parametresi ne olursa olsun geçersiz birtakım sentaksla karşılaşırsa bu aşamada error oluşturabilir. Örneğin: template void foo(T a) { T a; a = 10 // T türü ne olursa olsun bu satırdkai sentaks geçersizdir. cout << a << endl; abc // T türü ne olursa olsun bu satırdkai sentaks geçersizdir. } Burada T türü ne olursa olsun iki satırdaki atom yığını sentaks bakımından geçerli değildir. Dolayısıyla derleyici açımı beklemeden bu fonksiyon şablonunu görür görmez error rapor edebilir. 2) Fonksiyon şablonu çağrıldığında yapılan kontroller: Fonksiyon şablonu uygun biçimde çağrılmamış olabilir, çağrımda şablon parametreleri tespit edilememiş olabilir. Bu aşamdada da error durumları rapor edilebilmektedir. Örneğin: template void foo(T a, T b) { //... } //... foo(10, 3.14); // error! Burada derleyici fonksiyon şablonunu gördüğünde bir error rapor etmeyecektir. Ancak fonksiyon çağrılırken T türü belirlenemediğinden dolayı error rapor edilecektir. 3) Açım (instantiation) yapılan kontroller: En önemli kontroller açım yapılırken açım türü tespit edildikten sonra gerçekleştirilmektedir. Derleyici bu aşamda açım türünü doğru bir biçimde belirledikten sonra gonksiyon gövdesi içerisindeki kodun bu açım türü için geçerli olup olmadığına bakmaktadır. Hatalar önemli bir kısmı bu aşamada rapor edilmektedir. Aslında standartlarda yukarıdaki üç kontrol aşaması herhangi bir biçimde belirtilmemiştir. Fonksiyon şablonlarındaki hata tespiti tamamen derleyicinin kendi tasarımına bırakışmıştır. Biz burada tipik bir C++ derleyicisinin kontrolleri bu biçimde yaptığını ifade ettik. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyon şablonundaki şablon parametreleri herhangi bir bildirimle gizlenemez. Yani şablon parametreleri fonksiyon faaliyet alanına sahiptir. Şablon parametreleriyle aynı isimli bir bildirim yapılamaz. Öneğin: template void foo(T a) { { int T; // geçersiz! //... } } Burada şablon parametresinin ismi olan T ayrıca bir değişken ismi olarak bildirilmek istenmiştir. Bu işlem geçersizdir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyon şablonları için prototip bulundurulabilir. Yai bir fonksiyon şablonunun tanımlamasının onun çağrıldığı noktaya kadar yapılmış olması gerekmemektedir. Önce prototip bildirimi yapılıp tanımlama çağrılma noktasının aşağısında da yapılabilir. Tabii daha önceden de belirttiğimiz gibi fonksiyon şablonlarının bir biçimde derleme aşaqmasında derleyici tarafından görülmesi gerekmektedir. Örneğin: template T getmax(T a, T b); int main() { int maxval; maxval = getmax(10, 12); cout << maxval << endl; return 0; } template T getmax(T a, T b) { if (b < a) return a; return b; } Burada getmax isimli fonksiyon şablonun tanımlaması aşağıda yapılmıştır. Ancak yukarıda çağrılma noktasından önce bir prototip bildirimi bulundurulmuştur. Prototip bildiriminin bir şablon bildirimi biçiminde yapıldığına dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; template T getmax(T a, T b); int main() { int maxval; maxval = getmax(10, 12); cout << maxval << endl; return 0; } template T getmax(T a, T b) { if (b < a) return a; return b; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şablon parametreleri iki gruba ayrılmaktadır: 1) Tür belirten şablon parametreler (template type parameters) 2) Tür belirtmeyen şablon parametreleri (non-type template parameters) Eğer şablon parametrelerinin önünde typename ya da class anahtar sözcükleri varsa bu tür şablon parametreleri tür belirten parametrelerdir. Yani bu parametreler herhangi bir türü tamsil etmektedir. Biz şimdiye kadar zaten hep tür belirten şablon parametreleri kullandık. Örneğin: template void foo() { //... } Burada T şablon parametresi tür belirten bir parametredir. Tür belirtmeyen şablon parametreleri tamsayı türlerinden olmak zorundadır. Bu parametrelerde önce tamsayı türü sonra da parametre ismi belirtilir. Örneğin: template void foo() { //... } Burada T parametresi tür belirten şablon parametresi SIZE parametresi ise tür belirtmeyen şablon parametresidir. Tür belirtmeyen (non-type template parameters) şablon parametreleri için açımn sırasında sabit ifadesi kullanılmak zorundadır. Örneğin: foo(); Burada T türü int olarak SIZE türü ise 100 biçiminde belirlenmiştir. Tür belirtmeyen şablon parametreleri fonksiyon şablonlarının içerisinde sabit ifadesi biçiminde kullanılabilmektedir. Örneğin: template void foo() { T a[SIZE]; // geçerli, SIZE bir sabit ifadesi biçiminde kullanılabilir //... } Şablonların tür belirtmeyen parametreleri seyrek kullanılmaktadırş. Ancak onların sabit ifadesi belirtmesi bazı durumlarda faydalar sağlamaktadır. Örneğin: template void foo(T (&a)[SIZE]) { //... } Burada biz bu foo fonksiyonunu bir dizi ismiyle çağırırsak o dizi ismi referansa atandığı için (copy initialization) dizi isminin adresi alınacaktır. Bu adres de dizi referansına aktarılmaktadır. Burada derleyici SIZE parametresini otomatik olark tespit edebilecektir. Örneğin: int a[3]; foo(a); // T => int, SIZE => 3 Burada T paraöetresi int olarak SIZE parametresi ise 3 olarak tespit edilecektir. Yukarıda da belirttiğimiz gibi şablonların tür belirtmeyen parametreleri tamsayı türlerine ilişkin olmak zorudadır. Örneğin aşağıdaki tanımlama geçersizdir: template // geçersiz! void foo() { //... } Burada tür belirtmeyen K parametresi double türünden olamazç. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın tamamı da şablon olarak yazılabilir. Böyle sınıflara "sınıf şablonları (class templates)" denilmektedir. Sınıf şablonları yazılırken yine bir şablon bildirimi ile başlanır. Sonra normal sınıf bildirimi ile devam edilir. Örneğin: template class Sample { //... }; Şablonların tür belirten parametreleri yine sınıfın içerisinde ve tüm üye fonksiyonların içerisinde birer tür ismi (type specifier) olarak kullanılabilmektedir. Bir sınıf şablonu mutlaka şablon parametreleri açısal parantezler içerisinde belirtilerek kullanılmak zorundadır. Sınıf şablonlarında fonksiyon şablonlarında olduğu gibi "şablon parametreleri otomatik olarak tespit edilememektedir. Örneğin: Sample s; // geçersiz! Mutlaka sınıf isminden sonra açısal parantezlerler şablon parametreleri belirtilmek zorundadır. Örneğin: Sample s; // geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf şablonunun üye fonksiyonları sınıf içerisinde inline olarak tanımlanabilir. Örneğin: template class Sample { public: void foo() { T a; //... } void bar() { T b; //... } //... } Ancak sınıf şablonlarının üye fonksiyonları sınıfın dışında tanımlanacaksa fonsiyon şablonu gibi tanımlanmalıdr. Çünkü şablon bir sınıfın üye fonksiyonları şablon fonksiyonlar gibidir. Örneğin: template class Sample { public: void foo(); void bar(); //... }; template void Sample::foo() { //... } template void Sample::bar() { //... } Burada yeni öğrenelerin sık yaptığı hatalardan biri sınıf isminden sonra açısal parantez kullanmamaktadır. Bir sınıf şablonu açısal parantezsiz kullanılamaz. Örneğin: template void Sample::foo() // geçersiz! sınıf isminden sonra açısal parantezler kullanılmamış { //... } Aslında sınıf şablonunun üye fonksiyonları dışarıda yazılırken şablon parametrelerinin isimlerinin uyuşması gerekmemektedir. Ancak bu uyuşumu sağlamak iyi bir tekniktir. Yani aslında biz foo üye fonksiyonunu şöyle de tanımlayabilirdik: template void Sample::foo() { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; template class Sample { public: Sample(T val); void disp() const; private: T m_val; }; template Sample::Sample(T val) { m_val = val; } template void Sample::disp() const { cout << m_val << endl; } int main() { Sample s(10); s.disp(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf şablonu kullanılırken derleyici aslında açım sırasında sınıfın tüm üye fonksiyonlarını açmaz (instantiate etmez). Yalnızca kullanılmış olanları açar. Tabii bu durumun ayrıntıları derleyiciden derleyiciye değişebilmektedir. Örneğin: template class Sample { public: void foo(); void bar(); void tar(); //... }; //... Sample s; s.foo(); Burada derleyici muhtemelen yalnızca yapıcı fonksiyonu ve foo fonksiyounu açacaktır. Kullanılmayan fonksiyonların açılmasının kimseye bir faydası yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 97. Ders 17/09/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf şablonunun her farklı türden açılımı tamamen farklı bir belirtmektedir. Örneğin: template class Sample { //... }; Burada Sample türü ile Sample türü arasında hiçbir ilişki yoktur. Bunlar tamamen farklı türlerdir. Örneğin: Sample *ps; Sample s; ps = &s; // geçersiz! Sample ile Sample tamamen farklı türlerdir. Örneğin: Sample s; Sample k; Sample m; s = k; // s.operator =(k) s = m; // geçersiz! s.operator =(k) türler farklı! --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanmacağı gibi bir sınıf bir fonksiyona ya da başka bir sınıfa arkadaşlık verebiliyordu. Arkadaş fonksiyonların ve sınıfların arkadaş olunan sınıfa erişim ayrıcalıkları vardı. Sınıf şablonları söz konusu olduğunda arkadaşlık konusu biraz daha karışık hale gelmektedir. Bir sınıf şablonu normal bir fonksiyona ya da sınıfa arkadaşlık verirse o sınıfın farklı türlerden açılımlarının hepsi o fonksiyon ya da sınıfın arkadaşı olur. Örneğin: template class Sample { //... friend void foo(); private: int m_val; }; Burada Sample türü de Sample türü de diğer açılımların hepsi de foo fonksiyonuna arkadaşlık vermiş durumdadır. foo fonksiyonun daha önce bildiriminin yapılmasına gerek yoktur. Örneğin: template class Sample { //... friend void foo(); private: int m_val; }; void foo() { Sample s; Sample k; s.m_val = 10; // geçerli k.m_val = 20; // geçerli //... } Bir sınıf şablonu bir fonksiyon ya da sınıf şablonunun belirli açılımlarına arkadaşlık verebilir. Örneğin bir sınıf şablonu hangi türle açılmışsa aynı türden açılımlar için fonksiyon şablonlarına ya da sınıf şablonlarına arkadaşlık verebilir. Örneğin: template void foo(); template class Mample; template class Sample { //... friend void foo(); friend class Mample; private: int m_val; }; template class Mample { //... }; template void foo() { //... } Burada Sample sınıfının T türünden açılımı foo fonksiyonunun T türünden açılımına ve Mample sınıfının T türünden açılımına arkadaşlık vermektedir. Yani örneğin Sample sınıfı foo fonksiyonuna ve Mample sınıfına arkadaşlık vermiş durumdadır. Arkadaşlıkta açısal fonksiyon ya da sınıf isminden sonra açısal parantezler kullanıldığında daha önce şablon bildirimlerinin yapılmış olması gerekir. Örneğin buradaki foo fonksiyonu şöyle çağrılmış olsun: foo(); Fonksiyonun tanımlaması da şöyle yapılmış olsun: template void foo() { Sample s; Sample k; s.m_val = 10; // geçerli, Sample sınıfı foo fonksiyonuna arkadaşlık vermiş k.m_val = 20; // geçersiz! Sample sınıfı foo fonksiyonuna arkadaşlık vermemiş } Burada foo() çağrısı ile derleyici foo fonksiyonunu int türü için açacaktır. foo fonksiyonunun int türü için açılımı Sample nesnesinin private bölümüne erişebilir, ancak Sample nesnesinin private bölümüne erişemez. Çünkü Sample sınıfı foo fonksiyonuna arkadaşlık vermiştir fakat Sample sınıfı foo fonksiyonuna arkadaşlık vermemiştir. Tabii genellikle zaten programcılar bu tür durumlarda arkadaşlık verilen fonksiyon şablonu içeisinde ilgili sınıfı şablon parametresiyle kullanılar. Örneğin: template void foo() { Sample s; s.m_val = 10; // geçerli, Sample sınıfı foo fonksiyonuna arkadaşlık vermiş //... } Aslında mademki arkadaş fonksiyon bildirimi default olarak aynı isim alanındaki fonksiyonlara ilişkindir o halde şablon bildirimi sınıf içerisinde de yapılabilir. Örneğin: template class Sample { //... template friend void foo(); private: int m_val; }; template void foo() { //... } Ancak aynı işlem sınıflar için yapılamamaktadır. Örneğin: template class Sample { //... template friend class Mample; // geçersiz! private: int m_val; }; template class Mample { //... }; Bu tür durumlarda pratik bir çözüm arkadaş yapılan sınıfın daha yukarıda bildirilmesidir. Örneğin: template class Mample { //... }; template class Sample { //... friend class Mample; private: int m_val; }; Örneğin: template class LList; template class Node { private: T m_val; Node *m_next; Node *m_prev; friend class LList; }; template class LList { public: LList() : m_head(nullptr), m_tail(nullptr), m_count(0) {} ~Llist(); void add(int val); void walk() const; std::size_t count() { return m_count; } //... private: Node *m_head; Node *m_tail; std::size_t m_count; }; Burada Node sınıfının T türünden açılımı LList sınıfının T türünden açılımına bire bir arkadaşlık vermiştir. Yukarıdaki örnekte foo fonksiyonu için önce bir şablon bildiriminin yapıldığına dikkat ediniz. Bir sınıf şablonunun ya da fonksiyon şablonunun üm açılımları da bir sınıfın arkadaşı yapılabilir. Örneğin: class Sample { public: //... template friend void foo(); template friend class Mample; private: int m_val; }; Burada foo fonksiyonunun ve Mamle sınıfının tüm açılımları Sample sınıfının arkadaşıdır. Bu durumda fonksiyon ve sınıf şablonunu daha önce bildirilmek zorunda değildir. Örneğin: template void foo() { Sample s; s.m_val = 10; // geçerli } template class Mample { public: void test() { Sample s; s.m_val = 10; // geçerli } }; Tabii buradaki Sample sınıfı da sınıf şablonu olabilirdi: template class Sample { public: //... template friend void foo(); template friend class Mample; private: int m_val; }; Burada arkadaş tanımlamalarındaki şablon parametrelerinin ismi T olarak da verilebilirdi. Ancak bu ismi farklılaştırmak daha okunabilir bir durum oluşturmaktadır. Aşağıda basit bir bağlı liste sınıfının şablon biçiminde tanımlanmasına örnek verilmiştir. Bu sayede biz bağlı liste işlemlerini türden bağımsız bir biçimde gerçekleştirebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // llist.hpp #ifndef LLIST_HPP_ #define LLIST_HPP_ #include #include namespace CSD { template class LList; template class Node { public: Node(const T &val) : m_val(val) {} private: T m_val; Node *m_next; Node *m_prev; friend class LList; }; template class LList { public: LList() : m_head(nullptr), m_tail(nullptr), m_count(0) {} ~LList(); void add(const T &val); void walk() const; std::size_t count() { return m_count; } //... private: Node *m_head; Node *m_tail; std::size_t m_count; }; template void LList::add(const T &val) { Node *new_node = new Node(val); if (m_tail != nullptr) m_tail->m_next = new_node; else m_head = new_node; new_node->m_prev = m_tail; new_node->m_next = nullptr; m_tail = new_node; ++m_count; } template void LList::walk() const { Node *node; for (node = m_head; node != nullptr; node = node->m_next) std::cout << node->m_val << ' '; std::cout << std::endl; } template LList::~LList() { Node *node, *temp_node; node = m_head; while (node != nullptr) { temp_node = node->m_next; delete node; node = temp_node; } } } #endif // app.cpp #include #include #include "llist.hpp" using namespace std; using namespace CSD; class Person { public: Person(const char *name, int no) : m_name(name), m_no(no) {} friend ostream &operator << (ostream &os, const Person &per); private: string m_name; int m_no; }; ostream &operator << (ostream &os, const Person &per) { return os << '(' << per.m_name << ", " << per.m_no << ')'; } int main() { LList ll; ll.add(Person("Ali Serce", 123)); ll.add(Person("Kaan Aslan", 512)); ll.add(Person("Necati Ergin", 111)); ll.add(Person("Jale Kanlidere", 130)); ll.walk(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında C++'ın standart kütüphanesi içerisinde isimli başlık dosyasında çift bağlı liste işlemlerini yapan bir sınıf şablonu bulunmaktadır. Sınıf şablonunun zorunlu şablon parametresi tıpkı yukarıdaki örneğimizde olduğu gibi bağlı listenin tutacağı elemanların türünü belirtmektedir. Örneğin list a; Burada a int elemanları tutan bir bağlı liste işlevini yerine getirecektir. Bağlı listelerin sonuna push_back fonksiyonu ile başına da push_front fonksiyonu ile eleman eklenebilir. Örneğin: a.push_back(123); a.push_front(512); Bağlı liste içerisindeki elemanların sayısı vector sınıfında olduğu gibi size üye fonksiyonuyla elde edilmektedir. Örneğin: cout << a.size() << endl; Bağlı listeler iteratör yoluyla dolaşılmaktadır. Sınıfın begin üye fonksiyonu ilk elemana ilişkin iteratör değerini end üye fonksiyonu ise son elemandan sonraki elemana ilişkin iteratör değerini vermektedir. list sınıfının iteratörleri "iki yönlü (bidirectional)" iteratörlerdir. Yani iteratörler ++ operatörüyle artırılabilir -- operatörüyle eksiltilebilir. Örneğin: list a; a.push_back(Person("Ali Serce", 123)); a.push_front(Person("Kaan Aslan", 678)); a.push_back(Person("Necati Ergin", 111)); a.push_front(Person("Jale Kanlidere", 130)); for (list::iterator iter = a.begin(); iter != a.end(); ++iter) cout << *iter << endl; push_back ve push_front fonksiyonlarının yanı sıra elemanı doğrudan hedefte oluşturan emplace_back ve emplace_front fonksiyonları da bulunmaktadır. Bağlı listeyi tersten dolaşmak için "reverse iterator" kullanmak gerekir. Reverse iterator ters yönde ilerleyen iteratördür. Reverse iteratörleri destekleyen sınıflarda rbegin üye fonksiyonu son elemana ilişkin iteratörü vermektedir. rend üye fonksiyonu ise ilk elemandan bir önceki elemana ilişkin iteratörü vermektedir. Reverse iteratörleri ++ ile artırdığımızda aslında ters yönde ilerlemiş oluruz. Bu durumda list nesnesi ters yönde şöyle dolaşılmaktadır: for (list::reverse_iterator iter = a.rbegin(); iter != a.rend(); ++iter) cout << *iter << endl; Burada reverse iteratörün tür isminin reverse_iterator biçiminde olduğuna dikkat ediniz. Bu tür durumlarda C++11 ile dile eklenmiş olan auto tür belirleyicisini kullanmak yazımı kolaylaştırmaktadır. Örneğin: for (auto iter = a.rbegin(); iter != a.rend(); ++iter) cout << *iter << endl; Yalnızca list sınıfı değil vector sınıfı da reverse iteratörleri desteklemektedir. list sınıfı da aralık tabanlı for döngüleri ile kullanılabilmektedir. Örneğin: for (Person &per : a) cout << per << endl; list sınıfının [] operatör fonksiyonu yoktur. Zaten bağlı listelerde belli bir indeksteki elemana hızlı bir erişim mümkün değildir. Görüldüğü gibi vector sınıfı ile list sınıfının kullanımları birbirine çok benzerdir. (Bu benzerlik kasten yapılmıştır.) Fakat bu iki sınııfn kullandığı veri yapıları tamamen birbirinden farklıdır. Pekiyi ne zaman vector (yani dinamik dizi) ne zaman list (yani baplı liste) sınıfını kullanmak gerekir? d - Bağlı listelere eleman insert etmek ve bağlı listelerden eleman silmek sabit karmaşıklıkta (O(1) karmaşıklıkta) bir işlemdir. Halbuki aynı işlemler dinamik dizilerde doğrusal (O(N) karmaşıklıktadır. - Bağlı listelerde elemanların ardışıl bulunma zorunluluğu yoktur. Halbuki dinamik dizilerde elemanlar ardışıl bir biçimde tutulmaktadır. - Dinamik dizilerde belli bir indeksteki elemana [] operatörüyle hızlı bir biçimde erişilebilir. Ancak bağlı listelerde belli bir indeksteki elemana sıralı bir biçimde erişilmektedir. Bu durumda indeksli erişimin seyrek yapıldığı insert ve delete işlemlerinin sık yapıldığı, uzunluğu baştan bilinmeyen ve ardışıl alan sıkıntısı çekilen sistemlerde nesneleri saklamak için bağlı listeler, diğer durumlarda ise dinamik diziler tercih edilebilir. list nesnesine insert işlemi iteratör yoluyla yapılmaktadır. insert fonksiyonun birinci parametresi insert işleminin yapılacağı pozisyonun iteratör değerini, ikinci parametresi ise insert edilecek değeri belirtmektedir. insert fonksiyonu insert edilen nesnenin itertatBURADA KALDör değeriyle geri dönmektedir. Örneğin: auto iter = a.begin(); ++iter; ++iter; a.insert(iter, Person("Erol Oner", 678)); insert işlemleri her zaman "insert edilecek eleman ilgili iteraörün gösterdeği yerde olacak biçimde" yapılmaktadır. insert fonksiyonunun overload edilmiş diğer bir biçimi bağlı listenin belli bir yerine iki iteratörle belirtilen tüm elemanları insert etmektedir. Örneğin: vector v = {Person("Sacit Hos", 187), Person("Metin Asci", 687), Person("Guray Sonmez", 754)}; a.insert(iter, v.begin(), v.end()); Burada iter ile belirtilen iteratörden itibaren list nesnesine vektör nesnesinin tüm elemanları insert edilmiştir. Tabii aslında uygulamada programcı genellikle insert fonksiyonun geri döndürdüğü iteratör değerini saklayıp daha sonra o pozisyona insert işlemi uygulamaktadır: a.insert(a.end(), Person("Ali Serce", 123)); a.insert(a.end(), Person("Kaan Aslan", 678)); auto insert_pos = a.insert(a.end(), Person("Necati Ergin", 111)); a.insert(a.end(), Person("Jale Kanlidere", 130)); vector v = {Person("Sacit Hos", 187), Person("Metin Asci", 687), Person("Guray Sonmez", 754)}; a.insert(insert_pos, v.begin(), v.end()); Burada elemanlar sona insert edilmekle birlikte bir elemanın yeri saklanmış ve sonra oaraya insert işlemi yapılmıştır. insert fonksiyonunun initialzer_list parametreli overload biçimi de vardır. Örneğin: a.insert(insert_pos, {Person("Sacit Hos", 187), Person("Metin Asci", 687), Person("Guray Sonmez", 754)}); Eleman silme için erase üye fonksiyonu kullanılmaktadır. erase fonksiyonu da parametre olarak iterator alıp o iteratörün gösterdiği elemanı silmektedir. Örneğin: a.insert(a.end(), Person("Ali Serce", 123)); a.insert(a.end(), Person("Kaan Aslan", 678)); auto erase_pos = a.insert(a.end(), Person("Necati Ergin", 111)); a.insert(a.end(), Person("Jale Kanlidere", 130)); a.erase(erase_pos); list sınıfının diğer üye fonksiyonları ve ayrıntıları "İleri C++" kursunda ele alınmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 98. Ders 18/09/2024 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İteratörler konusunun aslında yukarıda açıkladığımızdan daha fazla ayrıntıları vardır. Bu ayrıntılar "İleri C++" kursunda ele alınmaktadır. Pekiyi biz kendi sınıfımıza nasıl iteratör desteği verebiliriz? Tabii böyle bir destek verilirken öncelikle sınıfa hangi türden iteratör desteğinin verileceğine karar verilmelidir. Örneğin bir çift bağlı liste sınıfına "iki yönlü (bidirectional)" iteratör desteği verilebilir. Biz de daha önce şablon biçiminde yazmış olduğumuz LList sınıfına "iki yönlü (bidirectional) iteratör" desteği verelim. Sınıfımıza "iki yönlü iteratör" desteği verebilmek için begin, end, rbegin, rend, ++, * ve != operatör fonksiyonlarının yazılması gerekir. Bu örnekte iteratörler LListIterator ve LListReverseIterator sınıflarıyla temsil edilmiştir. Bu sınıfların içerisinde aslında yalnızca iteratörün gösterdiği bağlı liste düğümünün adresi tutulmaktadır. ++ ve -- operatörleri bağlı listede ileri ve geri yönde hareket etmektedir. Aşağıda böyle bir örneğin kodları verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // llist.hpp #ifndef LLIST_HPP_ #define LLIST_HPP_ #include #include namespace CSD { template class LList; template class LListIterator; template class LListReverseIterator; template class Node { public: Node(const T &val) : m_val(val) {} private: T m_val; Node *m_next; Node *m_prev; friend class LList; friend class LListIterator; friend class LListReverseIterator; }; template class LListIterator { public: LListIterator(Node *node) : m_curnode(node) {} LListIterator &operator ++() { m_curnode = m_curnode->m_next; return *this; } bool operator !=(const LListIterator &iter) { return m_curnode != iter.m_curnode; } T &operator *() { return m_curnode->m_val; } LListIterator &operator --() { m_curnode = m_curnode->m_prev; return *this; } private: Node *m_curnode; }; template class LListReverseIterator { public: LListReverseIterator(Node *node) : m_curnode(node) {} LListReverseIterator &operator ++() { m_curnode = m_curnode->m_prev; return *this; } bool operator !=(const LListReverseIterator &iter) { return m_curnode != iter.m_curnode; } T &operator *() { return m_curnode->m_val; } LListReverseIterator &operator --() { m_curnode = m_curnode->m_next; return *this; } private: Node *m_curnode; }; template class LList { public: LList() : m_head(nullptr), m_tail(nullptr), m_count(0) {} ~LList(); void add(const T &val); void walk() const; std::size_t count() { return m_count; } LListIterator begin() const { return LListIterator(m_head); } LListIterator end() const { return LListIterator(nullptr); } LListReverseIterator rbegin() const { return LListReverseIterator(m_tail); } LListReverseIterator rend() const { return LListReverseIterator(nullptr); } typedef LListIterator iterator; typedef LListReverseIterator reverse_iterator; //... private: Node *m_head; Node *m_tail; std::size_t m_count; }; template void LList::add(const T &val) { Node *new_node = new Node(val); if (m_tail != nullptr) m_tail->m_next = new_node; else m_head = new_node; new_node->m_prev = m_tail; new_node->m_next = nullptr; m_tail = new_node; ++m_count; } template void LList::walk() const { Node *node; for (node = m_head; node != nullptr; node = node->m_next) std::cout << node->m_val << ' '; std::cout << std::endl; } template LList::~LList() { Node *node, *temp_node; node = m_head; while (node != nullptr) { temp_node = node->m_next; delete node; node = temp_node; } } } #endif // app.cpp // app.cpp #include #include #include "llist.hpp" #include using namespace std; using namespace CSD; class Person { public: Person(const char *name, int no) : m_name(name), m_no(no) {} friend ostream &operator << (ostream &os, const Person &per); private: string m_name; int m_no; }; ostream &operator << (ostream &os, const Person &per) { return os << '(' << per.m_name << ", " << per.m_no << ')'; } int main() { LList ll; ll.add(Person("Ali Serce", 123)); ll.add(Person("Kaan Aslan", 512)); ll.add(Person("Necati Ergin", 111)); ll.add(Person("Jale Kanlidere", 130)); for (LList::iterator iter = ll.begin(); iter != ll.end(); ++iter) { cout << *iter << endl; } cout << "----------------------" << endl; for (Person &per : ll) { cout << per << endl; } cout << "----------------------" << endl; for (LList::reverse_iterator iter = ll.rbegin(); iter != ll.rend(); ++iter) { cout << *iter << endl; } cout << "----------------------" << endl; list a; a.push_back(Person("Ali Serce", 123)); a.push_back(Person("Kaan Aslan", 512)); a.push_back(Person("Necati Ergin", 111)); a.push_back(Person("Jale Kanlidere", 130)); for (list::iterator iter2 = a.begin(); iter2 != a.end(); ++iter2) { cout << *iter2 << endl; } cout << "----------------------" << endl; for (Person &per : a) { cout << per << endl; } cout << "----------------------" << endl; for (list::reverse_iterator iter = a.rbegin(); iter != a.rend(); ++iter) { cout << *iter << endl; } return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi sınıfların static veri elemanlarının toplamda tek bir kopyası vardı. Bu elemanlar nesnenin içerisinde yer kaplamıyordu. Ayrıca static veri elemanlarının global bir tanımlamasının yapılması da gerekiyordu. Pekiyi bir sınıf şablonu içerisinde static veri elemanı olduğunda bu elemandan toplamda kaç tane bulunacaktır? Örneğin: template class Sample { //... static int ms_val; }; İşte sınıf şablonlarındaki static veri elemanlarının her farklı açılım için ayrı kopyası oluşturulmaktadır. Başka bir deyişle yukarıdaki örnekte Sample::ms_val ile Sample::ms_val farklı nesnelerdir. Sınıf şablonlarındaki static veri elemanlarının tanımlaması ise yine şablon olarak yapılır. Örneğin: template int Sample::ms_val; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // app.cpp #include using namespace std; template class Sample { public: //... static int ms_val; }; template int Sample::ms_val; int main() { Sample::ms_val = 10; Sample::ms_val = 20; cout << Sample::ms_val << endl; // 10 cout << Sample::ms_val << endl; // 20 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf şablonunun belli bir türdeki açılımına typedef ya da using bildirimleri ile alternatif isimler verilebilir. Örneğin: typedef list list_int; Burada artık list ile list_int arasında bir farklılık yoktur. Anımsanacağı gibi C++11 ile birlikte tür bildirimleri alternatif olarak using anahtar sözcüğü ile de yapılabiliyordu. Örneğin: using list_int = list; Örneğin aslında string sınıfı basic_string isimli sınıf şablonunun char türden açılımıdır: typedef basic_string string; Yani aslında string isimli bir sınıf yoktur. basic_string isimli bir sınıf şablonu vardır. string ismi basic_string biçiminde typedef edilmiştir. string sınıfının neden şablon olarak yazıldığını merak edebilirsiniz. Bunun nedeni yazıyı temsil eden karakterlerin char türü yerine başka türlerden de olabilmesini sağlamaktır. Örneğin başlık dosyasında aşağıdaki gibi de bir typedef bulunmaktadır: typedef basic_string wstring; Bu durumda biz her karakteri wchar_t türünden olan yazılar oluşturabiliriz. Tabii bunları yazdırmak için wcout nesnesi kullanılmaktadır: wstring s = L"this is a test"; wcout << s << endl; Aslında iostream sınıf sistemindeki sınıflar da şablon olarak bildirilmiştir: ios_base basic_ios basic_istream basic_ostream basic_iostream Bu türetme şemasında yalnızca ios_base sınıfı şablon sınıf değildir. Aslında ostream ismi basic_ostream biçiminde typedef edilmiştir: typedef basic_ostream ostream; Benzer biçimde aynı şey istream, ios ve iostream sınıfları için de yapılmıştır: typedef basic_istream istream; typedef basic_ios ios; typedef basic_iostream iostream; cout, wcout, cin, wcin nesneleri de şöyle tanımlanmıştır: basic_ostream cout; basic_istream cin; basic_ostream wcout; basic_istream wcin; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz yukarıda sınıf şablonlarının belli türden açılımlarına alternatif isimler verdik. sınıf şablonunun kendisine alternatif isim verme C++11'e kadar mümkün değildi. Aşağıdaki gibi bir typedef bildirimi eskiden de mevcut standartlarda da geçersizdir: typedef vector vec; typedef bildiriminde typedef anahtar sözcüğü kaldırıldığında geçerli bir bildirimin bulunyor olması gerekir. Başka bir deyişle typedef geçerli bir bildirimde kullanılabilmektedir. Halbuki aşağıdaki gibi bir bildirim geçerli değildir: vector vec; Ancak C++11 ile birlikte yeni using ile alternatif tür ismi oluşturma dile eklendiğinde sınıf şablonlarına da alternatif isimlerin verilmesi sağlanmıştır. Fakat bu işlem şablonsal biçimde aşağıdaki gibi yapılmaktadır: template using vec = vector; Artık vec ile vector tamamen aynı anlamdadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; template using vec = vector; int main() { vec v{1, 2, 3, 4, 5}; for (int &val : v) cout << val << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 99. Ders 23/09/2024 - Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Normal bir sınıfın ya da bir sınıf şablonunun bir üye fonksiyonu da ayrıca fonksiyon şablonu olabilir. Bu duruma İngilizce kısaca "member template" denilmektedir. Örneğin: class Sample { //... void foo(); template void bar(); }; Burada foo normal bir üye fonksiyondur. Ancak bar bir fonksiyon şablonudur. Bu biçimdeki bir fonksiyon şablonu sınıf içerisinde inline yazılabilir. Örneğin: class Sample { //... void foo(); template void bar(T a) { //... } }; Eğer "member template" dışarıda tanımlanacaksa tabii yine template sentaksının kullanılması gerekmektedir: class Sample { public: //... void foo(); template void bar(T a); }; void Sample::foo() { //... } template void Sample::bar(T a) { //... } Burada Sample sınıfı türünden bir nesne tanımlamış olalım: Sample s; bar fonksiyonu şablon üye fonksiyon olduğu için bu üye fonksiyonun gerektiğinde farklı açılımları sınıfa eklenebilecektir. Örneğin: s.bar(10); // s.bar(10) s.bar(3.14); // s.bar(3.14) Tabii üye fonksiyon şablonları (member template'ler) sınıf hangi başlık dosyasına bildirildiyse orada tanımlanmalıdır. Çünkü derleme işleminde derleyicinin şablon tanımlamasını görmesi gerekir. Bir sınıf şablonun da bir üye fonksiyonu bir fonksiyon şablonu olabilmektedir. Örneğin: template class Sample { //... void foo(); template void bar(K a); }; Burada hem Sample sınıfı şablon bir sınıftır hem de bar üye fonksiyonu şablon bir fonksiyondur. Biz foo fonksiyonu içerisinde yalnızca T şablon parametresini kullanabiliriz. Ancak bar üye fonksiyonu içerisinde hem T hem de K şablon parametreleri kullanılabilir. Bu biçimdeki fonksiyon şablonları yine sınıf içerisinde inline olarak tanımlanabilir: template class Sample { //... void foo(); template void bar(K a) { //... } }; Ancak böyle fonksiyonların dışarıda tanımlanması iki template sentaksı ile yapılmaktadır: template void Sample::foo() { //... } template template void Sample::bar(K a) { //... } Burada bar fonksiyonun tanımlamasında iki tane template sentaksının kullanıldığına dikkat ediniz. Yukarıdaki template sentaksı sınıf için aşağıdaki ise fonksiyon için gerekmektedir. Kullanıma şöyle örnek verebiliriz: Sample s; s.foo(); s.bar(3.14); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın yapıcı fonksiyonu bir fonksiyon şablonu biçiminde olabilir. Örneğin: class Sample { public: template Sample(T a); //... }; Burada Sample sınıfının yapıcı fonksiyonu şablon olarak bildirilmiştir. Yani "member template" durumundadır. Dışarıdaki tanımlaması aşağıdaki gibi yapılabilir: template Sample::Sample(T a) { //... } Biz bu sayede aşağıdaki gibi nesneler yaratabiliriz. Örneğin: Sample s(10); // geçerli T = int Sample s(3.14); // geçerli T = double Ancak sınıfların kopya yapıcı yapıcı fonksiyonları ve kopya atama operatör fonksiyonları hiçbir zaman şablon olarak açılmamaktadır. Örneğin: class Sample { public: Sample() = default; template Sample(const T &a); //... }; Burada yapısı fonksiyon yine bir member template olarak yazılmıştır. Şimdi aşağıdaki gibi bir koda bakınız: Sample s; Sample k(s); // dikkat! şablon açılımı yapılmayacak, özel durum Burada k nesnesi için kopya yapıcı fonksiyonu çalıştırılacaktır. Görünüşe göre bizim sınıfa yerleştirdiğimiz member template bu görevi yapar gibi gözükmektedir. Yani Sample k(s) bildiriminde k nesnesi için sanki bu üye fonksiyon şablonı T = Sample olarak açılabilir gibi gözükmektedir. Fakat C++'ta hiçbir zaman kopya yapıcı fonksiyonu ve kopya atama operatör fonksiyonu şablon fonksiyonlardan açılmamaktadır. Dolayısıyla yukarıdaki örnekte Sample sınıfının kopya yapıcı fonksiyonu derleyici tarafından yazılacaktır ve derleyici tarafından yazılan kopya yapıcı fonksiyon çalıştırılacaktır. Aynı durum atama işlemi için de söz konusudur. Örneğin: class Sample { public: Sample() = default; template Sample & operator =(const T &a); //... }; Sample s, k; s = k; // dikkat! kopya atama operatör fonksiyonu şablon olarak açılmayacak, özel durum Burada s = k işleminde şablon olarak yazılmış olan atama operatör fonksiyonunun devreye girmesini bekleyebilirsiniz. Ancak hçbir zaman C++'ta kopya atama operatör fonksiyonu da şablon olarak açılmamaktadır. Dolayısıyla burada programcının yazdığı şablon atama operatör fonksiyonu çağrılmayacak derleyicinin kendi yazdığı default kopya atama operatör fonksiyonu çağrılacaktır. Tabii aşağıdaki gibi bir atamada bu atama kopya atama operatör fonksiyonunun çağrılmasına yol açmadığından dolayı şablon açılımı yapılacaktır: s = 10; // şablon açılımı yapılacak Bu durumu aşağıdaki kodla test edebilirsiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; class Sample { public: Sample() = default; template Sample(const T &r); template Sample &operator =(const T &r); //... }; template Sample::Sample(const T &r) { cout << "Sample(const T &r)" << endl; } template Sample &Sample::operator =(const T &r) { static Sample temp; cout << "copy assignment operator" << endl; return temp; // yalnızca test için } int main() { Sample s; Sample k(s); s = k; s = 10; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bazen bir sınıf şablonunun özel bir tür için başka bir versiyonun yazılması gerekebilmektedir. Buna "özelleştirme (specialization)" denilmektedir. Örneğin vector sınıfının gerçekleştiriminde T şablon parametresi olmak üzere T türünden dinamik tahsisatlar yapılmaktadır. Ancak biz çok sayıda bool nesneyi bir vector nesnesinde tutmak istesek bool türden tahsisat yapmak bu özel durumda uygun olmayabilir. Çünkü tek bir bool nesnenin ne kadar yer kapladığının önemi olmasa da çok sayıda bool nesnenin kapladığı alanın azaltılması istenebilir. (bool bir bilginin aslında 1 bitle tutulabildiğini ancak C++ standartlarına göre bool türün en az 1 byte yer kaplayabileceğini anımsayınız.) İşte bu durumda vector sınıfının bool türden açılımı için kullanılabilecek bir vector versiyonunun ayrıca bulundurulması uygun olacaktır. Bir sınıf şablonunun belli bir tür için özelleştirilmesinde template anahtar sözcüğünden sonra açısal parantezlerin içi boş bırakılır ancak sınıf isminden sonra açısal parantezler içerisinde özelleştirilecek tür belirtilir. Örneğin: template class Sample { // ... }; template <> class Sample { //... }; Şimdi artık Sample sınıfının bool açılımı için aşağıdaki sınıf tanımlaması diğer açılımları için yukarıdaki sınıf tanımlaması kullanılacaktır. Özelleştirilmiş sınıfın bildiriminden önce genel şablon sınıfın bildirilmiş olması gerekmektedir. Tabii genel şablon sınıf ile özelleştirilmiş sınıfın aynı elemanlara sahip olması beklenir. Özelleştirilmiş sınıfın üye fonksiyonları dışarıda nasıl tanımlanır? Örneğin: template class Sample { public: void foo(); //... }; template <> class Sample { public: void foo(); //... }; Genel şablon sınıfın üye fonksiyonlarının dışarıda nasıl tanımlandığını zaten biliyoruz: template void Sample::foo() { //... } Ancak özelleştirilmiş sınıfın üye fonksiyonu bir şablon fonksiyon değildir. Yani o fonksiyonun içerisinde kullanılabalicek herhangi bir şablon parametresi yoktur. Bu nedenle fonksiyonun tanımlanması sırasında template sentaksı kullanılmaz. Örneğin: Sample::foo() { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ // app.cpp #include using namespace std; template class Sample { public: Sample() { cout << "Sample default constructor" << endl; } void foo(); //... }; template <> class Sample { public: Sample() { cout << "Sample default constructor" << endl; } void foo(); //... }; template void Sample::foo() { cout << "Sample.foo" << endl; } void Sample::foo() { cout << "Sample::foo" << endl; } int main() { Sample s; Sample k; s.foo(); k.foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şablon sınıflar "kısmi (partial)" olarak da özelleştirilebilmektedir. Buna "kısmi özelleştirme (partial specialization)" denilmektedir. Kısmi özelleştirmede şablon parametresinin belirli türleri için özelleştirme yapılır. Örneğin bir şablon sınıfın gösterici açımlarının farklı bir biçimde oluşturulması gerekebilmektedir. Kısmı açımda sınıf isminden sonra şablon parametresi dekleratörle ifade edilir. Örneğin: template class Sample { //... }; template class Sample { //... }; Görüldüğü gibi kısmi özelleştirmede yine şablon parametreleri belirtilmektedir. Ancak sınıf üsminden sonra türe ilişkin bir dekleratör bulundurulmaktadır. Burada Sample, Sample gibi açılımlarda üstteki sınıf şablonu, Sample, Sample gibi açılımlarda aşağıdaki sınıf şablonu kullanılacaktır. Tabii kısmi özelleştirmede de daha yukarıda önce genel şablon sınıfın bildirilmesi gerekmektedir. Kısmi özelleştirmede üye fonksiyonlar dışarıda yazılırken artık template sentaksı kullanılmaktadır. Örneğin: template class Sample { public: void foo(); //... }; template class Sample { public: void foo(); //... }; template void Sample::foo() { //... } template void Sample::foo() { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; template class Sample { public: Sample() { cout << "Sample default constructor" << endl; } void foo(); //... }; template class Sample { public: Sample() { cout << "Sample default constructor" << endl; } void foo(); //... }; template void Sample::foo() { cout << "Sample::foo" << endl; } template void Sample::foo() { cout << "Sample::foo" << endl; } int main() { Sample s; s.foo(); Sample k; k.foo(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıf şablonu birden fazla şablon parametresine sahip olabilir. Bu parametrelerden yalnızca bazıları özelleştirilebilir. Örneğin: template class Sample { //... }; template class Sample { //... }; Burada eğer açım sırasında ilk şablon parametresi int olarak girilirse aşağıdaki özelleştirilmiş biçim girilmezse yularıdaki özelleştirilmiş biçim kullanılacaktır. Üye fonksiyonların dışarıda tanımlanması da şöylşe yapılacaktır: template class Sample { public: void foo(); //... }; template class Sample { public: void foo(); //... }; template void Sample::foo() { //... } template void Sample::foo() { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyon şablonlarının özelleştirilmesi atlanmış --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyon şablonları da özelleştirilebilmektedir. Fonksiyon şablonları ile normal fonksiyonların overload edilebildiğini anımsayınız. Fonksiyon şablonlarında özelleştirme işlemine benzer bir işlem overload mekanizması yoluyla da yapılabilmektedir. Örneğin: template void foo(T a) { //... } void foo(int a) { //... } Biz konun başlarında da bir fonksiyon şablonu ile aynı isimli şablon olmayan fonksiyonların bir arada bulunabileceğini belirtmiştik. Overload resolution işleminde şablon olmayan fonksiyonun daha iyi dönüştürme sağladığı kabul edilmektedir. Örneğin: foo(3.14); // şablon açılımı yapılır foo(10); // int parametreli fonksiyon çağrılır Ayrıca C++'ta farklı sayıda şablon parametresine sahip aynı isimli ve aynı parametrik yapıya ilişkin fonksiyonlar da bir arada bulunabilmektedir. Yani şablon parametrelerinin sayısı fonksiyon imzasını değiştirmektedir. Örneğin aşağıdaki iki fonksiyon şablonu bir arada bulunabilir: template void foo() { //... } template void foo() { //... } Aşağıdaki çağrımlar yapılmış olsun: foo(); // üstteki foo fonksiyonu çağrılır foo(); // alttaki foo fonksiyonu çağrılır --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf bir sınıf şablonundan türetilebilir. Tabii bu durumda taban sınıf belirtilirken şablon parametresinin açısal parantezler içerisinde belirtilmesi gerekir. örneğin: template class A { //... }; class B : public A { //... }; Burada B sınıfı A sınıfının int açılımından türetilmiştir. Tabii bir sınıf şablonu normalk bir sınıftan da türetilebilir. Örneğin: class A { //... }; template class B : public A { //... }; Bu duurmda B'nin her bir açılımı A'dan türetilmiş olmaktadır. Örneğin: B x; B y; ... Bir sınıf şablonu başka bir sınıf şablonundan da türetilebilir. Örneğin: template class A { //... }; template class B : public A { //... }; Burada B hangi tür ile açılırsa aslında o A'nın aynı türden açılımından türetilmiş olur. Örneğin: B b; Burada B sınıfı A sınıfındna türetilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf şablonunun her farklı türdne açımı farklı bir tür belirtmektedir. Örneğin: template class Sample { //... }; Sample x; Sample *y; y = &x; // error! türler farklı --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıf şablonu her zaman kullanılırken açım türü belirtilerek kullanılmalıdır. Örneğin: template class Sample { //... }; Sample s; // error! Sample k; // geçerli Ancak istisna olarak bir sınıf şablonu sınıf bildirimi içerisinde ve sınıfın üye fonksiyonları içerisinde açım türü belirtilmeden kullanılabilir. Bu durumda açım türünün şablon türleri olduğu kabul edilir. Örneğin: template class Sample { public: void foo(); //... }; template void Sample::foo() { Sample a; // Smaple a ile aynı anlamda //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir şablon fonksiyonda ya da şablon sınıfta şablon parametresine dayalı tür isimlerini (type specifier) belirtirken typename anahtar sözcüğünün kullanılması gerekmektedir. Örneğin: template void foo() { T::some_name a; // dikkat hatalı! //... } Burada some_name T şablon parametresine ilişkin bir sınıfın elemanıdır. Bu eleman bir static veri elemanı da olabilir, bir typedef ismi de olabilir. İşte eğer programcı bu some_name elemanının bir tür ismi olduğunu (yani typedef ismi olduğunu) biliyorsa bunun önüne (yani soluna) typename anahtar sözcüğünü getirmesi gerekir. Yukarıdaki kodun doğru biçim şöyle olmalıydı: template void foo() { typename T::some_name a; // geçerli //... } Tabii eğer şablon parametresine dayalı olan isim bir tür ismi değilse typename anahtar sözcüğü kullanılmamlıdır. Örneğin: template void foo() { //... T::some_name * a; //... } Böyle bir ifadenin önünde typename anahtar sözcüğü olmadığı için derleyici bunu bir tür ismi olarak ele almaz, static bir elemanmış gibi ele alır. Dolayısıyla burada bir çarpma işlemi söz konusudur. Halbuki bu ifadenin önünde typename anahtar sözcüğü olsaydı bu bir gösterici bildirimi olurdu. Pekiyi derleyici açım sırasında zaten bu ismin (örneğimizde some_name) bir tür ismi olup olmadığını anlayamaz mı? Aslında derleyici açıma kadar beklerse bu ismin zaten ne olduğunu anlayabilir ve durumu uygun olarak ele alabilir. Pekiyi o zaman typename anahtar sözcüğüne neden gerek duyulmuştur? İşte bunun nedeni derleyicilere daha fazla olanak sunmaktır. Yani derleyiciler şablonu ilk kez gördüklerinde henüz açım yapılmadan bazı senktaks çözümlemelerini yapabilirler. Standartlar bu biçimde çalışan derleyicilere olanak sağlamak amacıyla tür isimlerinin açıkça typename ile belirtilmesini zorunlu tutmaktadır. Fakat derleyicilerin bir bölümü bu tür ifadeleri açım zamanında ele aldığından herhangi bir sorunla karşılaşılmamaktadır. Fakat yukarıda da belirttiğimiz gibi kural gereği şablon parametresine dayalı tür isimlerini belirtirken typename anahtar sözcüğünün kullanılması gerekmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şablon parametrelerine default tür verilebilir. Bu konu aslında fonksiyonlardaki default argüman kullanımına mantıksal olarak benzemektedir. Eğer bir şablon parametresi özellikle belli bir türle kullanılıyorsa yazım kolaylığı oluşturmak için o şablon parametresine default olarak o tür verilebilir. Bu işlem şablon parametresinden sonra '=' sentaksıyla yapılmaktadır. Örneğin: template class Sample { //... }; Burada Sampel sınıfınını kullanırken biz yalnızca T şablon parametresi için tür belirtebiliriz. Bu durumda K türü default olarak int alınacaktır. Örneğin: Sample s; bildirimi aşağıdakiyle tamamen eşdeğerdir: Sample s; Yine tıpkı default argüman alan fonksiyonlarda olduğu gibi bir şablon parametresine default değer verilmişse onun sağındakilerin hepsine default değer verilmiş olmak zorundadır. Örneğin: template class Sample { //... }; Bu bildirim geçerli değildir. Ancak aşağıdaki bildirim geçerlidir: template class Sample { //... }; Bir şablon parametresinin tüm türleri default değer alıyorsa eskiden yine de açısal parantezlerin kullanılması gerekiyordu. Ancak C++17 iel birlikte yapıcı fonksiyondna hareketle otomatik tür belirlemesi kuralı dile eklenmiş ve artık bu zorunluluk ortadan kalkmıştır. Örneğin: template class Sample { //... }; Burada iki şablon parametresi de default tür almaktadır. Dolayısıyla biz bu iki türü de nesne yaratırken belirtmek zorunda değiliz. Ancak eskiden yine de içi boş açısal parantezlerin kullanılması gerekiyordu. Örneğin: Sample<> s; Ancak C++11 ile birlikte bazı koşullar altında bu belirlemnin yapılması gerekmeyebilmektedir. Örneğin: Sample s; // C++17 ile birlite geçerli --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 100. Ders 25/09/2004 - Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir şablon parametresine verilen default türde önceki şablon parametreleri kullanılabilir. Örneğin: template > class Sample { //... }; Burada birinci şablon parametresi hangi türdense ikinci şablon parametresi vector sınıfının o türden açılımı türündendir. Örneğin: Sample s; tanımalaması ile aşağıdaki tanımlama tamamen eşdeğerdir: Sample> s; Tabii yukarıda da belirttiğimiz gibi bu durumda bir şablon parametresi için default tür belirtilirken ancak onun solundaki şablon parametreleri kullanılabilir, sağındaki parametreler kullanılamaz. Örneğin orijinal vector sınıfının standartlardaki bildirimi şöyledir: template> class vector { //... }; Görüldüğü gibi aslında vector sınıfının bir tane değil iki tane şablon parametresi vardır. İkinci şablon parametresinin değiştirilemsine çok seyrek gereksinim duyulmaktadır. Bu durumda örneğin: vector v; tanımlaması ile aşağıdaki bildirim eşdeğerdir: vector> v; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın çok kullanılan utility sınıflarından biri de pair isimli şablon sınıftır. Bu sınıfın bildirimi başlık dosyası içerisindedir: template struct pair; pair sınıfının iki şablon parametresi vardır. Sınıf iki farklı bilgiyi tek bir nesne biçiminde ifade etmek için kullanılmaktadır. Birden fazla nesneyi tek bir nesne biçiminde ifade etmek için bazı dillerde tuple ("tyupıl" ya da "tapıl" biçiminde okunuyor) denilen türler bulunmaktadır. Bu dillerdeki tuple türleri istenildiği kadar nesneyi tutabilmektedir. C++'a C++11 ile birlikte "variadic template" özelliği eklendikten sonra standart kütğphaneye ayrıca bir tuple sınıfı da eklenmiştir. pair sınıfı iki elemanlı bir tuple gibidir. Sınıfın yapıcı fonksiyonu iki elemanı alır ve sınıfın first ve second isimli public veri elemanlarında saklar. Programcı da bu elemanlara istediği zaman erişebilir. Örneğin: pair p(10, 3.14); cout << p.first << ", " << p.second << endl; // 10, 3.14 Aynı türden iki pair nesnesi == operatörü ile ve <=> opertatörü ile karşılaştırılabilmektedir. Eskiden aynı türden pair nesneleri tüm karşılaştırma operatörleriyle karşılaştırılabiliyordu. Sonra C++20 ile birlikte bu özellik silindi. pair sınıfı yardımcı bir sınıftır. İki farklı nesnesyi tek bir nesne gibi ifade etmekte kullanılır. Örneğin fonksiyonların geri dönüş değerlerinde çağırana iki değer iletmek için pair kullanılabilir. Örneğin bir dizinin hem en küçük elemanına hem de en büyük elemanına geri dönen bir fonksiyon bir pair nesnesi ile geri dönebilir: template pair minmax(const T *array, size_t size) { T min, max; min = max = array[0]; for (size_t i = 1; i < size; ++i) if (array[i] > max) max = array[i]; else if (array[i] < min) min = array[i]; return pair(min, max); } Biz de fonksiyonu şöyle çağırabiliriz: int a[] = {4, 6, 9, 2, 5}; auto pvals = minmax(a, 5); cout << pvals.first << ", " << pvals.second; Ya da örneğin bir dizide bir elemanı arayan find isimli bir fonksiyon yazmak isteyelim. Fonksiyon elemanın bulunduğu yerin dizi indeksini bize versin. Ancak indeks size_t olduğu için indeksin bulunamaması negatif bir değerle ifade edilemeyecektir. Bu durumda örneğin böyle bir fonksiyonu pair türü ile geri döndürebiliriz. pair nesnesinin ilk elemanı aramanın başarısını ikinci elemanı eğer arama başarılıysa bulunan indeksini belirtebilir: template pair find(const T *array, size_t size, const T &val) { size_t i; for (size_t i; i < size; ++i) if (array[i] == val) return pair(true, i); return pair(false, i); } Fonksiyon şöyle kullanılabilir: int a[] = {4, 6, 9, 2, 5}; auto presult = find(a, 5, 90); if (presult.first) cout << "found at index " << presult.second << endl; else cout << "cannot find!.." << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- pair sınıfının kullanımında biraz zorluk vardır. Çünkü programcı çok tuşa basmaktadır. pair nesnesi yaratılırken mecburen şablon parametreleri belirtilmek zorundadır. Pekiyi bu yazım nasıl kolaylaştırılabilir? İşte fonksiyon şablonlarındaki şablon parametrelerinin belirlenmesi (template argument deduction) özelliğinden faydalanılarak ek bir fonksiyonla yazım kolaylaştırılabilir. Örneğin: template inline pair mymake_pair(T1 first, T2 second) { return pair(first, second); } Böylece yukarıdaki find fonksiyonunu aşağıdaki gibi yeniden yazabiliriz: template pair find(const T *array, size_t size, const T &val) { size_t i; for (i = 0; i < size; ++i) if (array[i] == val) return mymake_pair(true, i); return mymake_pair(false, i); } Aslında burada yazmış olduğumuz mymake_pair fonksiyonu zaten balık dosyasında make_pair ismiyle bulunmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; template pair find(const T *array, size_t size, const T &val) { size_t i; for (i = 0; i < size; ++i) if (array[i] == val) return make_pair(true, i); return make_pair(false, i); } int main() { int a[] = {4, 6, 9, 2, 5}; auto presult = find(a, 5, 2); if (presult.first) cout << "found at index " << presult.second << endl; else cout << "cannot find!.." << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta C ile uyumlu olan sınıflara "aggregate sınıf (aggregate class)" denilmektedir. Aggregate sınıf kavramı C ile uyumu korumak için başından beri C++'ta bulunyordu. Ancak C++'ın stnadratları içerisinde tanımlamalarda zamanla değişiklikler yapılmıtır. Mevcut sıtandartlarda bir sınıfın (tabii bir yapı da bir sınıfır) aggregate olabilmesi için kabaca şu koşulları sağlıyor olması gerekir: - Sınıftaki bütün veri elemanları public bölümde olmak zorundadır. Bu durum bu sınıfın C'deki yapı gibi olması anlamına gelir. - Sınıfta sanal fonksiyon olmamlıdır. - Sınıf başka bir sınıftan türetilmişse taban sınıf da aggregate olmalıdır. Eskiden beri aggregate sınıf nesnelerine küme paranteziyle ilkdeğer verilebiliyordu (C uyumunu korumak için). Ancak C++11 ile birlikte "uniform initializer syntax" dile eklendiğinde aggregate sınıf türünden nesnelere küme parantezleri ile değer atanabililir hale gelmiştir. Örneğin: class Sample { public: int first, second; }; void foo(Sample s) { //... } //... Sample s = {100, 200}; // eskiden beri geçerliydi foo({10, 20}); // C++11 ile birlikte geçerli s = {10, 20}; // C++11 ile birlikte geçerli İşte bu özelliklerden dolayı C++1 ile birlikte pair nesnelerine de aslında bu biçimde değerler atanabilmektedir. Çünkü pair sınıfı da aggregate bir sınıftır. Örneğin: class Sample { public: int first, second; }; void foo(pair p) { //... } //... pair p{10, 3.14}; foo({1, 2.71828}); // C++11 ile birlikte geçerli p = {5, 6.28}; // C++11 ile birlikte geçerli Bu durumda daha önce yazmış oladuğumuz find fonksiyonu C++11 ile birlikte şöyle de yazılabilir: template pair find(const T *array, size_t size, const T &val) { size_t i; for (i = 0; i < size; ++i) if (array[i] == val) return {true, i}; return {false, i}; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart kütüphanede özel algoritmik bir yapı ile elemanları tutan bir grup sınıf vardır. Bunlara İngilizce "associative containers" denilmektedir. Bunların listesi şöyledir: map set multimap multiset unordered_map unordered_set unordered_multimap unordered_multiset map, set, multimap ve multiset sınıfları eskiden beri vardı. Ancak bunların unordered biçimleri C++11 ile eklendi. Bu nesne tutan sınıflar (container classes) anahtar-değer çiftlerini saklar.Sonra anahtar verildiğinde değeri özel algoritmalarla çok hjızlı bir biçimde geri verir. Her ne kadar standartlarda bu sınıfların kullandığı veri yapıları hakkında bir şey söylenmemişse de tipik olarak map, set, multimap ve multiset sınıfları dengelenmiş ikili ağaçlarla (balanced binary trees), bunların unordered biçimleri ise "hash tablolarıyla" gerçekleitirilmektedir. C++ kütüphaneleri genel olarak dengelenmiş ağaçlarda AVL algoritmasını, hash tablolarında da "açık adresleme (open addressing)" yöntemini tercih etmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- map sınıfının iki şablon parametresi vardır. Birinci şablon parametresi anahtarın türünü ikinci şablon parametresi değerin türünü belirtir. Örneğin biz isimleri anahtar, kişilerin numaralarını da değer yaparak bir map nesnesini şöyle oluşturabiliriz. map m; Bir map nesnesine anahtar değer çiftleri tipik olarak [] operatör fonksiyonu ile eklenmektedir. [] operatörünün içerisine anahtar yazılıp buna bir değer atanırsa o anahtar-değer çifti nesneye eklenmiş olur. Örneğin: map m; m["ali"] = 123; m["veli"] = 419; m["selami"] = 781; m["ayse"] = 114; m["fatma"] = 678; [] operatörü ile biz aynı zamanda bir anahtara karşı gelen değeri de elde edebiliriz. Örneğin: cout << m["selami"] << endl; // 781 Ancak anahtar yoksa bu operatör fonksiyonu değeri default değerlerle (yani Value() ifadesiyle) ouşturup anahtar-değer çiftini eklemekte ve eklediği default anahtar değeri ile geri dönmektedir. Örneğin: cout << m["sacit"] << endl; // 0 Nesneye eklenmiş toplam eleman sayısı diğer nesne tutan sınıflarda olduğu gibi sınıfın size üye fonksiyonu ile elde edilebilir. Sınıfın at metodu anahtar verildiğinde bize değeri vermektedir. Ancak anahtar bulunamazsa exception (out_of_range exception) fırlatmaktadır. Belli bir anahtarın olup olmadığını test eden, exception fırlatmayan ve eğer anahtar varsa onun değerini de elde eden find isimli üye fonksiyon bulunmaktadır. find fonksiyonu anahtarı alır ve bize bir iteratör verir. Bu iteratör * operatörü ile kullanıldığında bulunan elemanın anahtar-değer çifti bir paair nesnesi biçiminde verilmektedir. Eğer find başrısız olursa end iteratörü ile geri dönmektedir. Örneğin. map::iterator iter; iter = m.find("ayse"); if (iter != m.end()) cout << "found: " << iter->second << endl; else cout << "cannot find!.." << endl; Tabii bu tür durumlarda auto tür belirleyicisi daha kolay yazıma yol açmaktadır: auto iter = m.find("ayse"); if (iter != m.end()) cout << "found: " << iter->second << endl; else cout << "cannot find!.." << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir map nesnesi iteratör yoluyla dolaşılabilir. Bu durumda her dolaşımda bir anahtar-değer çifti pair nesnesi olarak elde edilir. map sınıfını iteratör yoluyla dolaşıldığında anahtara göre sıralı bir dolaşım yapılmaktadır. (İkili ağaçlarda sıralı dolaşımın "in-order" biçimde yapıldığını anımsayınız.) Örneğin: for (map::iterator iter = m.begin(); iter != m.end(); ++iter) cout << '(' << iter->first << " ," << iter->second << ')' << endl; Tabii burada yine auto belirleyicisi yazımı kolaylaştırmaktadır: for (auto iter = m.begin(); iter != m.end(); ++iter) cout << '(' << iter->first << " ," << iter->second << ')' << endl; map nesneleri de C++11 ile eklenen aralık tabanlı for döngüleriyle dolaşılabilir. Budurumda her dolaşımda yine pair nesneleri elde edilmektedir. Örneğin: for (pair kv : m) cout << '(' << kv.first << " ," << kv.second << ')' << endl; Burada yine auto belirleyicisi ile yazım kolaylaştırılabilir: for (auto kv : m) cout << '(' << kv.first << " ," << kv.second << ')' << endl; Referans yoluyla dolaşım da yapabiliriz. Burada referans doğrudan ikili ağaçtaki düğümdeki verileri gösteriyor durumda olur. Bu biçimde değer güncellenebilir. Ancak anahtar güncellenemez. Örneğin: for (auto &kv : m) cout << '(' << kv.first << " ," << kv.second << ')' << endl; map nesneleri anahtara göre ters sırada da dolaşılabilir. Bunun için reverse_iterator kullanmak gerekir. Örneğin: for (map::reverse_iterator iter = m.rbegin(); iter != m.rend(); ++iter) cout << '(' << iter->first << " ," << iter->second << ')' << endl; Tabii yine auto belirleyicisi ile yazım kolaylaştırılabilir: for (auto iter = m.rbegin(); iter != m.rend(); ++iter) cout << '(' << iter->first << " ," << iter->second << ')' << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıfın erase üye fonksiyonları ile anahtar verilerek anahtar-değer çifti silinebilir. erase fonksiyonları anahtarla da iteratörlerle de çalışabilmektedir. Eğer erase fonksiyonuna anahtar verilirse eğer anahtar bulunamazsa erase fonksiyonu 0 ile anahtar bulunup silinme gerçekleşirse 1 ile geri dönemktedir. Örneğin: map m; m["ali"] = 123; m["veli"] = 419; m["selami"] = 781; m["ayse"] = 114; m["fatma"] = 678; for (auto &kv : m) cout << '(' << kv.first << " ," << kv.second << ')' << endl; if (m.erase("ayse") == 0) cout << "cannot find key!.." << endl; else cout << "key-value erased" << endl; cout << "--------------------" << endl; for (auto &kv : m) cout << '(' << kv.first << " ," << kv.second << ')' << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart kütüphanede map sınıfına işlevsel olakka benzeyen set isimli bir sınıf da vardır. set sınıfı "küme kavramını" oluşturmak için düşünülmüştür. Matematikte küme "farklı elemanlardna oluşan topluluklara" denilmektedir. Matematiksel olarak bir kümede aynı eleman birden fazla bulunamaz. set sınıfının arka planda kullandığı veri yapısı map sınıfına çok benzemektedir. Genellikle C++ kütüphanelerinde bu veri yapısı da "dengelenmiş ikili ağaçlarla (balanced binary trees)" gerçekleştirilmektedir. set sınıfı isimli başlık dosyasında bildirilmiştir. Bir set nesnesi yine kümenin tutacağı elemanın türü belirtilerek yaratılır. Örneğin: set s; set sınıfının anahtar-değer çiftlerini tutmadığına yalnızca anahtar tuttuğuna dikkat ediniz. set veri yapısına eleman eklemek için insert üye fonksiyonları kullanılmaktadır. insert fonksiyonları anahtarı alarak elemanı kümeye yerleştirir ve yerleştirdiği yere ilişkin bir pair nesnesi ile geri döner. pair nesnesinin birinci elemanı eklenen elemana ilişkin iteratör değerinden, ikinci elemanı ise eklemenin başarılı ya da başarısız olduğunu belirten bool değerden oluşmaktadır. Örneğin: set s; s.insert(100); s.insert(400); s.insert(300); s.insert(200); pair::iterator, bool> result; result = s.insert(400); if (!result.second) { cout << "cannot insert"; exit(EXIT_FAILURE); } set nesnesi aynı elemandan birden fazla tutamayacağı için yukarıdaki son insert işlemi başarısız olacaktır. Tabii auto tür belirleyicisi burada yazımı oldukça kolaylaştırmaktadır. Örneğin: auto result = s.insert(400); if (!result.second) { cout << "cannot insert"; exit(EXIT_FAILURE); } Bir elemanın küme içerisinde olup olmadığını anlamak için find üye fonksiyonları kullanılmaktadır. Bu find fonksiyonları anahtar verildiğinde eğer elemanı küme içerisinde bulursa onun iteratör değerine, bulmazsa end iteratör değerine geri dönmektedir. Örneğin biz belli bir elemanın küme içerisinde olup olmadığını şöyle sorgulayabiliriz: set::iterator iter = s.find(300); if (iter != s.end()) cout << "found: " << *iter << endl; else cout << "cannot find!.." << endl; C++20 ile birlikte sınıfa aynı işi yapan contains isimli üye fonksiyonlar da eklenmiştir. Bu contains fonksiyonları bool bir değere geri dönmektedir. Örneğin: if (s.contains(300)) cout << "found..." << endl; else cout << "cannot find!.." << endl; Yine diğer nesne tutan sonıflarda (container classes) olduğu gibi set nesnesi içerisinde tutulan elemanların sayısı size üye fonksiyonuyla elde edilebilmektedir. Örneğin: set s; cout << s.size() << endl; // 4 Bir set nesnesi yine iki yönlü dolaşılabilmektedir. Yani set sınıfının iteratörleri iki yönlü (bidirectional) iteratörlerdir. Dolaşım aşağıdaki gibi yapılabilir: set s; s.insert(100); s.insert(400); s.insert(300); s.insert(200); for (set::iterator iter = s.begin(); iter != s.end(); ++iter) cout << *iter << " "; cout << endl; for (set::reverse_iterator iter = s.rbegin(); iter != s.rend(); ++iter) cout << *iter << " "; cout << endl; Normal dolaşım küçükten büyüğe, tersten dolaşım büyükten küçüğe yapılmaktadır. İteratör yoluyla dolaşabildiğimiz her nesneyi aralık tabanlı for döngüsüyle de dolaşabiliriz. Örneğin: for (auto val : s) cout << val << " "; cout << endl; Kümeden eleman silmek için erase üye fonksiyonları kullanılmaktadır. Silinecek elemana ilişkin ietaratörü parametre olarak alan erase fonksiyonları başarı durumunda silinen elemandna sonraki elemana ilişkin iteratör değerini geri dönüş değeri olarak verir. Silinecek elemana ilişkin anahtarı parametre olarak alan erase fonksiyonları ise kaç elemanın silindiği değerine (yani 0 ya da 1 değerine) geri dönmektedir. Örneğin: set s; s.insert(100); s.insert(200); s.insert(300); s.insert(400); for (auto val : s) cout << val << " "; cout << endl; if (s.erase(300)) cout << "erased..." << endl; else cout << "canno erase..." << endl; for (auto val : s) cout << val << " "; cout << endl; Eğer silme işleminde erase fonksiyonuna silinecek elemanın iteratörü verilirse yukarıda da belirttiğimiz gibi bu durumda fonksiyon silme başarılıysa silinen elemandan sonraki elemanın iteratör değerine (bu end iteratör de olabilir) geri dönmektedir. Tabii biz fonksiyona iteratör değerini parametre olarak verdiğimize göre zaten silmenin başarısız olması beklenemez. Örneğin: set s; s.insert(100); auto pos = s.insert(200); s.insert(300); s.insert(400); for (auto val : s) cout << val << " "; cout << endl; auto result = s.erase(pos.first); for (; result != s.end(); ++result) // 300 400 cout << *result << " "; cout << endl; set sınıfında elemana erişmek için bir [] operatör fonksiyonun olmadığına dikkat ediniz. set sınıfının neden gereksinim duyulabileceği konusunda kişilerin kafası karışabilmektedir. Bu sınıf tipik olarak şu amaçlarla kullanılmaktadır: 1) Çok sayıda değerin söz konusu olduğu (genellikle veri analizi uygulamalarında bu durumla karşılaşılır) durumlarda yinelenen (mükerrer) değerlerin atılması için set veri yapısı kullanılabilmektedir. 2) Bir grup elemanı sıralı tutmak için set veri yapısı kullanılabilmektedir. Bilindiği gibi sıralı bir diziye eleman insert etmek yüksek maliyetli bir işlemdir. 3) Küme işlemleri yapmak için de set veri yapısı kullanılabilmektedir. Örneğin iki kümenin kesişimi, birleşimi gibi işlemler set sınıfı yoluyla kolay bir biçimde yapılmaktadır. (Standart kütüphanede bu işlemler için set_xxx biçiminde fonksiyonlar bulundurulmuştur. Biz bu fonksiyonları burada ele almayacağız.) 4) Yine istenirse bu sınıf da anaharı ve değeri birlikte tutan sınıflarda anahtar verildiğinde değerin elde edilmesi amacıyla da kullanılabilir. Burada buna ilişkin örnek vermeyeceğiz. 5) set veri yapısı "var mı yok mu" testleri için de kullanılabilmektedir. map ve set sınıflarında insert, find ve erase işlemlerinin logaritmik bir karmaşıklıkta yapıldığnı bir kez daha ifade etmek istiyoruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- map ve set sınıflarının aynı anahtarı birden fazla kez tutan multimap ve multiset isimli multi'li biçimleri de vardır. multimap sınıfının genel kullanımı map sınıfına benzemektedir. Ancak bu sınıfta [] operatör fonksiyonu yoktur. Elemanlar insert fonksiyonlarıyla eklenir. Ancak aynı anahtara fakat farklı değerlere sahip anahtar-değer çiftleri aynı multimap nesnesi içerisinde bulunabilmektedir. Örneğin: multimap mm; mm.insert(make_pair("ali", 115)); mm.insert(make_pair("veli", 314)); mm.insert(make_pair("selami", 68)); mm.insert(make_pair("ayse", 641)); mm.insert(make_pair("fatma", 312)); mm.insert(make_pair("ali", 200)); mm.insert(make_pair("ali", 500)); İteratör yoluyla multimap nesnesi benzer biçimde dolaşılmaktadır: for (multimap::iterator iter = mm.begin(); iter != mm.end(); ++iter) cout << iter->first << ", " << iter->second << endl; Bu dolaşımdan ekranda şunlar görünecektir: ali, 115 ali, 200 ali, 400 ayse, 641 fatma, 312 selami, 68 veli, 314 Pekiyi biz find ile belli bir elemanı arasak aynı anahtara sahip olan anahtar-değer çiftlerini nasıl elde ederiz? Elemanı find ile bulup değer aynı olduğu sürece döngüde ilerleyebiliriz. Ancak bunun için sınıfta zaten equal_range isimli bir üye fonksiyon bulundurulmuştur. equal_range bize iki iteratörden oluşan bir pair nesnesi vermektedir. Bu pair nesnesinin ilk elemanı başlangıç iteratörü ikici elemanı ise bitiş iteratörüdür. Örneğin anahtarı "ali" olan tüm anahtar değer çiftlerini şöyle elde edebiliriz: auto result = mm.equal_range("ali"); for (auto iter = result.first; iter != result.second; ++iter) cout << iter->first << ", " << iter->second << endl; Ayrıca sınıfta aynı anahtara ilişkin ilk ve son iteratörleri ayrı ayrı veren lower_bound ve upper_bound üye fonksiyonları da bulunmaktadır. multimap ve multiset sınıfları ile ilgili başka ayrıntılar da vardır. Ancak burada bu ayrıntılar üzerinde durmayacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte map ve set sınıflarının "unordered" biçimleri de kütüphaneye eklenmiştir. Bu sınıflar "hash tablosu" denilen veri yapıları ile elemanları tutmaktadır. Hash tabloları bir elemanı hızla aramak, hızlı insert ve erase işlemleri yapmak için kullanılan önemli bir veri yapısıdır. map ve set sınıflarının unordered biçimleri şunlardır: unordered_map unordered_set unordered_multimap unordered_multiset Bu sınıfların bildirimleri ve başlık dosyasındadır. Pekiyi map ile unordered_map arasında set ile unordered_set arasında ne farklılık vardır? Daha önceden de belirttiğimiz map ve set sınıfları veri yapısı olarak "dengelenmiş ikili ağaçları" kullanmaktadır. Halbuki unordered_map ve unordered_set sınıfları veri yapısı olarak "hash toblolarını" kullanmaktadır. O halde bu iki tür sınıf arasındaki fark aslında bu veri yapıları arasındaki farkla ilgilidir. Bu veri yapıları arasında benzerlikler ve farklılıklar şöyle özetlenebilir: - Her iki veri yapısı da hızlı arama amacıyla kullanılabilmektedir. Ancak hash tablolarının arama performası dengelenmiş ikili ağaçlardan toplamda daha yüksektir. - Dengelenmiş ikili ağaçlar elemanları anahtara göre sıralı tutmaktadır. Halbuki hash tablolarında elemanlar herhangi bir biçimde sıralı tutulamamaktadır. - Hash tabloları genel olarak daha fazla bellek kullanma eğilimindedir. O halde daha hızlı arama, silme ve ekleme işlemleri için bunların unordered biçimleri tercih edilebilir. Ancak eleman sırası önemliyse nomal biçimler kullanılmalıdır. Ancak bu sınıfların unordered biçimlerinin genel kullanımı normal biçimlerine çok benzemektedir. Örneğin: unordered_map um; int val; um.insert(make_pair("ali", 115)); um.insert(make_pair("veli", 314)); um.insert(make_pair("selami", 68)); um.insert(make_pair("ayse", 641)); um.insert(make_pair("fatma", 312)); val = um["fatma"]; cout << val << endl; // 312 unordered_map ve unordered_set nesneleri yine iteratör yoluyla dolauşabilir. Ancak dolaşım herhangi bir sıraya göre yapılmamaktadır. Örneğin: for (auto iter = um.begin(); iter != um.end(); ++iter) cout << iter->first << ", " << iter->second << endl; Şöyle bir çıktı elde edilmiştir: ayse, 641 veli, 314 ali, 115 selami, 68 fatma, 312 Hash tablolarında "ayrı zincie oluşturma yönteminde" her zincire "bucket" de denilmektedir. Genellikle kütüphaneler ayrı zincir oluşturma yöntemini kullanmaktadır. Örneğin: cout << um.size() << endl; // 5 cout << um.bucket_count() << endl; // 8 fakat farklı olabilir unordered_set sınıfı da genel kullanım olarak set sınıfına benzemektedir. Örneğin: unordered_set us; us.insert(115); us.insert(314); us.insert(68); us.insert(641); us.insert(312); for (auto iter = us.begin(); iter != us.end(); ++iter) cout << *iter << " "; cout << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyon şablonu ya da sınıf şablonu kullanıldığı zamanın dışına programcının isteği ile "açıkça (explicit)" açılabir. Sınıf şablonları için açıkça açımın genel biçimi şöyledir: template <<şablon_parametrelerinin_listesi>>; Örneğin: template class Sample; template class vector; Burada biz kod içerisinde vector sınıfını ve Sample sınıfını hiç kullanmamış olsak bile bu sınıfların int açılımları oluşturulup amaç dosyaya yazılacaktır. Aynı durum fonksiyonlarında da söz konusudur. Fonksiyon şablonlarının açıkça açımı da benzer bir sentakla yapılmaktadır: template <<şablon_parametreleri>>(); Örneğin: template void foo(int a); Tabii bu açıkça açımın yapılması için daha önceden fonksiyonun şablon bildiriminin derleyici tarafından görülmesi gerekmektedir. Pekiyi açıkça açımın amacı nedir? İşte C++11 ile birlikte şablonlar extern olarak bildirilebilmeye başlanmıştır. extern bildirimin sınıflardaki genel biçimi şöyledir: extern template <<şablon_parametreleri>>; Bir proje birden fazla kaynak dosyadan oluştuğunda kaynak doslayar aynı sınıf şablonunun aynı türden açılımını kullandığında burada etkin olmayan iki durum söz konusudur: 1) Her dosya derlenirken derleyici ilgili sınıf şablonunu aynı tür için yeniden açar. Bu da toplam derleme zamanını uzatabilmektedir. 2) Bu biçimde her kaynak dosya derlenirken açım kendi amaç dosyası içerisinde yer kaplayacaktır. (Tabii linker bunlardan yalnızca tek bir kopyayı çalıştırılabilen dosyaya yazacaktır. ncak her amaç dosyada bunların yer kaplaması bir dezavantajdır.) İşte bir şablon açımı extern olarak bildirilebilir. Bu durumda derleyici bu şablonun kullanılmasına izin verir. Ancak şablonu o kaynak dosyada açmaz. Bu durum tanımlanan bir global değişkenin extern yapılarak başka bir kaynak dosyada kullanımına benzemektedir. Örneğin Sample sınıfının şablon bildirimi "sample.hpp" dosyası içerisinde olsun. Biz de "a.cpp", "b.cpp", "c.cpp" dosyaları içerisinde bu sınıfın int açılımını kullanacak olalım. Bu durumda bu sınıfın int açılımı "açıkça (explicit)" bir kaynak dosyada yapılır. Sonra diğer kaynak dosyalarda extern bildirimi bulundurulur. Böylece extern bildirimi ile kullanılan açılımlar gereksiz bir biçimde o kaynak dosyalara ilişkin amaç dosyalarda yer kaplamaz. Çünkü zaten derleyici bu açımları o dosyalarda hiç yapmayacaktır. Örneğin: // a.cpp #include "sample.hpp" template class Sample; // açıkça açım (explicit imstantiation) //... // b.cpp extern template class Sample; // extern bildirim //... // c.cpp extern template class Sample; // extern bildirim //... Görüldüğü gibi belli bir açılımı extern yapabilmek için extern template anahtar sözcükleri kullanılmaktadır. Fonksiyon şablonlarında da extern bildirimi yapılabilir. Genel biçim şöyledir: extern template ([parametre_bildirimi]);; Burada şablon parametreleri açılmış tür ile belirtilmektedir. Örneğin: tenplate void foo(T a) { //... } extern template void foo(int a); Bildirimde fonksiyon isminden sonra açısal parantezler içerisinde açım türü de belirtilebilir. Ancak buna gerek yoktur. Örneğin: extern template void foo(int a); Microsoft C++ derleyicilerinde extern şablon işlemleri derleyici tarafından henüz gerçekleştirilememiş olabilir. Ancak g++ ve clang++ derleyicileri bu işlemi başarıyla yapabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi bir fonksiyon şablonu çağrılırken biz şablon parametreleri için türleri açıkça açısal parantezler içerisinde belirtebiliyorduk. Örneğin: template void foo(T a, K b) { //... } //... foo(10, 3.14); Yine anımsanacağı gibi açıkça belirleme yapılmadıysa çağırma ifadesindeki argümanların türlerinden hareketle şablon parametrelerinin türleri tespit ediliyordu. Örneğin: foo(10, 3.14); Burada derleyici T türünün int olduğunu K türünün ise double olduğunu kendisi tespit edecektir. İşte fonksiyon argümanlarından hareketle şablon parametrelerinin türlerinin tespit edilmesi sürecine İngilizce "template argument deduction" denilmektedir. Biz bu sürece daha önce kabaca ele almıştık. Şimdi bu sürecin bazı ayrıntıları üzerinde duracağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şablon parametrelerinin tespit edilmesi sürecinde bazı ayrıntılar vardır. Ancak kabaca bu süreç argümanın türü ile şablon parametresinin türünün uyuşması için şablon parametresinin türünün ne olması gerektiğinin tespit edilmesi esasına dayanmaktadır. Örneğin: template void foo(T *a) { //... } //... double d; foo(&d); Burada &d ifadesi double * türündendir. Bu argüman T * türünden bir parametre değişkenine ilkdeğer olarak verilmiştir. O halde şöyle bir türsel eşitlik yazılabilir: T * = double * Burada T türünün double olduğu tespiti yapılacaktır. Tür tespitinde "üst düzey (top level)" const ve volatile belirleyicileri etkili olmamaktadır. Yani üst düzey const ve volatile belirleyicileri tür tesptinde atılmaktadır. Örneğin: template void foo(T a) { //... } //... const int a = 10; foo(a); Burada ""const int = T" eşitliğinden hareketle T türü tespit edilmeye çalışılsaydı T türü const int olarak belirlenirdi. Ancak a'daki const olma durumu üst düzey olduğu için tespit süreci "int = T" eşitliğinden hareketle yapılacaktır. Dolayısıyla T türü burada int olarak tespit edilecektir. Ancak üst düzey olmayan const ve volatile belirleyicileri tür tespitinde etkili olmaktadır. Örneğin: template void foo(T a) { //... } //... const char *s = "ankara"; foo(s); Burada T tür parametresi "const char * = T" eşitliğinden hareketle const char * olarak tespit edilecektir. Örneğin: template void foo(T *a) { //... } //... const char *s = "ankara"; foo(s); Burada T şablon parametresi "const char * = T *" eşitliğinden hareketle const char olarak tespit edilecektir. Bir referans argümanda kullanıldığında artık o argümanın türü o referansın gösterdiği nesnenin türündendir. Örneğin: template void foo(T a) { //... } //... int a = 123; int &r = a; foo(r); Burada T şablon parametresi "int = T" eşitliğinden hareketle tespit edilecektir. Dolayısıyla T şablon parametresi int türdendir. Örneğin: template void foo(T &r) { //... } //... int a = 10; foo(a); Burada T şablon parametresi "int = T &" eşitliğinden hareketle int olarak belirlenecektir. Ancak aşağıdaki durum kafa karıştırabilmektedir: template void foo(T &r) { //... } //... const int a = 10; foo(a); Burada a'daki const olma durumu sanki üst düzeymiş gibi düşünülmektedir. Ancak aslında arka planda a'nın adresi alınıp r'ye yerleştirileceği için buradaki const belirleyicisi bu örnekte tür tespitinde etkili olmaktadır. Tespit "const int = T &" biçiminde yapılmaktadır. Dolayısıyla bu örnekte T int olarak değil const int olarak tespit edilecektir. Yani bu örnekteki const niteleyicisi üst düzey olarak ele alınmamaktadır. Dizi isimleri dizilerin başlangıç adreslerini belirttiği için tür tespitinde bir adres olarak işleme sokulmaktadır. Örneğin: template void foo(T p) { //... } //... int a[] = {1, 2, 3, 4, 5}; foo(a); Burada "int * = T" eşitliğinden hareketle T türü int * olarak tespit edilecektir. Ancak fonksiyon şablonunun parametresi bir referans ise biz de bu fonksiyonu bir dizi ismiyle çağırmışsak bu durumda tespit süreci farklı olacaktır. Örneğin: template void foo(T &r) { //... } //... int a[] = {1, 2, 3, 4, 5}; foo(a); Burada "int * = T &r" eşitliğinden hareketle değil "int[5] = T &" eşitliğinden hareketle T türü int[5] olarak yani "5 elemanlı int türden dizi türü olarak" tespit edilecektir. Bu çıkarımın gösterici karşılığı aşağıdaki gibidir: template void foo(T *r) { //... } //... int a[] = {1, 2, 3, 4, 5}; foo(&a); Örneğin: template void foo(T (&r)[5]) { //... } //... int a[] = {1, 2, 3, 4, 5}; foo(a); Burada T şablon parametresi "int[5] = T (&r)[5]" eşitliğinden hareketle int olarak tespit edilecektir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte sağ taraf değeri referansları dile eklenince şablonlardaki şablon parametrelerinin tespiti sürecinde bazı durumları sağlayabilmek için "universial reference" ya da "forwarding reference" isminde bir kavram da dile eklenmiştir. Biz buna "iletim referansı" diyeceğiz. Bir fonksiyon şablonunun şablon türünden sağ taraf değeri referanslı parametre değişkenine iletim referanslı parametre değişkeni denilmektedir. Aslında böyle bir parametre değişkeni tam bir sağ taraf değeri referansı gibi davranmamaktadır. Örneğin: template void foo(T &&r) { //... } Burada fonksiyonun birinci parametresi bir iletim referansıdır. Örneğin: template void foo(T &a, int &&b) { //... } Burada fonksiyon parametrelerinden hiçbiri bir iletim referansı (forwarding reference) değildir. Bir parametre değişkeninin iletim referansı olması için onun türünün şablon parametresi biçiminde olması ve dekleratörünün de && atomu ile sanki sağ taraf değeri referansıymış gibi belirtilmesi gerekmektedir. İletim referansları özel bir biçimde tür tespitine sokulmaktadır. Bi iletim referansı mutlak bir sağ taraf değeri referansı değildir. "Duruma göre göre sağ taraf referansıi duruma göre saol taraf değeri referansı gibi" davranmaktadır. Yani örneğin: template void foo(T &&r) { //... } Burada r parametre değişkeni bazı çağırmalarda sol taraf değeri referansı gibi, bazı çağırmalarda sağ taraf referansı gibi ele alınmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 103. Ders 07/10/2024 Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir iletim referansı eğer fonksiyon bir sol taraf değeri ile çağrılırsa sol taraf değeri referansı gibi, sağ taraf değeri ile çağrılırsa sağ taraf regeransı gibi işlev görmektedir. Örneğin: template void foo(T &&r) { //... } Burada foo fonksiyonunun parametresi bir iletim referansıdır. Şimdi fonksiyonu şöyle çağırmış olalım: foo(10); // r paramere değişkeni sağ taraf değeri referansı gibi ele alınıyor Burada parametre değişkeni olan r sanki bir sağ taraf değeri referansı gibi ele alınacaktır. Örneğin: int a = 10; foo(a); // r paramere değişkeni sol taraf değeri referansı gibi ele alınıyor Burada parametre değişkeni olan r bir sol taraf değeri referansı gibi ele alınacaktır. Pekiyi bu çağrılarda T'nin türü nedir? İşte T'nin türünü de dahil edersek iletim referansının oluşturduğu durumu şöyle özetleyebiliriz: 1) Eğer iletim referansına karşı gelen argüman K türünden bir sağ taraf değeri ise bu durumda şablon parametresi K türündendir. Fonksiyon parametre değişkeni de T && türünden kabul edilir. 2) Eğer iletim referansına karşı gelen argüman K türünden bir sol taraf değeri ise bu durumda şablon parametresi K & türündendir. Fonksiyon parametre değişkeni de K & türünden kabul edilir. 3) Eğer iletim referansına karşı gelen argüman K türünden bir const/volatile sol taraf değeri ise bu durumda şablon parametresi const/volatile K & türündendir. Fonksiyon parametre değişkeni de const/volatile K & türünden kabul edilir. Örneğin: template void foo(T &&r) { //... } Fonksiyonu şöyle çağırmış olalım: foo(10); // T = int, parametre değişkenin türü int && Burada T türü int olarak açılır, fonksiyonun parametre değişkeni de int && türünden olur. Örneğin: int a = 10; foo(a); // T = int &, parametre değişkeninin türü int & Burada T türü int & olarak, parametre değişkeninin türü ise int & olarak açılacaktır. Örneğin: const int a = 10; foo(a); // T = const int &, parametre değişkeninin türü const int & İletim referansına sahip bir fonksiyonun hem sol taraf değeri ile hem de sağ taraf değeri ile çağrılabildiğine dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İletim referanslarıyla ilgili "referans daraltması (reference collapsing)" denilen bir durum vardır. Referans daraltması iletim parametrelerinde ve auto tür belirleyicisinde etkili olmaktadır. T iletim referansının şablon parametresi olmak üzere (T const/volatile niteleyicilerine sahip de olabilir) aşağıdaki referanslar geçerlidir ve bu referanslar T & anlamına gelmektedir: T & T & & T & && T && & Aşağıdaki referans ise T && anlamına gelmektedir: T && Listeyi yeniden bir bütün olarak verelim: T & => T & T & & => T & T & && => T & T && & => T & T && => T && Pekiyi bu referans daraltması ne anlama gelmektedir? Aşağıdaki fonksiyon şablonuna dikkat ediniz: template void foo(T &&r) { //... } Burada r bir iletim parametresidir. Şimdi fonksiyonu şöyle çağırmış olalım: foo(10); Burada T'nin int türden olacağını belirtmiştik. Bu durumda fonksiyonun parametresi int && yani int türündne bir sağ taraf değeri referansı olacaktır. Şimdi fonksiyonu şöyle çağırmış olalım: int a = 10; foo(a); Burada T'nin int & türündne olduğunu belirtmiştik. Bu durumda fonksiyonun parametre değişkeni T & && türünden olacaktır. Referans daraltmasıyla bu da zaten int & anlamına gelecektir. Referans daraltması yalnızca iletim parametrelerinde ve auto tür bellirleyicisinde etkili olmaktadır. Bunun dışında referans daraltması oluşturulamamaktadır. Örneğin: int a = 10; int & &&r = a; // geçersiz! referans daraltması burada uygulanmaz Örneğin: template void foo(T &r) { int a = 10; T & &&r = a; // geçerisz! geçersiz! referans daraltması burada uygulanmaz } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi biz bir sağ taraf değeri referansını sol taraf değeri ile bağlayamıyorduk (bing edemiyorduk). Bunun için move fonksiyonunu kullanıyorduk. Aslında move fonksiyonu sağ taraf değerine tür dönüştürmesi yapmaktadır. Örneğin: string s("ankara"); string k; k = move(s); Burada s bir sol taraf değeridir. Biz onu move fonksiyonuyla bir sağ taraf değeri haline getirdik. Dolayısıyla artık kopya atama operatör fonksiyonu değil taşıma atama operatör fonksiyonu çağrılacaktır. Yani bu ifade aşağıdakiyle eşdeğerdir: k.operator =(move(s)); Tabii artık biz bu örnekte s'i atamadna sonra kullanmamalıyız. move çağrısı yerine dönüştürme açıkça da yapılabilmektedir. Daha önceden de belirttiğimiz gibi static_cast operatörü ile sağ taraf değeri referansına dönüştürme yapabiliriz: k = static_cast(s); Ancak move fonksiyonu daha güzel ve anlaşılır bir görüntü vermektedir: k = move(s); Pekiyi move fonksiyonu nasıl yazılmıştır? Biz move fonksiyonuna her türe ilişkin sol taraf değerini ve sağ taraf değerini argüman olarak verebilmekteyiz. Fonksiyonun aşağıdaki gibi yazılamayacağına dikkat ediniz: template T &&mymove(T a) { return static_cast(a); } Çünkü burada fonksiyon parametre değişkeninin referansıyla geri dönmektedir. Dolayısıyla bu durum tanımsız davranışa yol açar. Örneğin: k = mymove(s); Burada T'nin string olarak olarak açılacağına ve parametre için kopya yapıcı fonksiyonun çağrılacağına ve fonksiyonun bu parametre değişkeninin referansıyla geri döneceğine dikkat ediniz. Bu tamamen uygunsuz bir durumdur. Pekiyi fonksiyon şöyle yazılabilir miydi? template T &&mymove(T &a) { return static_cast(a); } Fonksiyon böyle yazılsaydı sol taraf değeir argümanı için çalışırdı ancak sağ taraf değeri argümanı için çalışmazdı. O halde burada mymove fonksiyonun parametresi iletim referansı olmalıdır. Böylece argüman hem referans yoluyla aktarılmış olur hem de biz fonksiyonu sol taraf değeriyle de sağ taraf değeri ile de çağırabiliriz. Pekiyi fonksiyon bu durumda şöyle yazılabilir miydi? template T &&mymove(T &&a) { return static_cast(a); } İşte fonksiyon böyle de yazılamaz. Çünkü bu durumda fonksiyonu örneğin string türünden bir sol taraf değeri ile çağırsak T türü sting & olarak açılacaktır. Bu durumda fonksiyonun parametresi ve geri dönüş değeri string & && biçiminde olacak ve bu da referans daraltması ile string & haline gelecektir. Üstelik de zaten static_cast sağ taraf değeri referansına değil sol taraf değeri referansınadönüştürme yapacaktır. Dolayısıyla örneğin: k = mymove(s); ile mymove fonksiyonu sol taraf değeri referansı verecek ve string sınıfının taşıma atama operatör fonksiyonu değil kopya atama operatör fonksiyonu çağrılacaktır. Pekiyi o halde move fonksiyonu nasıl yazılmıştır? İşte şimdiye kadar gördüğümüz olanaklarla move fonksiyonu yazılamaz. move fonksiyonunun yazılabilmesi için remove_reference denilen bir "type trait" sınıflarının kullanılması gerekmektedir. Tüm bu sınıflar başlık dosyasında bildirilmiştir. Type trait sınıflarının hepsi şablon biçiminde yazılmıştır. Dolayısıyla kullanılırken açısal parantezler içerisinde şablon parametesi belirtilir. Bu sınıfların hepsinin public bölümde type isimli bir tür ismi vardır. Örneğin remove_reference şöyle kullanılır: remove_reference::type Biz burada int türünü elde etmiş olduk. remove_reference açısal parantez içerisindeki türde sol taraf değeri ya da sağ taraf değeri referansı varsa onu silmektedir, eğer yoksa o türün aynısını elde etmektedir. Örneğin: remove_reference::type a; Burada a nesnesi int türdendir. Çünkü remove_reference int & türündeki referansı silmektedir. O halde bizim fonksiyonu şöyle yazmamız gerekir: template remove_reference::type mymove(T &&r) { return static_cast::type &&>(r); } Tabii burada geri dönüş değerinde auto tür belirleyicisi de kullanılabilirdi. Aslında kütüphanedeki orijinal move fonksiyonu constexpt ve noexcept olarak yazılmıştır. constexpt fonksiyonların kendiliğinden inline olduğunu anımsayınız. Yine anımsanacağı gibi bir fonksiyonun constexpr olması demek onun derleme aşamasında çağrılabilir olması demektir. Böylece move fonksiyonu aslında çalışma sırasında hiç çağrılmayacaktır. Fonksiyonun noexcept olması herhangi fonksiyon içerisinde herhangi bir exception'ın fırlatılmayacağı anlamına gelir. Zaten böyle bir fonksiyonda exception oluşması mümkün değildir. O halde fonksiyonun orijinali aşağıdaki gibi yazılmıştır. template constexpr remove_reference::type mymove(T &&r) noexcpept { return static_cast::type &&>(r); } Fonksiyon constexpr olduğu için geri dönüş değeri sabit ifadesi olarak da kullanılabilmektedir. Örneğin: int a[move(10)]; // geçerli Kursun yapıldığı sırada Microsoft derleyicilerinde yukarıdaki constexpr olmaması dolayısıyla hata ortaya çıkmaktadır. Bu muhtemelen gerçekleştirimle ilgili eksik kalmış bir noktadan kaynaklanmaktadır. Ayrıca C++20 ile birlikte type trait sınıflarının using ile sonu _t ile biten alternatif isimleri de oluşturulmuştur. Böylece programcı bu type elemanını kullanmak zorunda kalmamaktadır. Örneğin: template using remove_reference_t = typename remove_reference::type Böylece biz artık örneğin remove_referance::type yerine remove_reference_t ifadesini de kullanabiliriz. Dolayısıyla yularıdaki mymove fonksiyonu C++20 ve sonrasında aşağıdaki gibi de yazılabilir: template constexpr remove_reference_t &&mymove(T &&r) { return static_cast &&>(r); } C++11 ile birlikte başlık dosyasında remove_reference benzeri pek çok özel type trait bulundurulmuştur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standartlara göre fonksiyon şablonundaki şablon parametresinin türü nasıl tespit ediliyorsa (template argument deduction) auto tür belirleyicisi ile tanımlanmış olan değişkenin türü de aynı biçimde tespit edilmektedir. Yani standartlara göre örneğin: auto x = ifade; Burada x değişkeninin türü sanki x bir fonksiyon şablonunun parametre değişkeniymiş o fonksiyon da bu ifadeyle çağrılmış gibi el alınıp tespit edilmektedir. Yani tespit süreci daha önce görmüş olduğumuz şablon parametrelerinin türlerinin tespit edilmesinde olduğu gibidir. Başka bir deyişle yukarıdaki auto türünün tespiti aşağıdaki tespit ile aynı kurallara göre yapılmaktadır. template void foo(T a); foo(ifade); Örneğin: int a[10]; auto x = a; Burada auto int * anlamına gelmektedir. Örneğin: const int a = 10; auto &r = a; Burada auto const int anlamına gelmektedir. O halde fonksiyon şablonlarındaki iletim referansları auto'da da geçerlidir. Örneğin: auto &&r = 10; Burada r de bir iletim referansıdır. Dolayısıyla r (kendisine bir sağ taraf değeri bağlandığı için) sağ taraf değeri referansı olur. Yani auto burada int olarak açılacaktır. Ancak örneğin: int a = 10; auto &&r = a; Burada r yine bir iletim referansıdır. Kendisine bir sol taraf değeri bağlandığı için r bir sol taraf değeridir. Burada auto yine int & olarak açılır. İletim referanslarına referans daraltması uygulandığı için r'nin yine sol taraf değeri referansı olacağına dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir fonksiyon şablonunun başka bir fonksiyon şablonunu çağırdığını düşünelim. Bu durumda eğer çağrılan fonksiyon çağıran fonksiyondaki referans parametrelerini kullanacaksa onların değer kategorilerinin (sol taraf değeri, sağ taraf değeri) korunması gerekebilmektedir. Sağ taraf değeri referanslarının ifade içerisinde sol taraf değeri belirttiğini anımsayınız. Örneğin: int &&r = 10; int &k = r; // geçerli, r burada sol taraf değeri belirtiyor Burada r sağ taraf değeri referansıdır. Ancak bu r ifade içerisinde kullanıldığında artık sol taraf değeri belirtmektedir. Örneğin: void bar(int &r) { r = 100; } void foo(int &&r) { bar(r); } //... --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 104. Ders 09/10/2024 Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi bir sarma (wrapper) fonksiyonun başka bir fonksiyonu çağırdığını düşünelim. Amacımız sanki bu sarma fonksiyon hiç çağrılmamış doğrudan asıl fonksiyon çağrılmış gibi bir etki oluşturmak olsun. Örneğin: template void foo(T &&r) { //... } template void wrapper(T &&r) { //... foo(r); } Burada amacımız wrapper isimli fonksiyon nasıl çağrılmışsa foo fonksiyonun da aynı biçimde çağrılmasını sağlamak olsun. Örneğin: int a = 10; wrapper(a); Bu çağrıda wrapper fdonksiyonundaki T türü int & olarak açılacaktır. Dolayısıyla parametre değişkeni de referans daraltması nedeniyle int & türünden olacaktır. Yani a wrapper fonksiyonuyla adres yoluyla aktarılmaktadır. wrapper fonksiyonu foo fonksiyonunu bu r parametresiyle çağırmıştır. Burada r ifadesi sol taraf değeri belirtir. Dolayısıyla foo fonksiyonundaki T de int & türünden olacaktır. foo fonksiyonun r parametre değişkeni de int & türünden olacaktır. Böylece wrapper(a) çağırması ile foo(a) çağırması eşdeğer hale gelecektir. Ancak örneğin: wrapper(10); Burada wrapper isimli fonksiyonundaki T int olarak açılacak, r parametre değişkeni de int && türünden (int türden sağ taraf değeri referansı) olacaktır. İşbu durumda wrapper fonksiyonu foo fonksiyonunu çağırdığında r artık sol taraf değeri belirttiği için foo fonksiyonun T şablon parametresi int & türünden olacak ve foo fonksiyonun r parametresi bir sol taraf değeri referansı haline gelecektir. Bu durumda wrapper(10) çağırması ile foo(10) çağırması aynı anlama gelmeyecektir. Burada bir noktaya dikkatinizi çekmek istiyoruz. Biz wrapper fonksiyonunu wrapper(10) biçimind eçağırdığımızda foo fonksiyonunun şablon parametresi olan T & int olarak açılacaktır. Halbuki biz foo fonksiyonunu doğrudan foo(10) biçiminde çağırmış olsaydık bu durumda foo fonksiyonun şablon parametresi olan T de int türündne olacaktı. Yani bu wrapper fonksiyonu bu iletimi (forwarding) bozarak yapmaktadır. İşte referans parametreli bir sarma fonksiyon asıl fonksiyonu kendisi nasıl çağrılmışsa sanki öyle çağırıyormuş gibi çağırması gerekebilmektedir. Buna iletim (forwarding) denilmektedir. (İngilizce bazen "forwarding" yerine "prefect forwarding" terimi de kullanılmaktadır.) Sorunsuz iletim için forward isimli standart bir fonksiyon şablonu bulundurulmuştur. Fonksiyon başlık dosyaıs içerisindedir ve prototipi şöyledir: template constexpr T&& forward(remove_reference_t&& t) noexcept; Bu fonksiyonun bir parametresi vardır. Geri dönüş değeri ise T && türündendir. Biz fonksiyonu tipik olarak açısal parantezler içerisinde şablon parametresini belirterek çağırırız. Argüman olarak da çağırdığımız fonksiyondaki iletim referansını veririz. Örneğin: template void foo(T &&r) { //... } template void wrapper(T &&r) { //... foo(forward(r)); } Burada wrapper isimli fonksiyon foo fonksiyonunu şöyle çağırmıştır: foo(forward(r)); Artık biz wrapper isimli fonksiyonu nasıl çağırmışsak sanki foo fonksiyonunu öyle çağırmış gibi oluruz. Aşağıdaki örnekte şablon parametresini yazı olarak veren type_name isimli bir fonksiyon kullanılmıştır. Bu örnekle iletimin düzgün yapılıp yapılmadığını anlayabilirsiniz. Bu örneği forward fonksiyonunu çağırmadan da deneyiniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; #include #include template std::string type_name() { typedef typename std::remove_reference::type TR; std::unique_ptr own( nullptr, std::free); std::string r = own != nullptr ? own.get() : typeid(TR).name(); if (std::is_const::value) r += " const"; if (std::is_volatile::value) r += " volatile"; if (std::is_lvalue_reference::value) r += " &"; else if (std::is_rvalue_reference::value) r += " &&"; return r; } template void foo(T &&r) { cout << type_name() << endl; } template void wrapper(T &&r) { //... foo(forward(r)); } int main() { int a = 10; wrapper(a); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aslında forward fonksiyonunun tek yaptığı şey iletim referasındaki referansı silerek onu yeniden sağ taraf değeri türünden referansa dönüştürmektedir. Örneğin: template constexpr T &&forward(remove_reference_t&& arg) noexcept { return static_cast(arg); } Biz buarada eğer fonksiyon bi rsol taraf değeri referansı ile çağrılmışsa onu referans daraltması sonucunda sol taraf değeri haline getiriyoruz. Bu durumda fonksiyonun geri dönüş değeri sol taraf değeri referansı olmaktadır. Eğer fonksiyon sağ taraf değeri ile çağrılmışsa fonksiyonun geri dönüş değeri sağ taraf değeri olmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İletim (forwarding) işlemine standart kütüphanenin içerisinde çokça karşılaşılmaktadır. Örneğin nesne tutan sınıvların (vector, list gibi) emplace fonkssiyonları ancak iletim yoluyla yazılabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Sınıf şablonları normal bir sınıftan türetilebilir. Örneğin: class A { //... }; template class B : public A { //... }; Burada B'nin tüm farklı türden açılımlarının taban sınıfı A'dır. Yani örneğin B sınıfının da B sınıfının da taban sınıfı A'dır. Normal bir sınıf sınıf bir sınıf şablonundan türetilebilir. Tabii bu durumda taban sınıf belirtirken açılım türü belirtilmek zorundadır. Örneğin: template class A { //... }; class B : public A { //.. }; Burada B sınıfı A sınıfının int açılımından türetilmiştir. Bir sınıf şablonu başka bir sınıf şablonundan da türetilebilir. Örneğin: template class A { //... }; template class B : public A { //... }; Burada B hangi türle açılırsa A'nın o türden açılımından türetilmiş olur. Örneğin B türü A türünden, B türü A türünden türetilmiş durumdadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++11 ile birlikte "değişken sayıda şablon parametresi alan fonksiyon şablonları" oluşturulabilmektedir. Bu tür şablonlara İngilizce "variadic tamplates" denilmektedir. Değişken sayıda şablon parametresi alabilen fonksiyonlar C++11 ile birlikte gelen bazı özelliklerin gerçekleştirilebilmesi için dile eklenmiştir. Örneğin vector, list gibi nesne tutan sınıflardaki emplace fonksiyonları ancak bu özellik kulalnılarak yazılabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Değişken sayıda şablon parametresi şablon bildiriminde typename ya da class anahtar sözcüğünden sonra ... (ellipsis) ile belirtilmektedir. Örneğin: template void foo() { //... } Burada ... (ellisis) ile typename ve class anahtar sözcükleri bitişik yazılmak zorunda değildir. Ancak daha çok bitişik yazım tercih edilmektedir. Değişken sayıda şablon parametresine "şablon parametre paketi (template parameter pack)" de denilmektedir. Yukarıdaki fonksiyonda Args şablon parametre paketidir. Şablon parametre paketi tek bir şablon parametresi anlamına gelmemektedir. Sıfır ya da daha fazla şablon parametresi anlamına gelmektedir. Şablon parametre paketine sahip olan bir fonksiyon şablonu çağrılırken şablon parametre paketi için sıfır tane ya da daha fazla şablon parametresi belirtilebilir. Örneğin: foo(); // geçerli Burada fonksiyon şablonunun T tür parametresi int olarak, şablon parametre paketi ise long ve double olarak belirtilmiştir. Şablon parametre paketine karşı gelen şablon parametreleri sıfır tane de olabilir. Örneğin: foo(); // geçerli Burada şablon parametre paketinde hiçbir şablon parametresi yoktur. Şablon parametre paketi şablon parametre listesinin sonunda bulunmak zorundadır. Fonksiyonun yalnızca bir tane şablon parametre paketi olabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şablon parametre paketi ffonksiyonun parametresinde kullanılabilir. Bu durumda parametre paketinin yanına yine ... getirilir. Örneğin: template void foo(T a, Args... args) { //... } Burada fonksiyon değişken sayıda parametre almaktadır. Fonksiyon çağrılırken en az bir argüman girilmek zorundadır. Diğer argümanların hepsi args ile temsil edilmektedir. Buradaki fonksiyonun args parametresine "fonksiyon parametre paketi (function parameter pack)" denilmektedir. Örneğin biz bu fonksiyonu aşağıdaki gibi çağırabiliriz: foo(10, "ali", 3.14); Burada T şablon parametresi int olarak açılacaktır. Şablon parametre paketi olan Args char * ve double türlerini temsil etmektedir. Fonksiyonun parametresindek args ise bu şablon parametreleri türünden fonksiyon parametrelerini temsil etmektedir. Yani yukarıdaki çağrımda aslında derleyici fonksiyon şablonunun adeta aşağıdaki gibi olduğunu düşünmektedir: template void foo(T a, Arg1 arg1, Arg2 arg2) { //... } Yani biz adeta variadic şablon fonksiyonunu çağırdığımızda derleyici bizim için yukarıdaki şablon fonksiyonu yazıp açmaktadır. Açılan fonksiyon ise şöyle olacaktır: void foo(int a, const char *arg1, double arg2) { //... } Bir programın bir kod yazıyormuş gibi işlev görmesine "meta programlama (meta programming)" denilmektedir. Değişken sayıda şablon parametresi alan fonksiyonlar bir bakıma aynı zamanda "meta programlama" ile ilgili haldedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyonun parametre paketi fonksiyon içerisinde kullanılırken parametre paketinden sonra yine ... getirilmektedir. Örneğin: template void foo(T a, Args... args) { //... bar(args...); } Burada foo fonksiyonun parametre paketindeki tüm parametreler argüman olarak bar fonksiyonuna geçirilmiştir. Değişken sayıda şablon parametresi alan fonksiyonlar özellikle sarma şablon fonksiyonlarda karşımıza çıkabilmektedir. Örneğin: template void foo(Args... args) { string s(args...); cout << s << endl; } Bu örnekte foo fonksiyonu aldığı tüm parametreleri string sınıfının yapıcı fonksiyonuna aktarmaktadır. Örneğin: foo(10, 'a'); // aaaaaaaaaa foo("ankara"); // ankara Burada foo fonksiyonuna biz string sınıfının yapıcı fonksiyonunun arümanlarını geçtik. O da bu argümanları doğrudan nesneye iletti. Örneğin: vector v{"ali", "veli", "selami"}; v.emplace_back(10, 'a'); for (string &s : v) cout << s << " "; cout << endl; Burada emplace_back aslında bizden doğrudan string sınıfının yapıcı fonksiyonları için argümanları istemektedir. Bu argümanları hedefte yaratacağı string nesnenin yapıcı fonksiyonuna iletmektedir. push_back üye fonksiyonu ile emplace_back üye fonksiyonu arasındaki farklara dikkat edinmiz. Örneğimizde push_back fonksiyonu parametre olarak bir string istemektedir. Halbuki emplace_back fonksiyonu string'in yapıcı fonksiyonlarının argümanlarını istemektedir. Dolayısıyla puch_back iki kere nesnenin yaratılmasına yol açarken emplace_back yalnızca nesnenin hedefte yaratılmasına yol açmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Değişken sayıda şablon parametresi alan fonksiyonlar meta programlama faaliyetinde kullanılabilmektedir. Biz burada meta programalama üzerinde durmayacağız. Aşağıdaki örnekle meta programalama hakkında bir fikir sahibi olabilrsiniz: template void foo(T a) { cout << a << endl;; } template void foo(T a, Args... args) { cout << a << ", "; foo(args...); } //... foo("ali", 10, 20, "veli", 3.14); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; template void foo(T a) { cout << a << endl;; } template void foo(T a, Args... args) { cout << a << ", "; foo(args...); } int main() { foo("ali", 10, 20, "veli", 3.14); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Fonksiyon parametre paketinde tüm parametrelerin referans olamsı sağlanabilmektedir. Örneğin: template void foo(T a, Args &... args) { //... } Burada artık birinci parametre dışındaki tüm parametreler referans yoluyla aktarılacaktıtr. Tabii buradaki parametreler sol tarafı değeri referansıdır. Ancak istenirse bu parametreler sağ taraf değeri referansı da yapılabilir: template void foo(T a, Args &&... args) { //... } Buradaki parametre paketindeki parametrelerin hepsi aynı zamanda iletim referansı durumundadır. Buradaki referanslar const da yapılabilir: template void foo(T a, const Args &... args) { //... } Tabii const anahtar sözcüğü & atomunun hemen soluna da getirilebilir: template void foo(T a, Args const &... args) { //... } Fonksiyon parametre paketindeki iletim parametreleri forward fonksiyonu ile değer kategorisi kullanılarak da iletilebilir. Örneğin: template void add_emplace(vector &vec, Args &&... args) { vec.emplace_back(forward(args)...); } Burada add_emplace fonksiyonu bir sarma fonksiyondur. Bu sarma fonksiyonda parametreler vector sınıfının emplace_back fonksiyonuna forward fonksiyonu yoluyla değer kategorisi korunarak aktarılmıştır. Burada forward işlemi şöyle yapılmıştır: vec.emplace_back(forward(args)...); ... sentaksının args'nin yanında değil fonksiyonun kapanış parantezinden sonra bulundurulduğuna dikkat ediniz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Son yıllarda "lambda ifadeleri (lambda expressions)" pek çok programlama diline sokuldu. Aslında lambda ifadelerine benzer sentaktik yapılar "fonksiyonel (functional)" programlama modeline sahip dillerde zaten uzun süredir bulunuyordu. Ancak son yıllarda klasik programlama dilleri de fonksiyonel öğeleri bünyesine katmaya çalışmıştır. Dolayısıyla lambda ifadeleri bu bağlamda pek çok programlama diline eklenmiştir. Örneğin C#, Java, Python gibi dillerde lamda ifadeleri bulunmaktadır. İşte nihayet lambda ifadeleri C++11 ile birlikte C++'a da eklenmiş durumdadır. Bir fonksiyon parametre olarak başka bir fonksiyonu alabilmektedir. C++;'ın standart kütüphanesinde parametre olarak fonksiyon alan fonksiyon şablonlarıyla oldukça sık karşılaşılmaktadır. Örneğin for_each fonksiyonu şöyle yazılmıştır: template UnaryFunc for_each(InputIt first, InputIt last, UnaryFunc f) { while (first != last) { f(*first); ++first; } return f; } Bu fonksiyon başlık dosyası içerisinde bulunmaktadır. Bir dizilimin her elemanı ile bizim verdiğimiz fonksiyonu çağırmaktadır. Örnek kullanım şöyle olabilir: void foo(int val) { cout << val << endl; } //... vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; for_each(v.begin(), v.end(), foo); Bu tür call back fonksiyon kullanılması gerektiği durumlarda fonksiyonu önce yukarıda tanımlayıp onu kullanmak zahmetlidir. İşte lamda ifadeleri "bir fonksiyonu ifade içerisinde oluşturup, kullanmayı sağlayan" bir sentaktik öğedir. Yukarıdaki kodun lamda ifadeleri ile oluşturulan eşdeğeri şöyledir: vector v{1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; for_each(v.begin(), v.end(), [](int val) {cout << val << endl; }); Biz burada adeta bir ifade (expression) biçiminde hem bir fonksiyonu oluşturduk hem de onu kullandık. Lambda ifadelerinin bir statüsünde olmadığına bir ifade statüsünde olduğuna dikkat ediniz. Yani başka ifadelerin içerisinde kullanılabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 05. Ders 14/10.2024 Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta lambda ifadelerinin genel biçimi şöyledir: <[[capture_listesi]]> [(parametre_listesi)] [-> sembolü izleyebilir. Bu -> sembolü geri lambda ifadesinin geri dönüş değerinin türünü belirtmektedir. Ancak bu -> sembolü hiç bulundurulmayabilir. Bu durumda lamda ifadesinin geri dönüş değeri return anahtar sözcüğündeki ifadeden hareketle otomatik olarak tespit edilmektedir. Nihayet bu öğelerden sonra bir blok bulunmak zorundadır. Örneğin aşağıdaki lambda ifadelerinin hepsi geçerlidir: [] (int a) -> int { ... } [] () -> int { ... } [] -> int { ... } [] { .... } Tabii burada köşeli parantezler içerisinde izleyen paragraflarda açıklayacağımız gibi "yakalama listesi (capture list)" bulunabilir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir lambda ifadesi oluşturulduğunda bu ne anlama gelmektedir? Lambda ifadelerini gören derleyici aslında bir sınıf oluşturmaktadır. Sonra bu sınıf türünden bir nesne yaratıp o nesneyi bize vermektedir. Derleyicinin oluşturduğu bu sınıfın ismi belli değildir. Bu ismi elde etmenin de makul bir yolu yoktur. Örneğin: auto f = [] (int a, int b) -> int { return a + b;}; Burada derleyici lambda ifadesi için bir sınıf oluşturacak, sonra o sınıf türünden bir nesne yaratacaktır. Biz de o nesneyi f'ya atamış oluyoruz. Lambda ifadeleri için derleyici tarafından oluşturulan sınıfın ismini programcı bilmediği için o sınıf türünden nesneyi de tür ismi vererek oluşturamaz. Dolayısıyla mecburen bu nesne auto tür belirleyici ile belirtilen bir değişkene atanabilir. Yukarıdaki örnekte f bir sınıf türünden sınıf nesnesi belirtmektedir. Ancak biz bu sınıfın ismini bilmemekteyiz. Derleyici lambda ifadesi için oluşturduğu sınıfa "fonksiyon çağırma operatör fonksiyonu" yerleştirmektedir. Bu operatör fonksiyonun ana bloğu lambda ifadesinin ana bloğu, parametreleri lambda ifadesinin parametreleri ve geri dönüş değeri de lambda ifadesinin geri dönüş değeri olmaktadır. Yani biz lambda ifadesini atadığımız nesneyi fonksiyon gibi çağırırsak aslında lambda ifadesinde belirttiğimiz kodlar çalıştırılacaktır. Derleyicinin sınıf için oluşturduğu fonksiyon çağırma operatör fonksiyonu inline biçimdedir. Örneğin: result = f(10, 20); Burada aslında aşağıdaki işlem yapılmaktadır: result = f.operator()(10, 20); Lamda ifadeleri için derleyicini oluşturduğu nesne geçici bir nesnedir, dolayısıyla sağ taraf değeri kabul edilmektedir. C++17 ve sonrasında bir sınıf nesnesi kendi türünden bir sağ taraf değeri ile ilkdeğer verilerek tanımlandığında "copy elision" işleminin zorunlu olarak yapıldığını anımsayınız. Yani biz lamdbda ifadesini bir bir değişkene ilkdeğer olarak verdiğimizde aslında lambda nesnesi hedefte yaratılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { auto f = [](int a, int b) -> int { return a + b; }; int result; result = f(10, 20); cout << result; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Yukarıda de belirttiğimiz gibi eğer fonksiyonun parametresi yoksa normal parantezler içi boş olarak belirtilebilir ya da tamamen ihmal edilebilir. Eğer geri dönüş değerinin türü belirtimezse geri dönüş değerinin türü tıpkı daha önce görmüş olduğumuz fonksiyonun geri dönüş değerinin türü yerine auto tür belirleyicisinin yazılması durumund aolduğu gibi derleyici tarafından return ifadesi dikkate alınarak tespit edilmektedir. Örneğin: auto f = [] (int a, double b) { return a + b;}; Burada lambda ifadesinin geri dönüş değeri derelyici tarafından double olarak tespit edilecektir. Tabii lambda ifadesinde birden fazla return kullanılıyorsa tüm return ifadelerinin aynı türden olması gerekir. Hiç return kullanılmaması durumunda ise lambda ifadesinin geri dönüş değeri void olarak tespit edilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { auto f = [](int a, int b) { return a + b; }; int result; result = f(10, 20); cout << result; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Eğer lambda ifadesi parametreye sahip değilse normal parantezler tamamen ihmal edilebilmektedir. Örneğin: auto f = [] { cout << "test" << endl; } Burada lambda ifadesinin parametresi yoktur. Geri dönüş değeri de void biçimdedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include using namespace std; int main() { auto f = [] { cout << "this is a test" << endl; }; f(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Çok seyrek karşılaşılsa da parametresi olmayan lamda ifadelerinde geri dönüş değerini türü yine -> sembolü ile belirtilebilmektedir. Örneğin: auto f = [] -> double { return 3.14; } Tabii bu örnekte biz geri dönüş değerini türünü belirtmeseydik de return ifadesinden hareketle derleyici geri dönüş değerini zaten double olarak belirleyecekti. Aşağıdaki lambda ifadesi de geçerlidir: auto f = [] {}; Bu oluşturulabilecek minimal lambda ifadesidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Konunun girişinde de belirttiğimiz gibi lambda ifadeleri "yukarıda fonksiyonu tanımla aşağıda ifade içerisinde kullan" yerine "doğrudan fonksiyonu ifade içerisinde yazarak kullan" biçiminde bir sadelik oluşturmaktadır. Lambda ifadeleri "fonksiyonel (functional)" dillerde çok eskiden beri vardı. Son 20 yıldır aslında fonksiyonel olmayan programlama dillerine fonksiyonel dillerdeki bazı özelliklerin de eklenmesi gibi bir eğilim ortaya çıkmıştır. Lambda ifadeleri genel olarak fonksiyonel olmayan dillere kısmi fonksiyonel özellik katmaktadır. Tabii lambda ifadeleri denildiğinde akla "callback fonksiyonlar" gelmektedir. C++'ın standart kütüphanesinde (biz kütüphanenin bu kısmını ayrıntılı incelemedik) bizden fonksiyon alan ve işini yaparken onu çağıran epey fonksiyon şablonu bulunmaktadır. Örneğin for_each fonksiyonu bir dizilimin her elemanı için bizim verdiğimiz bir fonksiyonu çağırmaktadır. Tabii bu fonksiyon şablon bir fonksiyon olduğu için biz bu fonksiyona gerçek bir fonksiyon değil fonksiyon gibi davranan bir sınıf nesnesi (buna "functor" da denildiğini anımsayınız) verebilmekteyiz. İşte bu tür fonksiyonlara biz doğrudan lambda ifadelerini parametre olarak geçebiliriz. Örneğin: vector v{1, 5, 8, 4, 5, 2, 3, 6}; for_each(v.begin(), v.end(), [](int a) { cout << a * a << endl;}); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { vector v{10, 5, 8, 4, 5, 2, 3, 6}; sort(v.begin(), v.end()); for (auto &x : v) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- başlık dosyasındaki sort fonksiyonunu biz daha önce kullanmıştık. Bu fonksiyon dizilime ilişkin başlangıç ve bitiş iteratörlerini bizden alıp dizilimi sıraya diziyordu. Biz böyle bir sıraya dizme uyguladığımızda dizilim küçükten büyüğe sıraya dizilmektedir. Örneğin: vector v{10, 5, 8, 4, 5, 2, 3, 6}; sort(v.begin(), v.end()); İşte aslında sort fonksiyonun birkaç overload biçimi de vardır. sort fonksiyonunun üç parametreli overload biçiminde biz fonksiyonun üçüncü parameteresine bir fonksiyon verdiğimizde fonksiyon sort işlemini yaparken bizim verdiğimiz fonksiyonu dizinin iki elemanıyla çağırır. Biz bu fonksiyonu bool bir değere geri döndürürüz. Eğer biz sıraya dizmenin küçükten büyüğe yapılmasını istiyorsak fonksiyonumuzu soldaki değer sağdaki değerden küçükse true değerine, sıralamanın büyükten küçüğe yapılmasını istiyorsak false değerine geri döndürürüz. Tabii bu sayede biz sıraya dizmenin başka ölçütlere göre de yapılmasını sağlayabiliriz. Örneğin: vector v{"izmir", "ankara", "van", "kayseri", "eskisehir", "afyonkarahisar", "erzincan", "istanbul", "kars", "rize"}; sort(v.begin(), v.end(), [](const string &s1, const string &s2) {return s1.size() < s2.size(); }); for (auto &s : v) cout << s << " "; cout << endl; Burada biz vektörü alfabetik sıraya göre değil şehirlerin karakter uzunluklarına göre sıraya dizdik. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { vector v{"izmir", "ankara", "van", "kayseri", "eskisehir", "afyonkarahisar", "erzincan", "istanbul", "kars", "rize"}; sort(v.begin(), v.end(), [](const string &s1, const string &s2) {return s1.size() < s2.size(); }); for (auto &s : v) cout << s << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz daha önce başlık dosyasındaki find fonksiyon şablonunu kullanmıştık. İşte standart kütüphanede find fonksiyonun yanı sıra find_if isimli bir fonksiyon da vardır. Bu fonksiyon bizden aranacak değeri değil bir fonksiyonu bizden ister. Dizilimin her elemanı ile bu fonksiyonu çağırır. Biz de eleman bulunduğunda fonksiyonu true ile geri döndürürüz. Böylece aramayı sitediğimiz gibi yapabiliriz. Standaty kütüphanede bool değerigeri dönen callback fonksiyonlara "predicate" de denilmektedir. Eğer "predicate" fonksiyon tek parametreye sahipse buna "unary predicate", iki parametreye sahipse buna da "binary predicate" denilmektedir. Bu terminolojiye göre find_if bizden "unary predicate" istemektedir. Örneğin: vector v{"izmir", "ankara", "van", "kayseri", "eskisehir", "afyonkarahisar", "erzincan", "istanbul", "kars", "rize"}; auto iter = find_if(v.begin(), v.end(), [](const string &s) { return s.size() == 4; }); if (iter != v.end()) cout << "found: " << *iter << endl; else cout << "not found!.." << endl; Burada biz uzunluğu 4 olan ilk şehri ("kars)" bulmaktayız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { vector v{"izmir", "ankara", "van", "kayseri", "eskisehir", "afyonkarahisar", "erzincan", "istanbul", "kars", "rize"}; auto iter = find_if(v.begin(), v.end(), [](const string &s) { return s.size() == 4; }); if (iter != v.end()) cout << "found: " << *iter << endl; else cout << "not found!.." << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Standart kütüphanede başlık dosyasında bulunan accumalte fonksiyonu biz dizilimin toplam değerini bize vermektedir. Fonksiyonun ilk iki parametresi dizilimin başlangıç ve bitiş iteratörlerini süçüncü parametresi ise toplamın başlangıç değerini (genellikle 0 alınır) belritmektedir. Örneğin: int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int result; result = accumulate(a, a + 10, 0); cout << result << endl; // 55 Buradan 55 değeri elde edilecektir. (1'den 10'a kadar sayıların toplamı 55'tir.) İşte accumulate fonksiyonunun dört parametreli overload biçimi bizden bir fonksiyon da ister. Bu fonksiyonu birinci parametreesine kümülatif değeri, ikincisi parametresine dizilimin o andaki elemanın değerini geçirerek çağırır. İşlemi bizim yapmamızı ister. Yani fonksiyonu çağırdıktan sora elde ettiği geri dönüş değerini yeni toplam değer olarak kullanır. Böylece bu fonksiyona daha değişik işlemler yaptırabiliriz. Örneğin yukarıdaki dizini nelemanlarının karelerinin toplamı şöyle edilebilir: int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int result; result = accumulate(a, a + 10, 0, [](int total, int val) { return total + val * val; }); cout << result << endl; // 385 --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10}; int result; result = accumulate(a, a + 10, 0, [](int total, int val) { return total + val * val; }); cout << result << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Biz daha önce stanrt kütüphanedeki nesne tutan sınıfların (container classes) erase üye fonksiyonlarını görmüştük. Bu fonksiyonlar belli bir elemanı silmek için ya da iki iteratör arasındaki elemanları silmek için kullanılıyordu. Ancak bu erase üye fonksiyonları bazı karmaşık silmeleri yapamamaktadır. Örneğin biz bir vektör içerisindeki değeri 3 olan elemanları silmek isteyebiliriz. Ya da örneğin biz bir isim listesindeki 4 karakterli isimleri silmek isteyebiliriz. İşte bunun için C++'ta remove/erase kalıbı (remove/erase idiom) denilen bir kalıp kullanılmaktadır. remove başlık dosyasında bulunan global bir fonksiyon şablonudur. Bunun "unary predicate" alan remove_if isimli bir versiyonu da vardır. remove fonksyionları gerçek silme işlemini yapmaz. Yalnızca silinecek elemanları kaydırarak dizilimin sonunda toplar ve bize o topladığı yerin sondaki iteratörünü verir. Biz de onun verdiği iteratörden sona kadar gerçekten ilgili nesne tutan sınıfın erase fonksiyonu ile silme yaparız. remove fonksiyonu spesifik elemanları silmek için remove_if fonksiyonları ise ""unary predicate" alarak koşulu sağlayan elemanları silmek için kullanılmaktadır. Örneğin biz int değerlerdne oluşan bir bağlı listede 3 olan değerleri silmek isteyelim. Bu işlemi yapabiliriz: list a{3, 6, 2, 8, 9, 3, 12, 3, 45, 65, 75, 3}; auto iter = remove(a.begin(), a.end(), 3); a.erase(iter, a.end()); Burada biz önce 3 dışındaki değerleri dizilimin sonunda topladık sonra onları erase ile sildik. Programcılar bu iki işlemi tek hamlede de aşağıdaki gibi yapabilmektedir: list a{3, 6, 2, 8, 9, 3, 12, 3, 45, 65, 75, 3}; a.erase(remove(a.begin(), a.end(), 3), a.end()); Biz remove_if fonksiyonu ile istediğimiz koşulu sağlayan elemanalrı da silebiliriz. Örneğin yukarıdaki listeden çift sayıları aşağıdaki gibi silebiliriz: list a{3, 6, 2, 8, 9, 3, 12, 3, 45, 65, 75, 3}; a.erase(remove_if(a.begin(), a.end(), [](int val) {return val % 2 == 0; }), a.end()); Pekiyi neden erase böyle böyle çalışmıyor da remove ile erase fonksiyonlarını bir arada kullanmamız gerekiyor? İşte normal diziler söz konusu olduğunda silme diye bir kavram yoktur. Bu tasarımda silinmek üzere işaretleme ile silme işlemi ayrı biçimde yapılmıştır. Biz remove fonksiyonlarını normal dizilerde de kullanabiliriz. Silinmeyecekleri başta topladıktan sonra silinecek kısmı başka biçimlerde de silebiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { list a{3, 6, 2, 8, 9, 3, 12, 3, 45, 65, 75, 3}; a.erase(remove_if(a.begin(), a.end(), [](int val) {return val % 2 == 0; }), a.end()); for (auto x : a) cout << x << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 106. Ders 16/10/2024 Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- remove fonksiyonu silinecek elemanları dizilimin sonunda toplamamaktadır. Silinmeycek olanları dizilimin başında toplamaktadır. remove fonksiyonunu çağırdıktan sonra başlangıç iteratöründen remove fonksiyonunun bize verdiği iteratöre kadar ilerlediğimizde silinemmiş olan elemanları elde ederiz. Tabii bu iteratörden dizilimin sonuna kadarki elemanlar artık gerçekten silinebilirler. remove fonksiyonunun gerçekleştirimi aşağıdaki gibi yapılabilir: template ForwardIt myremove(ForwardIt beg, ForwardIt end, const T &val) { ForwardIt pos = beg; for (; beg != end; ++beg) if (val != *beg) { *pos = *beg; ++pos; } return pos; } remove_if fonksiyonu da şöyle gerçekleştirilebilir: template ForwardIt myremove_if(ForwardIt beg, ForwardIt end, UnaryPred up) { ForwardIt pos = beg; for (; beg != end; ++beg) if (!up(*beg)) { *pos = *beg; ++pos; } return pos; } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; template ForwardIt myremove_if(ForwardIt beg, ForwardIt end, UnaryPred up) { ForwardIt pos = beg; for (; beg != end; ++beg) if (!up(*beg)) { *pos = *beg; ++pos; } return pos; } int main() { list a{3, 6, 2, 8, 9, 7, 12, 21, 45, 65}; auto pos = myremove_if(a.begin(), a.end(), [](int val) {return val % 2 == 0; }); a.erase(pos, a.end()); for (auto iter = a.begin(); iter != a.end(); ++iter) cout << *iter << " "; cout << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Lambda ifadeleri pek çok programlama dilinde sanki bir iç fonksiyon (nested functions) gibi etki göstermektedir. Yani bir lambda ifadesinin gövdesi içerisinde biz istersek lamda ifadesinin içinde bulunduğu fonksiyonun yerel değişkenlerini kullanabiliriz. Buna C++ terminolojisinde (diğer bazı dillerde de ) "yakalama (capturing)" denilmektedir. Yakalanacak değişkenler (yani lambda ifadesinin içerisinde dış fonksiyondaki kullanmak istediğimiz yerel değişkenler) lambda sentaksında köşeli, parantezlerin içersinde belirtilmektedir. Yakalama sentaksının bazı ayrıntıları vardır. Biz burada madde madde bu ayrıntılar üzerinde duracağız. - Köşeli parantezlerin içerisinde içinde bulunulan fonksiyonların yerel ve parametre değişkenlerinin isimleri bir liste biçiminde belirtilirse yalnızca o değişkenler yakalanmaktadır. Örneğin: int a = 10, b = 20; int result; auto f = [a, b](int val) { return a + b + val; }; result = f(5); cout << result << endl; Burada lamda ifadesinin içerisinde biz lambda ifadesinin içinde bulunduğu fonksiyonun a ve b yerel değişkenlerini kullanabilmek için köşeli parantezler içerisinde onların isimlerini belirttik. Burada derleyici aslında lambda ifadesi için bir sınıf oluşturup yakalanacak değişkenleri bu sınıfın private veri elemanlarına yerleştirmektedir. Bu yerleştirme işlemi lambda fonksiyonu çağrılırken değil lambda ifadesinin bulunduğu noktada yapılmaktadır. Yani yukarıdaki işlemin işlevsel eşdeğeri aşağıdaki gibidir: int a = 10, b = 20; int result; class SomeName { public: SomeName(int a, int b) : m_a(a), m_b(b) {} inline int operator ()(int val) const { return m_a + m_b + val; } private: int m_a; int m_b; }; auto f = Sample(a, b); result = f(5); cout << result; Biz lambda ifadesinde içinde bulunulan fonksiyonun yerel değişkeninin kulalndığımızda aslında o değişkeni değil o değişkenin sınıfın veri elemanına atanan kopyasını kullanmaktayız. Yukarıdaki eşdeğerlikte fonksiyon çağırma operatör fonksiyonun const bir üye fonksiyon olduğuna dikkat ediniz. Gerçekten lambda ifadesi için derleyicinin oluşturduğu sınıfın fonksiyon çağırma operatör fonksiyonu const bir üye fonksiyondur. const üye fonksiyonların sınıfın veri elemanlarına erişemeyeceğine dikkat ediniz. Örneğin: int a = 10; auto f = [a] { a = 100; cout << a << endl; } // geçersiz! a'yı lambda ifadesi içerisinde değiştiremeyiz Burada lambda ifadesinde yakalanan a değişkenini kullandığımızda aslında biz sınıfın veri elemanını kullanıyor olmaktayız. Lambda ifadeleri için yazılan yazılan fonksiyon çağırma operatör fonksiyonu default durumda const üye fonksiyon olduğu için a = 100 işlemi geçersizdir. Eğer biz fonksiyon çağırma operatör fonksiyonun const üye fonksiyon olmasını istemiyorsak bu durumda lambda ifadesinde parametre parantezinden sonra mutable anahtar sözcüğünü kullanmalıyız. Örneğin: auto f = [a, b](int val) mutable -> int { ... }; Artık biz burada lambda ifadesi içerisinde a ve b'yi değiştebiliriz. Tabii a ve b'yi değiştirmemiz yerel değişkeni değiştirdiğimiz anlamına gelmemektedir. C++23'e kadar mutable anahtar sözcüğünü kullanabilmek için parametre parantezinin içi boş olsa bulundurulması gerekiyordu. C++23 ile birlikte bu zorunluluk da kaldırıldı. Örneğin: auto f = [a, b] mutable -> int { ... }; // C++23'e kadar geçersiz, C++23 ile geçerli - Eğer yakalann değişkenin kendisinin değil de adresinin oluşturulacak sınıfa aktarılması isteniyorsa bunun için değişkenin önüne & atomu getirilmelidir. Örneğin: auto f = [a, &b] (int val) -> int { //... }; Burada a değişkeni değerle b değişkeni adres yoluyla lambda ifadesine aktarılmaktadır. Köşeli parantez içerisindeki dekleratörde * atomu kullanılamaz. Yanızca & atomu kullanılabilir. Normal yakalama ile adres yoluyla yakalama köşeli parantez içerisinde herhangi bir sırada belirtilebilir. Örneğin: auto f = [&a, b, &c] (int val) -> int { //... }; Aşağıdaki gibi bir lambda ifadesi kullanılmış olsun: int a = 10, b = 20; int result; auto f = [a, &b]() { b = 30; return a + b; }; Artık lambda ifadesinde b'yi değiştirdiğimizde gerçekten yerel değişken olan b'yi değiştirmiş oluruz. Bunun eşdeğer sınıf karşılığı şöyledir: int a = 10, b = 20; int result; class SomeName { public: SomeName(int a, int &b) : m_b(b) {} inline int operator ()() const { m_b = 30; return m_a + m_b; } private: int m_a; int &m_b; }; auto f = Sample(a, b); Burada bir noktaya dikkatinizi çekmek istiyoruz. Biz bu örnekte lambda ifadesini mutable yapmadık. Ancak yine de m_b'ye atama yapabildik. Bu durum gayet normaldir. const üye fonksiyonların içerisinde sınıfın veri elemanları const gibidir, referansların gösterdiği yerler const değildir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { int a = 10, b = 20; int result; auto f = [a, &b]() { b = 30; return a + b; }; result = f(); cout << result << endl; // 40 cout << a << ", " << b << endl; // 10, 20 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- - Eğer istenirse lambda ifadesinin içinde bulunduğu fonksiyonun tüm erişilebilir yerel ve parametre değişkenleri yakalanabilir. Bunun için köşeli parantezler içerisine = ya da & atomu yerleştirilir. = atomu tüm değişkenlerin değerle aktarılacağını, & atomu ise tüm değişkenlerin adres yoluyla aktarılacağını belirtmektedir. Tabii kçşeli parantez içerisine biz hem = hem de & yerleştiremeyiz. Örneğin: int a = 10, b = 20; auto f = [=](int val) { return a + b + val; }; Burada artık biz lambda ifadesi içerisinde a, b ve yerel değişkenlerini kullanabiliriz. Tabii default durumda lambda fonksiyonu const olduğu için yine lambda ifadesinde bu değişkenleri değiştiremeyiz. Ancak mutable belitleyicisi ile bunların değiştirilebilmesini sağlayabiliriz. Örneğin: auto f = [=](int val) mutable { //.... }; Köşeli parantezler içerisinde & atomu lambda ifadesinin içinde bulunduğu tüm erişebilir yerel değişkenlerin ve parametre değişkenlerinin adres yoluyla aktarılacağı anlamına gelmektedir. Örneğin: int a = 10; int b = 20; auto f = [&](int val) { a = 100; // yerel değişken olan a değiştiriliyor b = 200; // yerel değişken olan b değiştiriliyor //... }; Biz istersek tüm değişkenleri = ve & operatörüyle yakalarken onlardan bazılarını zıt biçimde de yakalayabiliriz. Örneğin: auto f = [=, &x, &y] { //... }; Burada x ve y'nin dışındaki tüm yerel ve parametre değişkenleri değer yoluyla ancak x ve y adres yoluyla yakalanmıştır. Aşağıdaki gibi bir yakalama geçerli değildir: auto f = [=, &x, y] { //... }; Burada zaten tüm yeral ve parametre değişkenleri değer yoluyla yakalanmıştır. Bizim ayrıca y'yi "değer yoluyla yakala" dememize gerek yoktur. Zaten bu durum geçersizdir. Örneğin: auto f = [&, x, y] { //... }; Bu ifade geçerlidir. Burada değişkenler adres yoluyla yakalanmıştır. Abncak x ve değer yoluyla yakalmıştır. Bnezer gerekçeyle aşağıdaki ifade de geçersizdir: auto f = [&, &x, y] { //... }; Burada x'in adres yoluyla yakalanması default durumdur. Böyle bir belirleme geçersizdir. Eğer köşeli parantez içerisinde & ya da = atomu ile başka değişkenler yerleştirilecekse önce & ve = atomunun sonra diğer değişkenlerin yerleştirilmesi gerekir. Aşağıdaki ifade geçersizdir: auto f = [x, y, &] { // geçersiz! //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- - Köşeli parantez içerisindeki yakalama listesinde global değişkenler ve static yerel değişkenler belirtilemez. Global değişkenler ve static terel değişkenler zaten her zaman erişilebilir durumdadır. Örneğin aşağıdaki yakalama listesi geçersizdir: int g_a; //... auto f = [g_a](int val) // geçersiz' { // ... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- - Bir sınıfın üye fonksiyonu içerisinde bir lambda ifadesi oluşturulduğunda bu lambda ifadesinde sınıfınm veri elemanlarını kullanamayız. Örneğin: class Sample { public: Sample(int a, int b) : m_a(a), m_b(b) {} void foo(); int m_a; int m_b; }; void Sample::foo() { auto f = [] { return m_a + m_b; }; // geçersiz! cout << f() << endl; } Burada lambda ifadesi içerisinde sınıfın veri elemanları kullanılamamktadır. Çünkü lambda ifadeleri aslında sınıf belirtmektedir. Bu sınıfa sınıfın veri elemanları aktarılmadıktan sonra kullanım mümkün değildir. İşte eğer köşeli parantezler içerisine this anajtar sözcüğü yerleştirilirse bu durumda lambda ifadesinde sınıfın veri elemanları kullanılabilir. Örneğin: void Sample::foo() { auto f = [this] { return m_a + m_b; }; // geçerli cout << f() << endl; } Buradaki this anahtar sözcüğü aktarımın adres yoluyla yapıldığı anlamına gelmektedir. Örneğin: void Sample::foo() { auto f = [this] { m_a = 100; m_b = 200; }; f(); } Burada lambda ifadesinde sınıf veri elemanları değiştirilmektedir. Tabii yine referanslar için söz konusu olan durum burada da söz konusudur. Yani lambda ifadesi const olsa bile bu değişikilk yapılabilmektedir. C++17'ye kadar sınıfın veri elemanları değerle aktarılamıyordu. C++17 ile birlikte köşeli parantez içerisine *this ifadesinin de yerleştirilmesi mümkün hale getirilmiştir. Örneğin: void Sample::foo() { auto f = [*this] () mutable { m_a = 100; m_b = 200; }; f(); } Biz burada m_a ve m_b değiştirdiğimizde artık sınıfın veri elemanlarını değiştirmiş olmuyoruz. Çünkü aktarım değer yoluyla yapılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++14 ile birlikte lambda ifadelerindeki parametre değişkenlerine default değer de verilebilir hale geldi. Burada sentaks ve semantik olarak tamamen normal fonksiyonlardaki default argüman kuralları uygulanmaktadır. Örneğin: auto f = [](int a = 10, int b = 20) { return a + b; }; cout << f(100, 200) << endl; cout << f(100) << endl; cout << f() << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Lambda ifadeleri noexcept exceptipn belirleyicisine de sahip olabilir. Örneğin: auto f = [](int a, int b) noexcept { return a + b; }; Burada f fonksiyonu çağrıldığında bir exceptionm oluşmayacağı garanti edilmiştir. mutable anahtar sözcüğü ile noexcept birlikte kullanılacaksa önce mutable sonra noexcept anahtar sözcükleri sentaksta belirtilmelidir. Örneğin: auto f = [=](int a, int b) mutable noexcept { //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir lambda ifadesi ifade tanımına uyfuğuna göre yazılır yazılmaz aynı zamanda çağrıla da bilir. Örneğin: [] { cout << "this is a test" << endl; }(); Böyle bir çağrımın ne amacı olabilir? Zira zaten lambda ifadesinin içerisindeki kod doğrudan dışarıda kod olarak da yazılabilir. Böylesi bir işlemin belki de tek makul gerekçesi const nesnelere karmaşık ilkdeğerlerin verilmesinin kolaylaştırılmasıdır. Örneğin: const int a = [] { //... return ifade; }(); const bir nesneye ilkdeğer vermenin zorunlu olduğunu anımsayınız. İşte verilecek ilkdeğer bir dizi işlemi gerektiriyorsa bu işlemlerin tek bir ifade olarak ele alınması için mecburen bir fonksiyonun kullanılması gerekir. Bu tür durumlarda fonksiyonu yukarıda tanımlayıp const nesneye ilkdeğer verirken çağırmak yerine lambda ifadesi oluştrup doğrudan onu çağırmak daha az zahmetlidir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 107. Ders 21/10/2024 Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++14 ile birlikte lambda ifadelerinin "generic" biçimde oluşturulması da sağlanmıştır. Lambda ifadeleri şablon olamaz ancak C++14 ile birlikte parametre parantezinin içerisinde parametre değişkeninin türü olarak auto anahtar sözcüğü kullanılabilmektedir. Bu da bir çeşit şablon etkisi yaratmaktadır. Örneğin: int x = 10, y = 10; auto f = [x, y](auto a) { //... return 0; }; Biz burada f lambda ifadesini hangi türden parametre ile çağırırsak onun parametresi o türden olacaktır. Tabii aslında derleyicinin bu lambda ifadesi için yazdığı sınıftaki fonksiyon çağırma operatör fonksiyonu şablon bir fonksiyondur. Yukarıdaki lambda ifadesinin eşdeğerinin aşağıdaki gibi olduğunu varsayabilirsiniz: class CompilerGeneratedName { public: CompilerGeneratedName(int &x, int &y) : m_x(x), m_y(y) {} template inline int operator()(T val) const { //... return 0; } private: int m_x; int m_y; }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bu bölümde C++'ın stamdart IO kütüphanesinin temel kullanımları üzerinde duracağız. C++'ın IO kütüphanesi sınıfsal bir biçimde (yani nesne yönelimli teknikle) tasarlanmıştır. Tasarım biraz karmaşık düzeydedir ve pek çok ayrıntı içermektedir. C'nin IO fonksiyonlarına göre C++'ın IO kütüphanesinin kullanımı daha zordur. Kütüphanein tasarımında bazı problemlerin olduğunu düşünüyoruz. C++'ın IO kütüphanesi diğer kütüphane bileşenlerinden olduğu gibi şablon tabanlıdır. Ancak kütüphanedeki sınıfların çeşitli türlerden açılımları çeşitli isimlerle typedef edilmiştir. Programcılar da genellikle kütüphaneyi şablon biçiminde değil bu typedef isimleri ile kullanmaktadır. Kütüphanenin temel türetme şeması şöyledir: ios_base (şablon sınıf değil) basic_ios basic_istream basic_ostream basic_iostream ios_base sınıfı şablon bir sınıf değildir. Diğer sınıflar şablon olarak bildirilmiştir. Burada sınıf şablonlarının iki şablon paraöetresi vardır. Birinci parametre "karakter kavramının hangi türle temsil edileceğini" belirtmektedir. Örneğin bir karakter bir byte'lık char türü ile temsil edilebilir ya da örneğin wchar_t ütü ile temsil edilebilir. Tabii en sık kullanılan temsil char türü ile temsildir. İkinci parametre "trait" denilen ve kullanılan karakter kümesi üzerinde temel işlemlerin nasıl yapılacağı üzerinde etkili olan parametredir. Bu parametre default değer almıştır. Dolayısıyla programcı bu parametreyi girmek zorunda değildir. Örneğin basci_ostream sınıfının şablon bildirimi şöyledir: template> class basic_ostream : virtual public std::basic_ios { //... }; Kütüphanedeki sınıf şablon sınıflarının char ve wchar_t türünden açılımları typedef edilmiştir. Bunların char türden açılımlarının tür isimleri başındaki "basic_" öneki kaldırılarak oluşturulmuştur. Yani bu sınıfların char türden açılımlarının typedef isimleri şöyledir: ios_base (şablon sınıf değil) ios istream ostream iostream Yani basic_ios ile ios, basic_istream ile istreami basic_ostream ile ostream, basic_iostream ile de iostream tür isimleri eşdeğerdir: typedef basic_ios ios; typedef basic_istream istream; typedef basic_ostream ostream; typedef basic_iostream iostream; Temel IO sınıf şablonlarının wchar_t türünden açılımları da char açılımlarının başına 'w' eklenerek isimlendirilmiştir: typedef basic_ios wios; typedef basic_istream wistream; typedef basic_ostream wostream; typedef basic_iostream wiostream; Uygulamada en sık olarak bu sınıfların char açılımları kullanılmaktadır. Buradaki istream, ostream ve iostream sınıfları dosyalar üzerinde okuma, yazma işlemlerini yapan genel sınıflardır. Biz daha önceden de cout nesnesinin ostream sınıfı türünden cin nesnesinin de istream sınıfı türünden olduğunu belirtmiştik. Bu nesnelerin wchar_t açılımli biçimleri de wcout wcin biçimindeydi. Biz bu nesneler yoluyla stdin ve stdout dosyaları ile (yani ekran ve klavye ile) işlemler yapabiliyorduk. Pekiyi dosya işlemleri nasıl yapılmaktadır? İşte dosya işlemleri için aşağıdaki sınıflar kullanılmaktadır: basic_ifstream basic_ofstream basic_fstream Bu sınıfların char ve wchar_t türünden açılımları da aşağıdaki gibi isimlendirilimitir: typedef basic_ifstream ifstream; typedef basic_oftream ofstream; typedef basic_ftream fstream; typedef basic_ifstream wifstream; typedef basic_oftream wofstream; typedef basic_ftream wfstream; basic_ifstream sınıfı basic_istream sııfından, basic_ofstream sınıfı basic_ostream sınıfından ve basic_fstream sınıfı ise basic_iostream sınıfından türetilmiş durumdadır: ios_base (şablon sınıf değil) basic_ios basic_istream basic_ostream basic_ifstream basic_iostream basic_ofstream basic_fstream basic_istream sınıfı (burada 'i' harfi "input" sözcüğünden geliyor) dosyalar üzerinde okuma yapmak için gereken üye fonksiyonları, basic_ostream sınıfı ise (buradaki 'o' harfi "output" sözcüğünden geliyor) dosyalar üzerinde yazma yapmak için gereken üye fonksiyonlardan oluşmaktadır. Dolayısıyla basic_ifstream sınıfı basic_istream sınıfından basic_ofstream sınıfı ise basic_ostream sınıfından türetilmiştir. C++'ın yukarıda açıkladığımız sınıflarına "stream sınıfları da" denilmektedir. Bu sınıf sisteminin çalışmasının anlaşılabilmesi için türetme şemasının tepesindeki ios_base ve basic_ios sınıflarının kabaca gözden geçirilmesi gerekir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- ios_base sınıfının şablon bir sınıf olmadığını belirtmiştik. Bu sınıf içerisinde her türlü açılım için anlamlı olabilecek en genel elemanlar bulundurulmuştur. Bu elemanların büyük çoğunluğu constexpr static sembolik sabitlerden oluşmaktadır. Sınıfta iostate türünden aşağıda belirtilen dört sembolik sabit bulunmaktadır: typedef /*implementation defined*/ iostate; static constexpr iostate goodbit = 0; static constexpr iostate badbit = /* implementation defined */ static constexpr iostate failbit = /* implementation defined */ static constexpr iostate eofbit = /* implementation defined */ Burada da görüldüğü gibi yalnızca goodbit değerinin 0 olduğu standartlarda belirtilmiştir. Diğer sembolik sabitler derleyicileri yazanlar tarafından farklı değerlerde oluşturulmuş olabilir. Pekiyi buradaki sembolik sabitler ne anlama gelmektedir? Bir stream nesnesi üzerinde bir işlem yapıldığında stream nesnesinin içerisinde tutulan iostate türünden bir veri elemanı bu sembolik sabitlerle set edilmektedir. Bu sembolik sbitler goodbit dışında tek biti 1 olan diğer bitleri 0 olan sayılar biçimindedir. goodbit son yapılan işlemin başarıyla gerçekleştirildiğini belirtmektedir. badbit ciddi IO hatalarını belirtmek için kullanılmaktadır. Bu tr hatalarda stream nesnesinin bütünlüğü bozulmuş olabilir. Tabii işlem sonucunda iostate nesnesinin badbit olarak set edilmesi çok seyrek karşılaşılabilecek bir durumdur. failbit son yapılan işlemin başarısız olduğunu belirtir. Bu başarısızlık genellikle olağan sebeplerden ya da yanlış parametrelerin geçilmesinden kaynaklanmıştır. Örneğin bir dosyanın sonundan okuma yapılmak istenirse bu patolojik bir durum değildir. Burada badbit set edilmez, ancak failbit set edilir. eofbit ise son yapılan işlemin EOF nedeniyle başarız olduğunu belirtmektedir. Genellikle eofbit failbit ile birlikte set edilir. Yani örneğin biz dosyanın sonundan okuma yapmak istersek hep bu işlem başarısız olur ve failbit set edilir ve aynı zamanda başarısızlık EOF dolayısıyla oluştuğu için eofbit set edilir. Pewkiyi biz stream nesnesi ile bir işlem yaptıktan sonra yukarıdaki bayrakların hangilerinin set edilip edilmediğini nasıl anlayabiliriz? İşte ios_base sınıfı yalnızca bu bayrakların değerlerini tutmaktadır ancak bu bayrakların değerlerini turan bir iostate nesnesi bu sınıfın veri elemanlarında yoktur. Bayrakların durumunu tutan iostate nesnesi basic_ios sınıfının private bölümünde bulundurulmuştur. Dolayısıyla bu bayrakların değerini veren üye fonksiyonlar da basic_ios sınıfında bulunmaktadır. Bu fonksiyonların hepsi bool bir değere geri dönmektedir. Bu fonksiyonlar şöyledir: good fonksiyonu: Eğer son işlem başarılıysa yani goodbir set edilmişse bu fonksiyon true değerine aksi takdirde false değerine geri dönmektedir. (goodbit değerinin 0 olduğunu anımsayınız. Yani aslında iostate veri elemanındaki tüm bitler 0 ise bu fonksiyon true değerine geri dönmektedir.) fail fonksiyonu: Bu fonksiyon failbit ya da badbit set edilmişse true değerine aksi takdirde false değerine geri dönmektedir. Fonksiyonun yalnızca failbit durumuna değil aynı zamanda badbit durumuna da baktığına dikkat ediniz. bad fonksiyonu: Eğer badbit set edilmişse true değerine set edilmemişse false değerine geri döner. eof fonksiyonu: Bu fonksiyon eğer eofbit set edilmişse true değerine, set edilmemişse false değerine geri dönmektedir. Programcılar genellikle fail fonksiyonunu kullanmaktadır. Yani bir işlemi yaptıktan sonra badbit ya da failbit bayraklarının set edilip edilmediğine bakmaktadır. Örneğin: int a; cin >> a; if (cin.fail()) { // başarısız olduysa } Burada programcı okumanın başarısız olduğunu test etmektedir. Tabii aynı işlem tersten şöyle de yapılabilirdi: cin >> a; if (cin.good()) { // başarılı olduysa } Stream sınıflarının basic_ios sınıfından gelen ! operatör fonksiyonu tamamen fail fonksiyonun geri dönüş değeri ile geri dönmektedir. Örneğin: int a; cin >> a; Burada cin.fail() ifadesi ile !cin ifadesi tamamen eşdeğerdir. Örneğin: if (!cin) { // başarısız olduysa } Nesnenin içerisindeki iostate türünden veri elemanın değerini (yani tüm bayrak durumlarının değerlerini) elde etmek için rdsrate isimli bir üye fonksiyon da bulundurulmuştur: iostate rdstate() const; Böylece programcı isterse bayrakları manuel olarak da kontrol edebilir. Örneğin: int a; cin >> a; if (cin.rdstate() & ios::failbit) { cerr << "cannot read..." << endl; exit(EXIT_FAILURE); } Burada failbit'in ios::failbit biçiminde belirtildiğine dikkat ediniz. Halbuki bu sembolik sabit ios_base sınıfındadır. Ancak türemiş sınıf ismiyle taban sınıfın static elemanlarına da erişilebildiğini anımsayınız. Programcılar daha az tuşa basmak için ios_base::failbit yazmak yerine ios::failbit yazmayı tercih edebilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_istream (istream) ve basic_ostream (ostream) sınıflarının faydalı birtakım üye fonksiyonları vardır. Biz bu üye fonksiyonları dosya nesneleriyle ya da cin ve cout nesneleriyle kullanabiliriz. basic_istream sınıfının get isimli üye fonksiyonları dosyadan temel okumaları yapmaktadır: int_type get(); basic_istream& get(char_type& ch); basic_istream& get(char_type* s, std::streamsize count); basic_istream& get(char_type* s, std::streamsize count, char_type delim); Parametresiz get fonksiyonu dosyadan bir karakter okumak için kullanılmaktadır. char_type & parametreli get fonksiyonu okuduğu karakteri adresiyle aldığı nesnye yerleştirir. Diğer iki fonksiyon birden fazla karakteri okumaktadır. Fakat üçüncü fonksiyon '\n' karaketerini görünce, dördüncü fonksiyon da spesifik bir karakteri görünce işlemini kesmektedir. Örneğin: char ch; ch = cin.get(); cout << ch << endl; Örneğin: char s[10]; cin.get(s, 10); cout << s << endl; üçüncü ve dördüncü fonksiyonlar '\n' ya da belirlenen karakteri gördüğünde onu tampona geri bakmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_istream sınıfının getline üye fonksiyonu bir satır okumak için kullanılmaktadır: basic_istream& getline(char_type *s, std::streamsize count); Fonksiyon '\n' karakterini de okur ancak bunun yerine null karakter yerleştirir. Yukarıdaki üçüncü get fonksiyonunun benzer işlemi yaptığına ancak '\n' karakterini okumadığını (yani onu tampondan almadığına) dikkat ediniz. Bu fonksiyon adeta C11 ile C'ye isteğe bağlı olarak eklenen gets_s fonksiyonuna benzemektedir. Örneğin: char s[10]; cin.getline(s, 10); cout << s << endl; basic_istream sınıfının read üye fonksiyonu n karakter okuma yapmaktadır. Bu fonksiyon özel bir karakter gördüğünde durmaz. Prototipi şöyledir: basic_istream& read(char_type *s, std::streamsize count); Fonksiyon talep edilenden daha az karakteri okuyabilir. Bu durumda eofbit set edilmektedir. Başarılı okunan karakter sayısı sınıfın gcount üye fonksiyonu ile elde edilmektedir: std::streamsize gcount() const; Örneğin: char s[100 + 1]; cin.read(s, 100); s[cin.gcount()] = '\0'; cout << s << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_ostream sınıfının da çeşitli üye fonksiyonları vardır. put fonksiyonu get fonksiyonunun tersini yapmaktadır. Yani dosyaya bir karakter (byte) yazmak için kullanılmaktadır: basic_ostream& put(char_type ch); Örneğin: cout.put('x'); write fonksiyonu da read fonksiyonun yazma yapan biçimi gibidir: basic_ostream& write(const char_type *s, std::streamsize count); Fonksiyon parametresiyle ikinci parametresiyle belirtilen miktarda karakteri dosyaya yazdırmaktadır. Örneğin: cout.write("ankara", 6); basic_ostream sınıfının flush fonksiyonu tampondaki bilgilerin hedefe aktarılması için kullanılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++23 ile birlikte başlık dosyasında yer tutucucuyla çalışan print println fonksiyonları da eklenmiştir. Bu fonksiyonlar değişken sayıda parametre alan şablonlar (variadic templates) kullanılarak yazılmıştır. Yer tutucular "{}" ibaresiyle belirtilmektedir. Bu yer tutucular C# ve Python dillerinde eskiden beri bulunuyordu. Örneğin: int a = 10, b = 20; print(cout, "a = {}, b = {}", a, b); // a = 10, b = 20 Kullanım adeta printf fonksiyonuna benzemektedir. Ancak tür belirtten bir format karakterinin olmadığına dikkat ediniz. Variadic template kullanıldığı için zaten bu türe uygun bir fonksiyon yazılmaktadır. Aslında küme parantezleri içerisinde argüman indeksi de belirtilebilmektedir. İlk argümanın indeksi 0'dır. Örneğin: int a = 10, b = 20; double c = 3.14; print(cout, "{2}, {1}, {0}", a, b, c); // 3.14, 20, 10 Küme parantezlerinin içerisinde formatlama yazıları da bulundurulabilir. Ancak indeks belirtilsin ya da belirtilmesin formatlama bilgisinden önce ':' karakteri olmalıdır. Formatlama yazısı printf fonksiyonun formatlama biçimine benzemektedir. Ancak Python dilindeki bazı formatlama özellikleri kullanılmıştır. Örneğin: int a = 10, b = 20; double c = 3.14; print(cout, "{:<10d}{:<5d}{:.10f}", a, b, c); // 10 20 3.1400000000 Ayrıca başlık dosyası içerisinde print fonksiyonun default stdout dosyasına yazan versiyonları da vardır. Yani biz eğer bir şeyi ekrana yazdıracasak bu başlık dosyasını include ederek ilk parametreyi cout geçmeyebiliriz. Formatlama sentaksı için aşağıdaki bağlantıdan faydalanabilirsiniz: https://en.cppreference.com/w/cpp/utility/format/spec --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi biz örneğin cout ile bir değeri hex olarak nasıl yazdırabiliriz? İşte stream nesnelerinin içerisinde ios_base sınıfından gelen "formatlama bayrakları (formatting flags)" bayraklarını tutan bir veri elemanı vardır. Bu yazma ve okuma eylemleri bu formatalama bayraklarının durumuna göre değişiklik göstermektedir. formatlama bayraklarıyla ilgili ios_base sınıfındaki üye fonksiyonlar şuınlardır: fmtflags flags() const; fmtflags flags(fmtflags flags); fmtflags setf(fmtflags flags); void unsetf(fmtflags flags) flags fonksiyonu formatlama bayraklarının değerini bir bütün olarak alıp set etmektedir. setf ise birtakım bayrakları zaten var olan bayraklara eklemektedir. unsetf fonksiyonu ise birtakım bayrakları var olan bayraklardan çıkartmaktadır. Bayrak değerleri ios_base sınıfının içerisinde constexpr biçimde sembolik sabit olarak bildirilmiştir: static constexpr fmtflags dec = /*implementation defined*/ static constexpr fmtflags oct = /*implementation defined*/ static constexpr fmtflags hex = /*implementation defined*/ static constexpr fmtflags basefield = dec | oct | hex; static constexpr fmtflags left = /*implementation defined*/ static constexpr fmtflags right = /*implementation defined*/ static constexpr fmtflags internal = /*implementation defined*/ static constexpr fmtflags adjustfield = left | right | internal; static constexpr fmtflags scientific = /*implementation defined*/ static constexpr fmtflags fixed = /*implementation defined*/ static constexpr fmtflags floatfield = scientific | fixed; static constexpr fmtflags boolalpha = /*implementation defined*/ static constexpr fmtflags showbase = /*implementation defined*/ static constexpr fmtflags showpoint = /*implementation defined*/ static constexpr fmtflags showpos = /*implementation defined*/ static constexpr fmtflags skipws = /*implementation defined*/ static constexpr fmtflags unitbuf = /*implementation defined*/ static constexpr fmtflags uppercase = /*implementation defined*/ Bu formatlama bayrakları belli koşullarda etkili olmaktadır. Örneğin scientific bayrağı set edilmiş olsun. Biz bir tamsayı yazdırırken bu bayrağın bir etkisi olmayacaktır. Bu bayrak ancak gerçek sayı türlerini yazdırırken etkili olacaktır. dec, oct ve hex bayrakları tamsayı türlerinin yazıdırılması sırasında yazdırmanın kaçlık sisteme göre yapılacağını belirtmektedir. Default durumda dec bayrağı set edilmiş durumdadır. Yani tamsayı türleri 10'luk sisteme göre yazdırılır. Örneğin: int a = 100, b = 200; cout << a << ", " << b << endl; // 100, 200 cout.unsetf(ios::dec); cout.setf(ios::hex); cout << a << ", " << b << endl; // 64, c8 cout.setf(ios::uppercase); cout << a << ", " << b << endl; // 64, C8 cout.unsetf(ios::hex); cout.setf(ios::dec); cout.setf(ios::showpos); cout << a << ", " << b << endl; // +100, +200 Görüldüğü gibi formatlama bayrakları ile yazdırma yapmak biraz zahmetlidir. İşte bunun için manipülatörler oluşturulmuştur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Manipülatörler parametreli ya da parametresiz biçimde bulunabilmektedir. Parametresiz manipülatörler aslında fonksiyon isimleridir. Dolayısıyla overload resolution sırasında devreye girip uygun formalama bayraklarını set etmektedir. Örneğin: int a = 100, b = 200; cout << a << ", " << b << endl; // 100, 200 cout << hex << uppercase << a << ", " << b << endl; // 64, C8 Pekiyi bu manipülatörler nasıl çalışmaktadır? Buradaki parametresiz manipülatörler yukarıda da belirttiğimiz gibi birer fonksiyo nismidir. Bu fonksiyonları parametreleri ve geri dönüş değerleri basic_ostream & türündendir. ostrean sınıfının böyle bir fonksiyon gösterici parametresine sahip << operatör fonksiyonu vardır. Bu fonksiyon da bu manipülatör fonksiyonunu çağırmaktadır. Manipülatör fonksiyonları da formatlama bayraklarını set etmeketdir. Bu işlemin nasıl yapıldığına yönelik fikir oluşturması için aşağıda küçük bir örnek veriyoruz. Bu örnek yalnızca mekanizamanın nasıl çalıştığının anlaşılmaıs için verilmiştir. Gerçek ostream sınıf sistemiyle örneğin bir ilgisi yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; class myostream { public: myostream() : m_frmtflags(dec) {} myostream &operator <<(int val) { if (m_frmtflags & dec) printf("%d", val); else if (m_frmtflags & hex) printf("%x", val); //... return *this; } myostream &operator <<(const char *s) { printf("%s", s); return *this; } myostream &operator <<(myostream &(*pf)(myostream &)) { return pf(*this); } //... static constexpr unsigned dec = 0x1; static constexpr unsigned hex = 0x2; //... void setf(unsigned flag) { m_frmtflags |= flag; } void unsetf(unsigned flag) { m_frmtflags &= ~flag; } private: unsigned m_frmtflags; }; myostream &hex(myostream &r) { r.unsetf(myostream::dec); r.setf(myostream::hex); return r; } myostream mycout; int main() { int a = 100; mycout << hex << a << "\n"; // mycout.operator <<(hex).operator << (a).operator << ("\n"); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Parametreli manipülatörler bir sınıf biçiminde yazılmışlardır. setw manipülatörü int türden bir genişlik parametresi almaktadır ve yazdırılacak değerleri burada belirtilen alan içerisinde yazıdırır. Örneğin: int a = 10, b = 20; for (int i = 0; i < 100; ++i) cout << left << setw(20) << i << i * i << endl; Burada 0'dan 100'e kadar sayıların kareleri bıçakla kesilmiş gibi yazdırılmaktadır. dosyası içerisinde başka parametreli manipülatörler de vardır. Örneğin: double d = 3.141592653589793238462643; cout << d << endl; // 3.14159 cout << setprecision(10) << d << endl; // 3.141592654 cout << d << endl; // 3.141592654 --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int a = 10, b = 20; for (int i = 0; i < 100; ++i) cout << left << setw(20) << i << i * i << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Şimdi de dosya işlemlerinin standart sınıflarla nasıl yapılacağı üzerinde duralım. Daha önceden de belirttiğimiz gibi dosya sınıflarının türetme şeması şöyledir: ios_base (şablon sınıf değil) basic_ios basic_istream basic_ostream basic_ifstream basic_iostream basic_ofstream basic_fstream Eğer bi bir dosyadan yalnızca okuma yapacaksak basic_ifstream sınıfını kullanabiliriz. basic_ifstream sınıfı basic_istream sınıfından türetilmiştir. Dolayısıyla yukarıda görmüş olduğumuz basic_istream sınıfının üye fonksiyonlarını kullanabiliriz. Bir dosya yazma yapmak için basic_ofstream sınıfını kullanabiliriz. Bu sınıf da basic_ostream sınıfından türetilmiştir. Nihayet bir dosyadan hem okuma hem de o dosyaya yazma yapmak istiyorsak basic_fstream sınıfını türetebiliriz. Bu sınıf basic_iostream sınıfından türetilmiştir. Dolayısıyla biz basic_fstream nesnesi ile hem basic_istream sınıfının hem de basic_ostream sınıfının üye fonksiyonlarını kullanabiliriz. Bu sınıfların char türden açılımları ifstream, ofstream ve fstream biçiminde typedef edilmiştir. Dosya sınıfların hepsinin bildirimi dosyası içerisindedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_ifstream sınıfının yapıcı fonksiyonu şöyledir: explicit basic_ifstream(const char *filename, ios_base::openmode mode = ios_base::in); Burada default açış modu ios_base::in belirtilmiştir. Açış modları şunlardan oluşturulabilmektedir: app (Sona ekleme modu) binary (Binary mode) in (Okuma amaçlı açım) out (Yazma amaçlı açım) trunc (Sıfırlayarak açım) ate (Dosya göstericisini sona çekerek açım) noreplace (C++23) (Dosya varsa başarısız olacak biçimde "exclusive" açım) Tabii bu modların hepsini ifstream nesnesini yaratırken kullanamayız. ifstream yazmaya izin vermediğine göre out modunun ifstream için bir anlamı yoktur. dosyalar default text modda açılmaktadır. Binary modda açım için binary bayrağının da açışta kullanılması gerekir. Örneğin: ifstream f("test.txt"); Burada "test.txt" isimli dosya açılmak istenmiştir. Pekiyi işlemin başarısı nasıl kontrol edilecektir. Dosya açılamazsa failbit set edilmektedir. Dolayısıyla biz bu işlemden hemen sonra fail fonksiyonu ile ya da bool türüne dönüştürme yapan operatör fonksiyonu ile ya da ! operatör fonksiyonu kontrolü yapabiliriz. Örneğin: ifstream f("test.cpp"); if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } Aslında sonraki paragraflarda ele alacağımız gibi durum bayraklarından istediğimiz bazıları set edildiğinde fonksiyonların otomatik exception oluşturması da sağlanabilmektedir. Ancak bu yöntem kullanılmayacaksa tek yol yukarıdaki gibi yapıcı fonksiyondan sonra kpontrolün yapılmasıdır. basic_ifstream sınııfının yapıcı fonksiyonunun birinci parametresi açılacak dosyanın yol ifadesini, ikinci parametresi ise açış modeunu almaktadır. Bu ikinci parametreye ios::in biçiminde default değer verilmiştir. Fonksiyonun birinci parametresi const T * ve basic_string & türleri için overload edilmiştir. Yani biz fonksiyonun birinci parametresine istersek bir basic_string nesnesi de geçebiliriz. Örneğin: string path = "app.cpp"; ifstream f(path); if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } İstersek biz basic_ifstream nesnesini default yapıcı fonksiyon ile yaratıp dosyayı sınıfın open üye fonksiyonu ile açabiliriz. open fonksiyonun parametrik yapısı yapıcı fonksiyon ile aynı biçimdedir. Örneğin: ifstream f; f.open("app.cpp", ios::in); if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 109. Ders 30/10/2024 Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Dosyayı basic_ifstream sınıfı ile açtıktan sonra biz artık basic_istream sınıfından gelen (yani cin nesnesiyle kullanabildiğimiz) üye fonksiyonlar yoluyla dosya işlemlerini yapabiliriz. Örneğin: ifstream f("app.cpp"); char ch; if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } while (f.get(ch)) cout << ch; Ya da örneğin: ifstream f("app.cpp"); char buf[1024]; if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } while (f.getline(buf, 1024)) cout << buf << endl; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_ifstream sınıfı ile açtığımız dosya otomatik olarak sınıfın yıkıcı fonksiyonu tarafından kapatılmaktadır. Tabii biz dosyanın daha önce kapatılmasını da isteyenbiliriz: ifstream f("app.cpp"); //... f.close(); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_ifstream sınıfı ile dosyayı binary modda da açabiliriz. Bunun için açış moduna ios::binary bayrağının da dahil edilmesi gerekir. Örneğin: ifstream f("app.cpp", ios::in|ios::binary); if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } Tabii dosyayı okuma amaçlı binary modda açtıktan sonra genellikle okumaları basic_istream sınıfının read fonksiyonuyla yaparız. Örneğin: if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } f.read(buf, 512); Aşağıda bir dosyayı binary modda açıp ilk 512 byte'ı hex olarak görüntüleyen örnek bir program verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include #include using namespace std; int main() { ifstream f("app.cpp", ios::in|ios::binary); char buf[512]; if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } if (!f.read(buf, 512) && f.gcount() == 0) { cerr << "cannot read file" << endl; exit(EXIT_FAILURE); } cout << hex << setfill('0') << uppercase; auto n = f.gcount(); for (streamsize i = 0; i < n; ++i) { cout << setw(2) << static_cast(buf[i]) << (i % 16 == 15 ? '\n' : ' '); //printf("%02X%c", (unsigned char)buf[i], i % 16 == 15 ? '\n' : ' '); //print("{:02X}{}", (unsigned char)buf[i], i % 16 == 15 ? '\n' : ' '); } f.close(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Dosyayı yazma yapma amaçlı açmak için basic_ofstream sınıfı kullanılmaktadır. Bu sınıfın char türden açılımı ofstream olarak typedef edilmiştir. basic_ofstream sınıfının genel kullanımı basic_ifstream sınıfına benzerdir. Ancak bu sınıf basic_ostream sınıfından türetildiği için biz bu türden bir sınıf nesnesi ile ancak ostream sınıfının üye fonksiyonlarını kullanabiliriz. basic_ofstream sınıfı ile dosya açmak için yine sınıfın yapıcı fonksiyonu kullanılabilir. Sınıfın yapıcı fonksiyonu yine dosyanın yol ifadesini const char * olarak ya da string olarak bizden almaktadır. Yine yapıcı fonksiyonun ikinci parametresi açış modunu belirtmektedir. Açış modu default durumda ios::out biçimindedir. ostream sınıfı ile dosya açılırken açış modlarının anlamları şöyledir: - ios::out (default durum): Bu modda dosya yoksa yaratılıp açılır, dosya varsa sıfırlanır. Bu C'deki "w" moduna karşılık gelmektedir. - ios::binary: Bu yine binary modda açım yapmak için kullanılır. Tipik olarak ios::out ile birlikte kullanılmaktadır. - ios::app: Bu modda dosya yoksa yaratılır ve açılır, dosya varsa olan açılır. Her yazılan dosyanın sonuna eklenir. C'deki "a" moduna karşılık gelmektedir. - ios::ate: Bu modda yine dosya yoksa yaratılır ve açılır, dosya varsa sıfırlanmaz, ancak dosya göstericisi dosyanın sonuna çekilir. Yani açıldığında dosya göstericisi dosyanın sonunda olur. Yine dosyasnın başarılı bir biçimde açılıp açılmadığı ! operatörüyle ya da fail fonksiyonu ile tespit edilebilir. Açılmış olan dosyalar sınıfın yıkıcı fonksiyonu tarafından kapatılır. Yıkıcı fonksiyondan önce dosya kapatılacaksa yine close üye fonksiyonu kullanılmalıdır . Örneğin: ofstream f("test.txt"); if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } //... f.close(); Bu biçimde açılmış dosyalara basic_ostream sınıfındaki daha önce gördüğümüz üye fonksiyonlarla yazma yapabiliriz. Örneğin: ofstream f("test.txt"); if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } f << "this is a test" << endl; f.write("test", 4); f.close(); --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { ofstream f("test.txt"); if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } f << "this is a test" << endl; f.write("test", 4); f.close(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıda dosya kopyalayan bir fonksiyon örneği verilmiştir. Bu fonksiyonda bir döngü içerisinde kaynak dosyadan bir grup byte okunup hedef dosyaya yazdırılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; constexpr int BUFFER_SIZE = 4096; bool copy_file(const string &source_path, const string &dest_path) { char buf[BUFFER_SIZE]; streamsize n; ifstream fs(source_path, ios::in | ios::binary); ofstream fd(dest_path, ios::out|ios::binary); if (fs.fail() || fd.fail()) return false; while (fs.read(buf, BUFFER_SIZE) || (n = fs.gcount()) > 0) if (!fd.write(buf, n)) return false; if (!fs.eof()) return false; fd.close(); fs.close(); return true; } int main() { if (!copy_file("app.cpp", "test.cpp")) { cerr << "cannot copy file!.." << endl; exit(EXIT_FAILURE); } cout << "success..." << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_fstream sınıfı basic_iostream sınıfından türetilmiştir. Dolayısıyla biz bu sınıf yoluyla dosyadan hem okuma yapabiliriz hem de dosyaya yazma yapabiliriz. Bu sınıfın char türdne açılımı fstream ismiyle typedef edilmiştir. Sınıfın genel kullanımı tamamen basic_ifstream ve basic_ofstream sınıflarının birleşimi gibidir. SInıfın yapıcı fonksiyonu yine dosyanın yol ifadesini bizden const char * ya da const string & parametresiyle almaktadır. Default modu ios_base::in|ios_base::out biçimindedir. Yine sınıfın open ve close üye fonksiyonları bulunmaktadır. Örneğin: fstream f("test.txt"); if (!f) { cerr << "cannot open file!.." << endl; exit(EXIT_FAILURE); } Biz basic_fstream nesnesi ile hem basic_istream sınıfının üye fonksiyonlarını hem de basic_ostream sınıfının üye fonksiyonlarını kullanabiliriz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C'de fopen ile açılmış dosyalarda tek bir göstericisi vardır. Okuma işleminde de yazma işleminde de aynı dosya göstericisi ilerletilmektedir. Ancak okumadan yazmaya, yazmadan okumaya geçişte dosya göztericisinin fseek fonksiyonu ile (ya da rewind fonksiyonu ile) konumlandırılması gerekmektedir. Oysa C++'ın IO kütüphanesinde iki ayrı dosya göstericisi bulunmaktadır. Bunlardan biri "okuma" göstericisi diğeri ise "yazma" göstericisidir. Okuma işlemi okuma göstericisinin gösterdiği yerden itibaren yazma işlemi ise yazma göstericisinin gösterdiği yerden itibaren yapılmaktadır. Okuma göstericisi basic_istream sınıfında yazma göstericisi ise basic_ostream fınıfında tutulmaktadır. Bu sınıflarda okuma ve yazma göstericilerini konumlandıran seekg ve seekp isimli iki fonksiyon bulunmaktadır. Tabii seekg fonksiyonu basic_istream sınıfında, seekp fonksiyonu ise basic_ostream sınıfındadır. (Burada g harfi "get" sözcüğünden, "p" harfi ise "put" sözcüğünden gelmektedir.) --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 110. Ders 04/11-2024 Pazartesi --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Okuma ve yazma dosya göstericilerini konumlandırmak için basic_istream sınıfından gelen seekg ve basic_ostream sınıfından gelen seekp fonksiyonları kullanılmaktadır. Okuma ve yazma göstericilerinin konumları da sırasıyla tellg ve tellp fonksiyonları ile elde edilmekltedir. seekg ve seekp fonksiyonlarının iki overload biçimleri vardır: basic_istream& seekg(pos_type pos); basic_istream& seekg(off_type off, std::ios_base::seekdir dir); basic_ostream& seekp(pos_type pos); basic_ostream& seekp(off_type off, std::ios_base::seekdir dir); Fonksiyonların tek parametreli versiyonları konumlandırmayı baştan itibaren iki parametreli versiyonları ise konumlandırmayı belirtilen orijinden itibaren yapmaktadır. Orijin belirten bayraklar ios_base sınıfında seekdir türüyle aşağıdaki gibi bildirilmiştir: beg end cur Her ne kadar iki dosya dosya göstericisi birbirindne ayrıysa da bunlar aynı tamponu kullandıkları için tıpkı C'de olduğu gibi okumadan yazmaya geçişte, yazmanadan okumaya geçişte dosya göstericisinin konumlandırılması gerekmektedir. Örneğin: ifstream f("app.cpp"); string line; f.seekg(40); getline(f, line); cout << line << endl; Burada biz dosya göstericisini 40'ıncı offset'e konumlandırıp oradna okuma yaptık. Aşağıda dosya fstream sınıfı ile açılıp önce dosyaya yazma yapılmış sonra okuma dosya göstercisi dosyanın başına çekilerek dosyadan okuma yapılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { fstream f("test.txt"); string line; f << "this ia test" << endl; f << "this is an other test" << endl; cout << f.tellp() << endl; f.seekg(0); getline(f, line); cout << line << endl; getline(f, line); cout << line << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Stream nesnelerinin eof, fail veya bad bitleri set edilmişse bunlar reset edilene kadar dosya işlemlerinin başarısızlıkla sonuçlanacağına dikkat ediniz. Örneğin biz bir dosyayı ifstream sınıfı ile açıp sonuna kadar bir döngü içerisinde okumuş olalım. Bu durumda eof ve fail bitler set edilecektir. Biz dosya göstericisini başa çeksek bile bu bitleri temizlemeden okuma yapamayız. basic_ios sınıfından gelen clear fonksiyonunun bu bayrakları reset ettiğini anımsayınız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; int main() { ifstream f("app.cpp"); string line; while (getline(f, line)) cout << line << endl; f.clear(); // dikkat! eof bayrağı ve fail bayrağı reset edilmezse okuma dosya göstericisi başa çekilse bile okuma yapılamaz f.seekg(0); while (getline(f, line)) cout << line << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi C'de dosya işlemleri bir tampon (buffer) eşliğinde yürütülüyordu. Örneğin biz bir dosyadan bir byte bile okumuş olsak aslında okuma yapan fonksiyon taponu dolduruyor ve sonraki okumalarda disk işlemi gerekmeden okumanın yapılmasını sağlıyordu. Bu tamponun yazma sırasında da kullanıldığını anımsayınız. İşte C++'ta da dosya işlemleri bit tampon yoluyla yapılmaktadır. Ancak C++'ta bu tampon basic_streambuf isimli bir sınıfla temsil edilmiştir. Ancak okuma tamponu ve yazma tamponu biçiminde iki ayrı tampon yoktur. Tampon nesnesi (yani basic_streambuf türünden nesne) basic_ios sınıfında bulunmaktadır. İzleyen paragraflarda açıklanacağı gibi bu sınıf "sanal taban sınıf (virtual base class)" biçiminde kullanılmaktadır. basic_streambuf sınıfının char türnden açılımı streambuf ismiyle typedef edilmiştir. Biz basic_istream ve basic_ostream nesnelerinin kullandığı basic_streambuf nesnesini basic_ios sınıfından gelen rdbuf fonksiyonu ile elde edebiliriz ve set edebiliriz. Biz kursumuzda bu basic_streambuf sınıfını görmeyeceğiz. Bu sınıfın kullanımının çeşitli ayrıntıları vardır. Bu konu "İleri C++ Kursunda" ele alınmaktadır --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Anımsanacağı gibi C'de fprintf fonksiyonunun dosyaya değil de char türden bir diziye yazan biçimi sprintf ve snprintf isimli fonksiyonlardır. Benzer biçimde fscanf fonksiyonunun da dosyadan değil de char türdne bir dizinden okuma yapan biçimi sscanf isimli fonksiyondur. İşte C++'ta sprintf ve sscanf fonksiyonların işlevsellikleri basic_istringstream ve basic_ostringstream sınıfları tarafındna yerine getirilmektedir. Bu sınıfların char türden açılımları istringstream ve ostringstrem ismiyle typedef edielmiştir. basic_istringstream sınıfı sscanf işlevselliğini, basic_ostringstream sınıfı ise sprintf işlevselliğini sağlamaktadır. Her iki işlevselliği sağlayan basic_stringstream isimli bir sınıf da vardır. Bu sınıfın da char türdne açılımı stringstream ismiyle typedef edilmiştir. Sınıfların türetme şemaları şöyledir: ios_base (şablon sınıf değil) basic_ios basic_istream basic_ostream basic_istringstream basic_iostream basic_ostringstream basic_stringstream String stream sınıflarının bildirimleri başlık dosyasında yapılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_ostringstream sınıfının kullanımı oldukça kolaydır. Nesne sınıfın default yapıcı fonksiyonu ile yaratılır. Sonra sanki dosyaya yazıyormuş gibi basic_ostream sınıfından gelen fonksiyonlarla (örneğin << operatör fonksiyonlarıyla) yazma yapılır. Aslında bu yazılanlar basic_streambuf ile temsil edilen tampon nesnesinin içerisine yaılmaktadır. Yazılan yazı da sınıfın str isimli üye fonksiyonuyla string nesnesi olarak elde edilir. Parametresiz str fonksiyonu bize yazıyı string nesnesi olarak verir const basic_string & parametreleri str fonksiyonu ise nesne içerisindeki yazıyı bütünsel olarak yeniden set eder. int a = 10, b = 20; ostringstream oss; oss << "a = " << a << ", b = " << b; cout << oss.str() << endl; // a = 10, b = 20 oss.str("this is a test"); // this is a test cout << oss.str() << endl; basic_ostringstream nesnesine istediğimiz kadar çok şey yazabiliriz. Bu nesnenin kullandığı basic_streambuf tampon nesnesi otomatik büyütülmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int a = 10, b = 20; ostringstream oss; oss << "a = " << a << ", b = " << b; cout << oss.str() << endl; // a = 10, b = 20 oss.str("this is a test"); // this is a test cout << oss.str() << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_istringstream sınıfının da kullanımı oldukça kolaydır. Nesne genellikle sınııfn basic_string & parametreli yapıcı fonksiyonu ile yaratılır. Bu fonksiyon okumanın yapılacağı yazıyı belirtmektedir. Sonra okuma işlemi sınıfın basic_istream sınıfından gelen üye fonksiyonlarıyla (örneği >> operatör fonksiyonlarıyla) yapılır. Örneğin: int a, b, c; istringstream iss("100 200 300"); iss >> a >> b >> c; cout << "a = " << a << ", b = " << b << ", c = " << c << endl; // a = 100, b = 200, c = 300 Nesne içerisindeki yazı yine str üye fonksiyonu ile alınıp tümden aynı üye fonksiyon ile değiştirilebilir. Ancak bu tür durumlarda eof bayrağına dikkat ediniz. eof bayrağı bir kez set edildiğinde biz str fonksiyonu ile tampona yeni yazı yerleştirsek bile bayrakları temizlemeden sonraki okumayı başarılı biçimde gerçekleştiremeyiz. Örneğin: int a, b, c; istringstream iss("100 200 300"); iss >> a >> b >> c; cout << "a = " << a << ", b = " << b << ", c = " << c << endl; // a = 100, b = 200, c = 300 cout << iss.str() << endl; // 100 200 300 iss.str("300 400 500"); cout << iss.str() << endl; // 300 400 500 iss.clear(); // dikkat bu fonksiyonu çağıramazsak aşağıdaki okumalar yapılamaz iss >> a >> b >> c; cout << "a = " << a << ", b = " << b << ", c = " << c << endl; // a = 100, b = 200, c = 300 --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int a, b, c; istringstream iss("100 200 300"); iss >> a >> b >> c; cout << "a = " << a << ", b = " << b << ", c = " << c << endl; // a = 100, b = 200, c = 300 cout << iss.str() << endl; // 100 200 300 iss.str("300 400 500"); cout << iss.str() << endl; // 300 400 500 iss.clear(); // dikkat bu fonksiyonu çağıramazsak aşağıdaki okumalar yapılamaz iss >> a >> b >> c; cout << "a = " << a << ", b = " << b << ", c = " << c << endl; // a = 100, b = 200, c = 300 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- basic_stringstream sınıfı hem basic_istringstream sınıfının hem de basic_ostringstream sınıfının işlevselliklerine sahiptir. Dolayısıyla biz bu sınıf türündne nesneyle hem sprintf hem de sscanf fonksiyonlarının yaptığı işlemleri yapabiliriz. Yşne nesne sınıfın yapıcı fonksiyonu ile bir verilerek yaratılabilir ya da yazı daha sonra sınıfın str fonksiyonu ile set edilebilir. Örneğin: int a, b, c; stringstream ss("100 200 300"); ss >> a >> b >> c; cout << "a = " << a << ", b = " << b << ", c = " << c << endl; ss.clear(); ss.seekp(0); // dikkat bayrakların yine reset edilemsi gerekir ss << 400 << " " << 500; cout << ss.str() << endl; // 400 500 300 Burada önce nesne "100 200 300" yazısıyla yaratılmıştır. Sonra okuma yapılmıştır. Dolayısıyla son okumada eof bayrağı set edilecektir. Biz de eğer bayrakları temizlemezsen sonraki yazma işlemini başarıyla yapamayız. Yine okumadan yazmaya yazmadan okumaya geçişte dosya göstericisinin (yani get ya da put göstericisinin) konumlandırılması gerekmektedir. basic_stringstream nesnesini bir fstream nesnesinin belleğe yazan biçimi gibi düşünmelisiniz. Yani dosyalar için söylediğimiz her şey bu nesneler için de geçerlidir. Örneğin: int a, b, c; stringstream ss("100 200 300"); ss >> a >> b >> c; cout << "a = " << a << ", b = " << b << ", c = " << c << endl; ss.clear(); ss.seekp(0, ios::end); ss << " " << 400 << " " << 500; cout << ss.str() << endl; // 100 200 300 400 500 --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; int main() { int a, b, c; stringstream ss("100 200 300"); ss >> a >> b >> c; cout << "a = " << a << ", b = " << b << ", c = " << c << endl; ss.clear(); ss.seekp(0, ios::end); ss << " " << 400 << " " << 500; cout << ss.str() << endl; // 100 200 300 400 500 return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta bir sınıf içerisinde başka bir sınıf ya da enum bildirimi yapılabilir. Bir sınıf başka bir sınıfın içerisinde bildirilirse bu durum hiçbir biçimde bir veri kapsaması oluşturmaz. (Bu durum Java gibi bazı dilelrde veri kapsaması da oluşturabilmektedir.) Yani bir sınıfı başka bir sınıfın içerisinde bildirdiğimiz zaman biz yalnızca iç sınııf faaliyet alanı bakımından dış sınıf ile ilişkilendirmiş oluruz. Dış sınıfın içerisinde iç sınıf doğrudan kullanılabilir. Ancak dışarıdan eğer erişim hakkı varsa dış sınıf ismiyle niteliklendirilerek iç sınıf kullanılabilmektedir. Örneğin: class A { public: class B { public: //... private: int m_b; }; //... private: int m_a; }; Burada A sınıf ile B sınıfı arasında herhangi bir içerme ilişkisi yoktur. Burada B sınıfının A sınıfının içerisinde bildirilmesinin dışarıda bildirilmesinden tek farkı isimsel bir içerme sağlamasıdır. Biz burada B ismini A'nın içerisinde kullanabiliriz. Ancak dışarıdan kullanamayız. Fakat B sınıfı A sınıfının public bölümğnde olduğu için dışarıdan B sınıfını A::B niteliklendirmesiyle kullanabiliriz. Örneğin: class A { public: class B { public: //... private: int m_b; }; void foo(); private: int m_a; }; void A::foo() { B b; // geçerli //... } //... A a; // geçerli A::B b; // geçerli Burada B isminin A içerisinde (sınıf bildiriminde ve A'nın üye fonksionları içerisinde) doğrudan kullanılabilediğine ancak dışarıdan doğrudan değil A::B biçiminde niteliklendirilerek kullanılabildiğine dikkat ediniz. Burada A sınıfı türündne bir nesne B'nin veri elemanlarını, B sınıfı türünden bir nesne de A'nın veri elemanlarını kaosamamaktadır. Bu iki sınıf tamamen isimsel kapsama dışında bağımsız iki sınıftır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ta sınıf bildirimi içerisinde oluşturulan tür isimleri sınıf bildirimi içerisinde kullanılacaksa ancak bu bildirimden daha sonra kullanılabilir. Ancak sınıf bildiriminde oluşturulan tür isimleri daha yukarıda bile olsa üye fonksiyonlar içerisinde kullanılabilmektedir. Bu kural eleştirilebilir. ancak C++'ın ilk versiyonlarından beri böyledir. Örneğin: class A { public: void foo() { B b; // geçerli //.. } B m_invalid; // geçersiz! class B { public: //... private: int m_b; }; private: B m_b; // geçerli }; Burada B sınıfı A sınıfının içerisinde bildirilmiştir. A sınıfının B sınıfı türünden bir veri elemanına sahip olmasının istendiğini düşünelim. Bu veri elemanı ancak B sınıfının bildrimindne sonra bildirilebilir. Ancak üye fonksiyonlar içerisinde bu fonksiyonlar daha yukarıda tanımşanmış olsa bile bu tür isimleri kullanılabilmektedir. Bu kural yalnızca sınıf içerisinde sınıf bildiriminde değil her türlü tür bildiriminde geçerlidir. Örneğin: class A { public: void foo() { I a; // geçerli //.. } I m_invalid; // geçersiz! typedef int I; private: I m_a; // geçerli }; Bu bağlamda fonksiyonların geri dönüş değerlerinin ve parametrelerinin fonksiyonun dışında olduğu kabul edilmektedir. Örneğin: class A { public: I foo(); // geçersiz! void bar(I a); // geçersiz! typedef int I; //... }; Bir üye fonksiyon sınıfın dışınd atanımlanırken geri dönüş değerindeki isimler fonksiyonun tanımlamasının yerleştirildiği yere göre, parametredeki isimler ise sınıf faaliyet alanında da aranmaktadır. Örneğin: class A { public: typedef int I; I foo(I a); // geçerli //... }; I A::foo(I a) // geçersiz { //... } Buradaki foo fonksiyonun dışarıdkai tanımlamasına dikkat ediniz. Geri dönüş değerinde kullanılan I ismi isim aramasında bulunamayacaktır. Ancak parametredeki I ismi isim aramasında bulunacaktır. (Bu tasarımın nedeni muhtemelen parametre isimlerinin A:: niteliklendirmesinden sonra gelmesindendir.) Üye fonksiyonun dışarıdaki tanımlamasında fonksiyonun parametresindeki ya da iç bloğu içerisindeki isimler sınıf faaliyet alanında bulunamazsa global faaliyet alanında tanımlamanın yerleitirildiği yereden yukarıda aranmaktadır, sınıf bildiriminin yerleştirildiği yerden yukarıda aranmamaktadır. Örneğin: class A { public: int foo(int a); //... }; typedef int I; int A::foo(I a) // geçerli { //... } Buradaki I ismi önce sınıf faaliyet alanında aranacak eğer orada bulunamazsa tanımlamanın yerleştirildiğ yerden itibaren yukarıdaki global alanlarda aranacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- 111. Ders 06/11/2024 Çarşamba --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi bir sınıf içerisinde bir sınıf bildirmenin anlamı ve faydası ne olabilir? İşte eğer bir sınıf yazılırken başka bir sınıftan faydalanılıyorsa ancak bu faydalanılan sınıfın dışarıdan kullanılması anlamsız ise bu durumda bu sınıf bir iç sınıf olarak bildirilebilir. Böylece bu sınıfın yalnızca içinde bildirilen sınıf tarafından ve yalnızca o sınıfın gerçekleştiriminde kullanılabileceği belirtilmiş olur. Zaten dışarıdan kullanımın anlamsız olduğu durumda boşuna isim kirliliği ve kafa karışıklığı oluşturulmayacaktır. Örneğin bir bağlı liste sınıfı yazacak olalım. Bu bağlı liste sınıfında düğümleri temsil eden bir Node sınıfından faydalanacak ollaım. Bu Node sınıfının dışarıdan kullanılmasının bir anlamı yoktur. Bu Node sınıfı yalnızca bağlı listenin gerçekleştiriminde kullanılacaktır. O halde buradaki Node sınıfını dışarıda bildirmek yerine bağlı liste sınıfının içerisinde bildirmek daha iyi bir tekniktir. Örneğin: template class LList { public: //... private: template struct Node { Node *m_next; Node *m_prev; T m_val; }; Node *m_head; Node *m_tail; std::size_t m_count; }; C++'ta dış sınıfın iç sınıfa iç sınıfın da dış sınıfa hiç bir özel erişim ayrıcalığı yoktur. (Halbuki örneğin C#'ta iç sınıfın dış sınıfa bir erişim ayrıcalığı vardır.) Yukarıdaki örnekte Node sınıfını struct olarak yani elemanları public bölümde olacak biçimde bildirdik. Eğer bu sınıfın elemanlarını yine private bölümde gizlemek isteseydik bu durumda LList sınıfının bu sınıfın private elemanlarına erişmesi için Node sınıfı tarafından ona arkadaşlık verilmesi gerekirdi. Örneğinİ: template class LList { public: //... private: template class Node { public: friend class LList; //... private: Node *m_next; Node *m_prev; T m_val; }; Node *m_head; Node *m_tail; std::size_t m_count; }; Burada LList sınıfının üye fonksiyonlarının Node sınıfının private bölümüne erişebilmesi için arkadaşlık bildiriminin yapılmış olması gerekir. Çünkü iç sınıfın dış sınıfa, dış sınıfın da iç sınıfa hiçbir özel erişim ayrıcalığı yoktur. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Eskiden enum sabitlerine bir faaliyet alanı kazandırabilmek için onlar bir sınıf ile sarmalanıyordu. Örneğin: struct StyleType { enum Style { Up, Down, Hatch, Cross }; }; struct DirectionType { enum Direction { Up, Right, Down, Left }; }; Burada artık StyleType::Up ile DirectionType::Up birbirlerine karışmayacaktır. C++11 ile birlikte "faaliyet alanlı enum'lar (scoped enumerations)" dile eklenince artık bu biçimdeki sarmalamaya gerek kalmadı. Örneğin: enum Class StyleType { Up, Down, Hatch, Cross }; struc DirectionType { Up, Right, Down, Left }; Burada yine StyleType::Up ile DirectionType::Up birbirlerine karışmayacaktır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Bir sınıfın içerisinde tür bildirimleri özellikle C++'ın standart kütüphanesinde çokça karşımıza çıkmaktadır. Örneğin: string s{"ankara"}; string::size_type sz = s.size(); Tabii C++11 ile birlikte artık bu tür bildirimleri typedef belirleyicisinin yaznı sıra using bleirleyicisi ile de yapılabilmektedir. Örneğin: class Sample { public: typedef int I; using D = double; //... }; --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İç içe sınıf bildirimlerinde iç sınıfın üye fonksiyonları dış isim alanlarında tanımlanabilir. Ancak dış sınıfta tanımlanamaz.İç sınıfın üye fonksiyonları dış sınııfn yerleştirildiği isim alanında ya da o isim alanını kapsayan isim alanlarında tanımlanabilir. Örneğin: class A { public: void foo(); class B { public: //... private: void bar(); }; }; void A::foo() { //... } void A::B::bar() { //... } --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Kursumuzda son olarak "sanal taban sınıflar (virtual base classes)" üzerinde duracağız. Sanal taban sınıflar baklava (ya da elmas) biçiminde türetmelerde karşımıza çıkan bir durumdur. B ve C sınıflarının A sınıfından, D sınıfınında çoklu olarak B ve C sınıflarından türetildiğini varsayalım: A B C D class A { //... }; class B : public A { //... }; class C : public A { //... }; class D : public B, public C { //... }; Burada biz bağımısz bir B nesnnesi yaratsaydık bu B nesnesinin içerisinde A'nın veri elemanları da bulunurdu. Benzer biçimde biz bağımsız bir C nesnesi yaratsaydık bunun içerisinde de A'nın veri elemanları bulunacaktı. O halde D nesnesinin içerisinde B kolundan gelen ve C kolundan gelen iki ayrı A veri elemanları bulunacaktır. D sınıfı türünden bir nesne tipik olarak aşağıdaki gibi veri eleman bloklarına sahip olacaktır: D d; A_dat B_dat A_dat C_dat D_dat İşte baklava biçiminde çoklu türetmelerde tepedeki taban sınıfın veri elemalarının çoklu türetilmiş sınıfta birden fazla kez yer alaması genellikle istenilen bir durum değildir. Bu tür türetmelerde tepedeki sınıfın (örneğimizde A) veri elemanlarının tek bir kopyasının bulunması istenir. iostream sınıf sistemi baklava biçimindeki türetmelere tipik bir örnek oluşturmaktadır: ios_base basic_ios basic_istream basic_ostream basic_iostream Burada iostream nesnesi içerisinde basic_ios ve ios_base elemanlarının birden fazla kez bulunması uygun değildir. Çünkü bu sınıf sisteminde tamponlama nesnesi (basic_streambuf) ve Bayrak nesneleri, basic_ios sınıfının içerisindedir. Bunlardan ikişer tane olması tüm sistemi bozabilir. Örneğin böylesi bir durumda biz okuma yaptığımızda failbit set edildiğinde yazma yaparken bu set işlemi gözükmeyebilir. Böyle bir sistemde okuma ve yazma tamponları basic_istream ve basic_ostream için farklılaşacaktır. Bu da tampon mekanizmasını tamamen bozucu etki yapacaktır. Baklava türetmesindeki taban yinelenmesini aşağıdaki basit örnekle hemen anlayabiliriz: class A { public: int m_a; }; class B : public A { public: int m_b; }; class C : public A { public: int m_c; }; class D : public B, public C { public: int m_d; }; //... D d; cout << sizeof(d) << endl; // 20 Burada d nesnesinin kapladığı alan d nesnesi içerisinde iki ayrı m_a elemanı olduğu için 20 olacaktır. Bu tür durumlarda iki anlamlılık hatalarına ayrıca dikkat edilmesi gerekir. Örneğin: D d; d.m_a = 10; // geçersiz! ambiguity Burada d nesnesinin içeriisnde iki ayrı m_a elemanı olduğuna göre hangi m_a elemanının kullanıldığı anlaşılamamaktadır. Tabii aşapğıdaki gibi erişimlerde bir sorun oluşmayacaktır: d.B::m_a = 10; d.C::m_a = 20; Burada d.B::m_a d nesnesinin B kolondan gelen m_a elemanı, d.C::m_a ise d nesnesinin C kolundan gelen m_a elemanıdır. Fakat yukarıda da belirttiğimiz gibi bu tür baklava tarzı türetmelerde tepedeki taban ısnıfın elemanlarının çoklu türetilmiş sınıfta yalnızca bir tane olması istenir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İşte sanal taban sınıf bildirimi tamamen baklava biçimindeki çoklu türetmelerde tepedeki taban sınıfın çoklu türetilmiş nesnede toplam tek bir kopyasının bulunmasını sağlamak için kullanılmaktadır. Sanal taban sınıf bildiriminde türetme sentaksındaki taban sınıfın önüne virtual anahtar sözcüğü getirilir. Örneğin: class A { public: int m_a; }; class B : virtual public A { public: int m_b; }; class C : virtual public A { public: int m_c; }; class D : public B, public C { public: int m_d; }; D d; virtual anahtar sözcüğü ile türetme biçimi belirten anahtar sözcükler yer değiştirmeli olarak yazılabilir. Ancak genellikle önce virtual anahtar sözcüğünün belirtilmesi tercih edilmektedir. Burada sanal taban sınıf bildiriminin yinelenen sınıfta yapıldığına dikkat ediniz. Pekiyi burada virtual anahtar sözcüğü tam olarak neyi sağlamaktadır? İşte virtual olarak bildirimiş taban sınıflara eğer türemiş sınıf nesnesi içerisinde farklı kollardan erişiliyorsa ondan tek bir kopya bulundurulur. Yani yukarıdaki örnekte artık d nesnesi içerisindeki iki farklı m_a elemanı olmayacak toplamda tek bir m_a elemanı olacaktır. Dolayısıyla artık d.m_a biçimindeki erişim geçerli hale gelmektedir. Taban sınıfı belirtirken kullanılan virtual belirleyicisinin işlev görebilmesi için farklı kollardan aynı sınıfa uygulanması gerekir. Aşağıdaki gibi tek bir koldan uygulanan virtual bildirimi bu anlamda bir fayda sağlamayacaktır: A B (virtual A) C D Burada B sınıfının bildiriminde A'nın virtual olarak bildirildiğini ancak C sınıfının bildiriminde A'nın virtual olarak bildirilmediğini varsayalım. İşte bu durum d nesnesi içerisinde tek bir A sınıfı veri elemanlarının bulunmasına yol açmayacaktır. Farklı kollardan aynı sınıfa virtual bildirimi uygulanırsa o veri elemanları birleştirilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Pekiyi baklava biçiminde sanal taban sınıf bildirimi ile tepedeki sınıfın veri elemanlarının tek kopyasının oluşturulması durumunda nesnelerin bellek organizasyonu nasıl yapılacaktır? Örneğin: B b; C c; Buradaki b nesnesinin tipik organizasyonun şöyle olduğunu görmüştük: A_dat B_dat c nesnesinin organizasyonun da şöyle olmasını bekleriz: A_dat C_dat O halde D sınıfı türünden d nesnesinin bellek organizasyonu nasıl olacaktır? Aşağıdaki gibi olması beklenebilir: A_dat B_dat C_dat D_dat Ancak organizasyon böyle yapılamaz. Çünkü türemiş sınıf türünden nesnenin adresi onun bütün taban sınıflarına atanabileceğine göre bir uyumsuzluk oluşur: B *pb; C *pc; pb = &d; // geçerli pc = &d; // geçerli Bıurada pc göstericisinin gösterdiği yerde C'nin veri elemanları vardır. Onun hemen yukarısında A'nın veri elemanlarının olması gerekir. Ancak yukarıdaki organizasyonda A'nın değil B'nin veri elemnalrı vardır. O halde organizasyon bu biçimde yapılamaz. Pekiyi nasıl yapılmaktadır? İşte ilk akla gelen makul yöntem türemiş sınıfların sanal taban sınıfa doğrudan değil bir gösterici yoluyla erişmesidir. ÖrneğiN: B b; Burada b nesnesin içerisinde bir gösterici tutulabilir. Bu gösterici B'nin A parçasını gösterir. Derleyici de her zaman bı gösterici yoluyla erişimi yapar. Örneğin: C c; Burada da c nesnesinin içerisinde yine C'nin A parçasını gösteren bir gösterici bulundurulabilir. Örneğin: D d; Burada da yine d nesnesinin içerisinde hem B nesnesi hem de C nesnesi bulundurulacak ve aynı zamanda d'nin içerisinde bir gösterici nesnesin A parçasını gösterecektir. Artık d'nin adresini B ya da C sınıfı türündne göstericiye atayıp erişim yapıldığında bir soorun çıkmaz. Çünkü zaten erişim doğrudan değil hep bu göstericiler yoluyla yapılmaktadır. Ancak aslında Microsoft derleyicilerinin, gcc ve clang derleyicilerinin kullandığı yöntem bunun biraz daha genelleştirilmiş olan biçimidir. Bu yöntemde bir handikap sınıfın birden fazla sanal taban sınıfın olduğu durumda göstericilerin çok yer kaplamasıdır. Bunun yerine nesnese tek bir gösterici tutmak ama bu göstericinin sanal taban sınıfların indekslerini (ya da adreslerini) belirten bir dizinin başlangıç adresini göstermesi daha genel bir çözümdü. Tabii programcının bu ayrıntıları bilemsi gerekmemektedir. Derleyici bir biçimde buradaki erişimi belirttiğimiz yöntemlerle zaten yapmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Baklava biçiminde çoklu türetilmiş sanal taban sınıflı sınıfların yapıcı fonksiyonlarında baklavaın yukarısındaki birleştirilmiş olan sınıfın yapıcı fonksiyonu her zaman önce çağrılır. Sonra bildirimdeki sıraya göre kollara ilişkin yapıcı fanksiyonlar çağrılır eb sonunda nesnenin ilişkin olduğu sınıfın yapıcı fonksiyonu çağrılacaktır. Örneğin: A B C D Burada B ve C bildirimlerinde A sınıfı sanal taban sınıf olarak belirtilmiş olsun. Şimdi D sınıfı türünden bir nesne tanımlayalım: D d; İşte burada önce A sınıfının yapıcı fonksiyonu sonra B sınıfının yapıcı fonksiyonu, sonra C sınıfının yapıcı fonksiyonu ve en sonunda da D sınıfının yapıcı fonksiyonu çalıştırılacaktır. (Tabii bunun için D sınıfının taban bildiriminde önce B sonra C'nin belirtilmesi gerekmektedir.) Türemiş sınıfın yapıcı fonksiyonunun MIL sentaksında yalnızca sınıfın doğrudan taban sınıflarına ilişkin yapıcı fonksiyonlar belirtilebiliyordu. Ancak bu tür baklava tarzı türetmelerde istisna olarak çoklu türetilmiş sınıfın yapıcı fonksiyonunda yalnızca doğrudan taba sınıfların değil birleştirilmiş sanal taban sınıfların da yapıcı fonksiyonalrı belirtilebilmektedir. Örneğin: D:::D(....) : A(...), B(...), C(...) { //... } Burada A sınıfı D sınıfının doğrudan tanab sınıfı olmadığı halde MIL sentaksında belirtilmiştir. Tabii her zaman yıkıcı fonksiyonlar yapıcı fonksiyonlarla ters sırada çağrılmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Thread'ler konusu oldukça detaya sahip olan ayrı bir biçimde ele alınması gereken bir konudur. Thread'ler işletim sistemini çekirdekleri (kernels) tarafından oluşturulan bir mekanizmadır. Dolayısıyşa farklı işletim sistemlerinde thread'ler konusunda o işletim sistemine özgü farklılıklar çsz konusudur. Bugün en önemli iki platform Microsoft Windows ve UNIX/Linux platformudur. Bu iki işletim sistemi grubu biribirinden tamamen farklıdır. Dolayısıyla bu iki işletim sisteminin sunduğu sistem fonksiyonları da birbirlerinden farklıdır. Thread'ler konusu bu nedenle işletim sistemi bağımlı bir konudur. Bukonuyu işletim sistem sisteminden bağımsız hale getirebilmek için yani "cross platform" bir biçimde ele alabilmek için çeşitli kütüphaneler kullanılabilmektedir. Windows'ta aşağı seviyeli thread işlemleri Windows'un sistem fonksiyonlarını çağıran "Windows API fonksiyonlarıyla" gerçekleştirilmektedir. Benzer biçimde UNIX/Linux dünyasında thread işlemleri o sistemlerdeki sistem fonksiyonlarını çağıran POSIX fonksiyonlarıyla gerçekleştirilmektedir. Java ve .NET gibi ortamlar thread işlemleri için kendi thread sınıflarını bulundurmuşlardır. Bu thread sınıfları Windows ortamlarındda Windows'un API fonksiyonları kullanılarak UNIX/Linux ortamlarında POSIX fonksiyonları kullanrak yazılmışlardır. Ancak bu kısım framework tarafından halledildiği için bu ortamlar cross platform thread işlemlerine olanak sağlamaktadır. Yine bazı framework'ler kendi thread sınıflarına sahiptir ve onlar da cross platform thread işlemlerine olabak vermektedir. Örneğin Qt kütüphanesindeki QThread sınıfı yine Windows sistemlerinde Windows'un API fonksiyonlarını çağıracak biçimde UNIX/Linux sistemlerinde POSIX fonksiyonlarını çağıracak biçimde cross-paltform olarak yazılmıştır. C standartlarında 2011 yılna kadar thread'ler hiç söz konusu edilmemiştir. C11 ile birlikte C'ye "optional" yalın bir thread kütüphanesi eklenmişse de bu kütüphane Microsoft ve GNU ve cland derleyicileri tarafından desteklenmemektedir. C++'a thread kütüphanesi C++11 ile birlikte eklenmiştir. Dolayısıyla artık cross-platform thread işlemleri C++'ın standart kütüphanesi yoluyla yapılır duruma gelmiştir. Şüphesiz C++'ın standart thread kütüphanesi de Windoes'ta Windows'un API fonksiyonları kullanılarak UNIX/Linux ve Mac sistemlerinde POSIX fonksiyonları kullaılarak yazılmış durumdadır. Ancak C++ programcısı thread'leri hep aynı biçimde kullanır. İşletim sistemi farklılığı kütüphane tarafından gerçekleştirim sırasında kendi içerisinde halledilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- İşletim sistemlerinde çalışmakta olan programalara "proses (process)" denilmektedir. Proses çalışmakta olan programın bütün her şeyini anlatan bir kavramdır. Yani onun yetki derecesini, çalışma dizininin, bellekteki yükleme adresini, açtığı dosyaları vs. Ancak prosesin akışlarına "thread" denilmektedir. Eskiden thread'ler yoktu. Thread'ler 90 yılların ortalarına doğru işletim sistemlerine girmiştir. Microsoft'un ilk thread'li işletim sistemi Windows NT (1993)'dir. Sonra bunu Windows 95 (1995) izlemiştir. UNIX/Linux dünyasına da thread'ler 90'lı yılların ortalarında girmiştir. Thread'ler proseslerin bağımsız çizelgelenen akışlarıdır. Modern işletim sistemleri genellikle "zaman paylaşımlı (time sharing)" bir thread çizelgelemesi kullanmaktdır. Yani işletim sistemi bir thread'i alır onu belli bir süre çalıştırır sonra onun çalışmasına ara verir. Diğer thread'i alır onu da belli bir süre çalıştırır. Hep böyle thread'leir parça parça çalıştırıp durdurarak çalıştırmaktadır. Kullanıcı sanki programlarını "hep çalışıyormuş zanneder" aslında programlar hep çalışmamaktadır. İşletim sistemleri onları zaman paylaşımlı bir biçimde çalıştırmaktadır. Bir thread'in parçalı çalışma süresine "quanta süresi (time quantum)" denilmektedir. Thread'in quanta süresi çeşitli faktörlere bağlı olarak değişebilmekle birlikte Windows'ta tipik olarak 20 ms. UNIX/Linxu ve Mac sistemlerinde 60 ms. kadardır. Çok işlemcili ya da çok çekirdekli sistemlerde prensip değişmez. İşletim sistemleri her işlemci ya da çekirdek için ayrı bir kuyruk oluşturur o kuyrukta yine zaman paylaşımlı bir çalışma uygular. Çok işlemci ya da çekirdeğin bulunduğu durumda işler bu işlemciler ve çekirdekler tarafından paylaşıldığı için toplamda daha hızlı bir çalışma söz konusu olur. Thread'in parçalı çalışma süresi dolduğunda thread akışı zorla işletim sistemi tarafından alınmaktadır. Bu zorla akışın alınmasına İngilizce "preemption" denilmetedir. Bu tür işletim sistemlerine ise "preemptive" işletim sistemleri denir. Akışın quanta süresi dolduunda zorla alınması donanım kesmeleri yoluyla yapılmaktadır. Artık işlemcilerde de işlemcisnin kendi içerisinde bu amaçla timer devreleri bulundurulmaktadır. Bir proses çalışmaya tek bir thread'le başlar. Buna prosesin "ana thread'i (main thread)" denilmektedir. Örneğin C'de main'den giren akış ana thread akışıdır. Diğer thread'ler programcı tarafından yaratılmaktadır. Tabii yukarıda da belirtildiği gibi aslında thread'lerin yaratılması işletim sisteminin sistem fonksiyonları ile yapılmaktadır. Ancak bu sistem fonksiyonlarını çağıran çeşitli kütüphaneler ve arayüzler oluşturulmuştur. C++'ın thread ktüphanesi de neticede aslında i,şletim sisteminin sistem fonksiyonlarıı çağırmaktadır. Ancak yukarıda da sözünü ettiğimiz gibi C++'ın standart kütüphanesi bu bağlamda "portable" bir arayüz sunmaktadır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ /*------------------------------------------------------------------------------------------------------------------------------------------------------------- C++'ın thread kütüphanesindeki sınıfların ve fonksiyonların önemli bölümü başlık dosyası içerisidedir. Thread'ler thread isimli sınıf yoluyla yaratılmaktadır. thread sınıfı türünden bir nesne yaratıldığında thread de yaratılmış olur. Thread nesnesi yaratılır yaratılmaz thread akışı da başlatılmaktadır. Bir thread yaratıldığında thread akışı belli bir fonksiyondan başlatılmaktadır. İşte thread yaratılırken programcı thread akışının başlatılacağı fonksiyonu da verir. Thread fonksiyonu herhangi bir prototipe sahip olabilir. Zaten threda sınıfı da fonksiyonları şablon olan bir sınıftır. thread sınıfının kendisi şablon sınıf değildir. Ancak fonksiyonlarının bazıları şablon fonksiyonlardır. Bir thread nesnes yaratıldıktan sonra mutlaka bu nesne ile join ya da detach üye fonksiyonlarının çağrılması gerekir. Eğer bu fonksiyonlar çağrılmazsa thread nesnesi faaliyet alanını bitirdiğinde thread sınıfının yıkıcı fonksiyonu std::terminate fonksiyonunu çağırır. Bu fonksiyon da std::abort fonksiyonunu çağırarak prosesi sonlandırır. Aşağıda thread yaratma işlemine bir örnek verilmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include using namespace std; void thread_proc() { cout << "thread is running..." << endl; } int main() { thread t(thread_proc); //... t.join(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Aşağıdaki örnekte bir therad akışı yaratılmış ve sonra hem main thread'te hem de yaralıan thread'te birer saniye beklenerek birlikte çalışma sağlanmışır. Bir saniye beklemek için C++11 ile C++'a eklenen chrono kütüphanesinden (duration isimli sınıftan) faydalanılmıştır. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; using namespace std::chrono; void thread_proc() { for (int i = 0; i < 10; ++i) { cout << "mythread: " << i << endl; this_thread::sleep_for(milliseconds(1000)); } } int main() { thread t(thread_proc); for (int i = 0; i < 10; ++i) { cout << "main thread: " << i << endl; this_thread::sleep_for(milliseconds(1000)); } t.join(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- thread sınıfının yapıcı fonksiyonları şablon fonksiyon durumundadır. Dolayısıyşla programcı thread fonksiyonu olarak herhangi bir fonksiyonu geçirebilir. Hatta örneğin fonksiyonu taklit eden (function object) yani fonksiyon çağırma operatör fonksiyonu bulunan bir sınıf nesnesini de bu bağlamda kullanabilmektedir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include using namespace std; using namespace std::chrono; class MyThreadClass { public: void operator ()() { for (int i = 0; i < 10; ++i) { cout << "mythread: " << i << endl; this_thread::sleep_for(milliseconds(1000)); } } }; int main() { MyThreadClass mtc; thread t(mtc); for (int i = 0; i < 10; ++i) { cout << "main thread: " << i << endl; this_thread::sleep_for(milliseconds(1000)); } t.join(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- Thread akışının başlatılacağı fonksiyon parametreli olabilir. Bu durumda parametreler thread sınıfının yapıcı fonksiyonunda thread fonksiyonu belirtildikten snra bir liste biçiminde beirtilir. Bunu sağlamak için C++11 ile birlikte "variadic template" konusu standartlara eklenmiştir. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include using namespace std; using namespace std::chrono; void thread_proc(string name) { for (int i = 0; i < 10; ++i) { cout << name <<':' << i << endl; this_thread::sleep_for(milliseconds(1000)); } } int main() { thread t(thread_proc, "my thread"); for (int i = 0; i < 10; ++i) { cout << "main thread: " << i << endl; this_thread::sleep_for(milliseconds(1000)); } t.join(); return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- join isimli üye fonksiyon thread akışı bitene kadar onu çağıran thread'i bloke ederek bekletir. Yani biz join fonksiyonunu çağırdığımızda eğer thread henüz bitmemişse bitene kadar join içerisinde beklemiş oluruz. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include using namespace std; using namespace std::chrono; void thread_proc(string name) { for (int i = 0; i < 10; ++i) { cout << name <<':' << i << endl; this_thread::sleep_for(milliseconds(1000)); } } int main() { thread t(thread_proc, "my thread"); t.join(); cout << "ok" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- detach fonksiyonu thread akışı ile nesne arasında ilişkiyi kesmektedir. Yani detach yaptığımızda thread akışı devam eder ancak thread nesnesi ile bu akışın bir ilgisi kalmaz. Yani biz yaratttığımız thread akışının sahipliğini bırakmış oluruz. Aşağıdaki kodda thread'in ekrana yazacağı yazılar görülmeeyecektir. Bunun nedenini sonraki kısımda ele alacağız. --------------------------------------------------------------------------------------------------------------------------------------------------------------*/ #include #include #include #include using namespace std; using namespace std::chrono; void thread_proc(string name) { for (int i = 0; i < 10; ++i) { cout << name <<':' << i << endl; this_thread::sleep_for(milliseconds(1000)); } } int main() { thread t(thread_proc, "my thread"); t.detach(); cout << "ok" << endl; return 0; } /*------------------------------------------------------------------------------------------------------------------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------------------------*/