/*----------------------------------------------------------------------------------------------------------------------
                                                
                                                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 İle 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: 08/04/2025 - Salı
                              
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C Programlama Dili 1971-1972 yıllarında Dennis Ritchie tarafından AT&T Bell Lab'ta UNIX işletim sisteminin bir yan 
    ürünü olarak geliştirilmişir. AT&T Bell Lab. Ken Thompson, Brian Kernighan gibi önemli kişilerle UNIX isimli işletim 
    sistemini geliştiriyordu. UNIX işletim sistemi o zamanın DEC PDP-8  makineleri için yazılıyordu. İlk UNIX sistemleri 
    ağırlıklı sembolik makine dilinde yazılmıştır. Ancak yazımı kolaylaştırmak için Ken Thompson'ın B ismini verdiği 
    programlama dilinden (dilciğinen de diyebiliriz) de faydalanılmıştır. İşte Dennis Ritchie bu B programlama Dilini 
    geliştirerek C haline getirmiştir. UNIX işletim sistemi 1973 yılında tamamen C kullanılarak yeniden yazılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    1978 yılında Dennis Ricthie ve Brian Kernighan C'yi tanıtan "The C Programming Laanguage" isimli bir kitap yazdılar. 
    Daha sonra bu kitabın 1987 yılında 2. Baskısı (Second Edition) oluşturuldu. Nihayet 1989 yılında C Programlama Dili 
    ANSI (American National Standard Institute) tarafıında standardize edildi. C'nin ANSI standartları "ANSI C" olarak 
    bilinmektedir ve kısaca bu standartlara C89 denilmektedir. C'de 1989 öncesi devire "standart öncesi devir" denir. 
    1990 yılında ISO kurumu C'nin ANSI standratlarını alarak bölüm numaralandırmalarını değiştirip tasdik etmiştir. Bu 
    standartlar "ANSI/ISO 9899:1990" kod numarasıyla basılmıştır. Bu standarda halk arasında C90 denilmektedir. C Programlama 
    Dili 90'lı yıllarda dünyanın en popüler ve yaygın programlama dili haline gelmiştir. ISO 1999 yılında C'ya bazı kurallar 
    ekleyerek yeni bir standart oluşturdu. Bu standartlar da "ISO/IEC 9899:1999" kod numarasını verdi. Bu standartlar da 
    halk arasında kısaca C99 olarak ifade edilmektedir. Daha sonra ISO yine C'ya bazı eklemeler yaparak 2011 standartlarını 
    oluşturdu. Bu standartların kod numarası "ISO/IEC 9899:2011" biçimindedir. Bu standartlar da kalk arasında C11 olarak 
    ifade edilmektedir. Nihayet ISO 2017 yılında yeni bir standart oluşturdu. Ancak bu standart C11'in bir düzeltmesi 
    biçimindedir. Şu anda C'nin en son standardı "ISO/IEC 9899:2017" kod numarasıyla tasdik edilen biçimidir. Bu da halk 
    arasında C17 olarak ifade edilmektedir. C'nin son standardı olan C23 üzerinde çalışılmaktadır. Halen tam olarak tasdik 
    edilmemiştir. Bu standartlar 2023 yılında basılamadığı için artık C2Y olarak isimlendirilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C++ Programlama Dİli C Programlama Dİlinin nesne yönelimli bir biçimi olarak düşünülebilir. Tamamen olmasa da C++ C'yi 
    kapsamaktadır ve fazlalıkları vardır. C++'a bu fazlalıklar "Nesne Yönelimli Programlama Tekniğini (NYPT)" uygulamak 
    için eklenmiştir. Derneğimizde C++ Programlama Dilinin eğitimi C bilenlere yönelik olarak verilmektedir. 

    C++ Programlama Dilinin standart gelişimi de şöyledir:

    - ISO/IEC 14882: 1998 (C++98)
    - ISO/IEC 14882 :2003 (C++03)
    - ISO/IEC 14882 :2011 (C++11)
    - ISO/IEC 14882 :2014 (C++14)
    - ISO/IEC 14882 :2017 (C++17)
    - ISO/IEC 14882 :2020 (C++20)
    - ISO/IEC 14882 :2020 (C++23 Henüz basılmadı)
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bilgisayar donanımını yöneten, donanım ile kullanıcı arasında arayüz oluşturan temel sistem programlarına "işletim 
    sistemi (operating system)" denilmektedir. İşletim sistemleri iki katman olarak düşünülebilir. Çekirdek (kernel) 
    işletim sisteminin donanımı yöneten motor kısmıdır. Kabuk (shell) ise kullanıcı ile arayüz oluşturan kısmıdır. İşletim 
    sistemleri çeşitli bakımlardan sınıflandırılabilir. Örneğin:

    - Tek prosesli işletim sistemleri
    - Çok prosesli işletim sistemleri
    - Gerçek zamanlı işletim sistemleri
    - Masaüstü (desktop) ve mobil işletim sistemleri
    - Server işletim sistemleri

    Bugün masaüstü işletim sistemlerinin en yaygın kullanılanı Windows sistemleridir (75 civarı). Bunu macOS izlemektedir 
    (22 civarı) bunu da Linux izlemektedir. (%2.5 civarı). Mobil işletim sistemlerinin en yaygın olanı ise %70 civarında 
    kullanıma sahip olan Android sistemleridir. Bunu %25 civarlarında kullanıma sahip olan IOS sistemleri izlemektedir.
    Diğer mobil işletim sistemleri %1'in oldukça altındadır. 

    Linux sistemleri Server dünyasında en yaygın kullanılan işletim sistemleridir. Server sistemlerinin %70 civarı Linux 
    makinelerden oluşmaktadır. Artık pek gömülü sistem projelerinde de Linux işletim sistemi kullanılmaktadır.

    Bazı işletim sistemleri sıfırdan yazılmıştır. Bazı işletim sistemleri ise mevcut işletim sistemlerinin kodlarıdan 
    faydalanılarak oluşturulmuştur. Örneğin Android büyük ölçüde Linux çekirdeğinin kodlarına sahiptir. Windows, Linux 
    özgün kod temeline sahip olan işletim sistemleridir. Eskiden BSD sistemleri özgün değildi. Sonra tamamen sıfırdan 
    yeniden yazıldı. Benzer biçimde Solaris gibi sistemlerde sıfırdan yazılmıştır. Anak macOS sistemleri böyle değildir. 
    macOS sistemleri FreeBSD ve Mach isimli çekirdeklerin birleşimiyle oluşturulmuştur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir bilgisayar sisteminde en önemli üç birim "CPU", "RAM" ve "Disk" birimleridir. Bütün işlemler CPU (Central Processing 
    Unit) tarafından yapılır. CPU işlemlerin yapıldığı kavramsal birimdir. Bunun entegre devre biçiminde üretilmiş haline 
    "mikroişlemci (microprocessor)" denilmektedir. CPU'nun elektiriksel olarak bağlantılı olduğu belleklere "ana bellek 
    (main memory)", ya da "Birincil bellek (primary memory)" denilmektedir. Ana belleklere ise halk arasında RAM denilmektedir. 
    Programlama dillerindeki değişkenler program çalışırken RAM'de bulunurlar. Ancak işlemler CPU tarafından yapılır. 
    Örneğin:

    a = b + c;

    gibi bir işlemde aslında a, b, ve c RAM'de bulunmaktadır. Bu işlem yapılırken b ve c CPU'ya çekilir. CPU içerisindeki 
    elektrik devreleri toplama işlemini yapar. Sonuç RAM'deki a'ya aktarılır. Bilgisayarın güç kaynağı kapatıldığında 
    RAM'deki bilgiler silinmektedir. Bunun için bu bilgilerin daha kalıcı bir bellekte saklanması gerekir. Bu tür belleklere 
    "ikinci bllekler (secondary storage device)" denilmektedir. Eskiden ikincil bellek olarak floppy disketler, CD/DVD 
    ROM'lar ve hard diskler kullanılıyordu. Ancak günümüzde artık SSD (solid State Disk) denilen "flash bellekler" 
    kullanılmaktadır. Genellikle bilgisayar sistemlerinde ikincil belleklerle birincil bellekler arasında bir aktarım 
    yolu bulunmaktadır. Bu aktarım yardımcı işlemciler (bunlara DMA (Dynamic Memory Access) denilmektedir) tarafından 
    yapılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C doğal kodlu bir çalışma sistemi için tasarlanmıştır. Biz C'de bir kod yazıp bunu derlediğimizde ve bağladığımızda 
    çalıştırılabilir (executable) bir dosya elde ederiz. Bu dosyanın içerisinde o anda çalışmakta olduğumuz mikroişlemcinin 
    doğrudan çalıştırabileceği makine komutları bulunur. Yani C'de yazdığımız ve derlediğimiz program mikroişlemci tarafından 
    doğrudan çalıştırılmaktadır.

    Ancak 1990'lı yılların ortalarında Java ortamıyla (Java framework) birlikte ve sonra da 2002 yılında .NET ortamıyla 
    birlikte "arakodlu çalışma sistemi" yaygınlaşmaya başlamıştır. Bu sistemde derleyicilerin ürettiği kodlar gerçek bir 
    mikroişlemcinin makine kodları değildir. Kendi içerisinde belli bir standardı olan ancak hiçbir mikroişlemcinin makine 
    kodu olmayan yapay bir ara koddur (intermediate code). Dolayısıyla bu arakod mikroişlemci tarafında çalıştırılamaz. 
    İşte bu arakodlar çalıştırılmak istendiğinde bu ortamların (frameworkds) bir alt sistemi devreye girmekte ve bu arakodları 
    o anda gerçek makine komutlarına dünüştürüp çalıştırmaktadır. Bu sürece "tam zamanın derleme (just-in time compilation)" 
    denilmektedir. 

    Java ismi hem bir ortam (framework) belirtmekte hem de bir programlama dili belirtmektedir. Oysa .NET platformun ismi 
    C# ise programlama dilinin ismidir. Java Programlama Dilinde yazılmış olan kodun derlenmesiyle elde edilen ara koda 
    "Java Byte Code" denilmektedir. Benzer biçimde C# ile yazılmış kodun derlenmesiyle elde edilen arakoda ise "Common 
    Intermediate Language (CIL)" denilmektedir. Her iki ortamda da bu kodlar doğrudan değil bu ortamların alt sistemleri 
    tarafından çalıştırılmak istedniğinde belli bir düzen içerisinde o anda gerçek komutlarına dönüştürülmektedir. Tabii 
    böyle bir arakod sistemi JIT derlemesi nedeniyle doğal kodlu sistemlere göre daha yavaş bir çalışma sunmaktadır. 
    Microsoft kendi .NET sistemi için buradaki zaman kaybının %18 civarında olduğunu belirtmektedir.

    C'de yazılmış ve derlenmiş olan bir program hem işletim sistemine hem de işlemciye bağımlıdır. Yani biz Windows 
    sistemlerinde x86 serisi Intel işlemcilerinin bulunduğu bir bilgisayareda yazdığımız ve derlediğimiz kodu Linux'ta 
    çalıştıramayız. İşte Java gibi .NET gibi ortamlarda yazılmış ve derlenmiş olan kodlar işletim sisteminden ve işlemciden 
    bağımsız arakoda dönüştürülmektedir. Böylece bu ortamlar çeşitli işletim sistemi ve mikroişlemci mimarileri için 
    yazılmış olduğundan Java ve .NET ortamları için yazılan programlar "platform bağımsız" bir biçimde her yerde 
    çalışabilmektedir. 

    Java ve .NET gibi ortamlara İngilizce "framework" denilmekltedir. Platform sözcüğü İngilizce daha çok "işletim sistemi 
    ve işlemcinin" oluşturduğu küme için söylenmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Taşınabilirlik (portability) eski bir terimdir ve hala kullanılmaktadır. Taşıanbilirlik denildiğinde "kaynak kodun 
    taşınabilirliği" anlaşılmaktadır. Bu bağlamda taşınabilirlik "yazılmış olan kaynak kodun başka sistemlere götürüldüğünde
    sorunsuz derlenmesi ve aynı biçimde çalışması" anlamına gelemektedir. Örneğin C'nin taşınabilir bir dil olması demek 
    C programlarının standart bir dilde yazıldığından her C derleyicisinin bunu kabul etmesi demektir. Ancak son 30 yıldır 
    derlenmiş olan programların taşınabilirliği biçiminde "binary portability" terim de kullanılmaya başlanmıştır. Derlenmiş 
    programın taşınabilirliği onun başka platformlara götürüldüğünde sorunsuz çalışabilmesi anlamına gelmektedir. Java 
    gibi .NET gibi rtamlar bunu hedeflemektedir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir programlama dilinde yazılmış olan bir programı eşdeğer olarak başka bir dile dönüştüren araçlara "çevirici programlar 
    (translators)" denilmektedir. Çevirici programlarda çevrilecek dile "kaynak dil (source language)" çevrime işleminin 
    sonunda elde edilen programın diline ise "hedef dil (target language)" denilmektedir. Hedef dili alçak seviyeli olan 
    çevirici programlara ise "derleyici (compiler)" denilmekltedir. Saf makine dilleri, sembolik makine dilleri ve ara 
    kodlar alçak seviyeli dillerdir. 

    Yorumlayıcılar (interpreters) kaynak kodu okuyup hiç hedef kod üretmeden doğrudan çalıştıran programlardır. Dolayısıyla 
    yorumlayıcılar aslında çevirici programlar değildir. Bazı dillerde yalnızca derleyicilerle çalışılır (örneğin C, C++). 
    Bazı dillerde ise yalnızca yorumlayıcılar bulunmaktadır. (Örneğin Ruby, R gibi). Bazı dillerde ise hem derleyiciler 
    hem de yorumlayıcılarla programlar çalıştırılabilir (örneğin Basic gibi). (Biz İngilizce "interpreter" sözcüğünü Türkçe
    "yorumlayıcı" biçiminde çeviriyoruz. Aslında İngilizce "interpreter" sözcüğü "translator" sözcüğü dikkate alınarak 
    "mütercim tercüman" anlamında uydurulmulmuştur.)

    Derleyici yazmak yorumlayıcı yazmaktan daha zordur. Derleyiciler ile yazılan kod genel olarak daha hızlı çalıştırılmaktadır. 
    Yorumlayıcılarla çalışırken biz kaynak kodu gizleyemeyiz. Ancak derleyicilerle çalışırken bir üretilen makine kodlarını 
    karşı tarafa verebiliriz. 

    Eğer bir derleyici kendisinin çalıştığı işlemciden farklı bir işlemci için kod üretiyorsa o tür derleyicilere 
    "çapraz derleyiciler (cross compilers)" denilmektedir. Örneğin x86 işlemcilerinin bulunduğu Windows sistemlerinde 
    çalışan bir C derleyicisi eğer örneğin PIC işlemcileri kod üretiyorsa bu bir çapraz derleyicidir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Kendisi bir bilgisayar olmayıp asıl amacı başka işlemleri yapmak olan aygıtlardaki bilgisayar devrelerine "gömülü 
    sistemler (embedded systems)" denilmektedir. Örneğin ölçü aletlerinin, kapı güvenlik sistemlerinin, turnike geçiş 
    sistemlerinin, çamaşır makinelerinin, buzdolaplarının, fırınların, kahve makinelerinin içerisindeki bilgisayar sistemlerini
    gömülü sistemlere örnek olarak verebiliriz. Gömülü sistemlerdeki bilgisayar devreleri genel olarak düşük güç harcayan, 
    düşük kapasiteli, ancak ucuz olma eğilimindedir. Mikrodenetleyiciler bu tür gömülü sistemlerde yoğun biçimde kullanılmaktadır. 
    Dolayısıyla gömülü sistemler dünyasında aşağı seviyeli bir çalışma söz konusu olduğu için C Programlama Dili de bu 
    sistemlerde yoğun olarak kullanılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Dil (language) iletişimde kullanılan semboller kümesidir. Dil karmaşık bir olgudur. Bir olgunun dil olarak değerlendirilmesi 
    için iki kural topluluğunun o olguda bulunyor olması gerekir: Sentaks ve smantik. Bir dilin en yalın öğelerine "atom 
    (token)" denilmektedir. Örneğin doğal dillerde atomlar zözcüklerdir. İşte sentaks atomların doğru yazılmasına ve doğru 
    dizilmesine ilişkin kurallardır. Örneğin:

    I going to am school

    Burada bir sentakshatası yapılmıştır. Buradaki atomlar uygun bir biçimde yan yana getirilmemiştir. Burada yapılan hata 
    aşağıdaki C kodunda yapılan hataya tamamen benzemektedir:

    if a > ( 10 ) printf("Ok");

    Sentaks bakımından doğru olan atom dizilimlerinin ne anlam ifade ettiğine ilişkin kurallara "semantik" denilmektedir. 
    Sentaks ve semantik kurallara sahip her olguya dil denilmektedir. Örneğin HTML'de bir senataks vardır. Oluşturulan 
    tag'ların bir anlamı da vardır. O zaman HTML bir dildir. 

    Diller doğal diller ve kurgusal diller olmak üzere iki ayrılır. Doğal diller Türkçe gibi İngilizce gibi doğal yaşam 
    sonıcında oluşmuş dillerdir. Doğal dillerde sentaksın matematiksel düzeyde kesin ifade edilmesi mümkün değildir. 
    Çünkü doğal dillerde çok istisnalar vardır. Kurgusal diller insanların belli bir mantık çerçevesinde belli bir amaç 
    doğrultusunda tasarladığı dillerdir. Bunların sentaksları kesindir. İki anlamlılık ve istisna çok yoktur ya da çok 
    azdır. Bilgisayar alanında kullanılan kurgusal dillere "bilgisayar dilleri (computer languages)" denilmektedir. Bir 
    bilgisayar dilinde bir akış varsa ona aynı zamanda "programlama dili (programming language)" denilmektedir. Örneğin 
    HTML bir bilgisayar dilidir. Ancak bir programlama dili değildir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Programlama dilleri çeşitli biçimlerde sınıflandırılabilmektedir. En çok kullanılan sınıflandırma biçimleri şunlardır:

    1) Seviyelerine göre sınıflandırma
    2) Uygulama alanlarına göre sınıflandırma
    3) Programlama modeline göre sınıflandırma
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Seviye (level) bir programalama dilinin insan algısına yakınlığının bir ölçüsüdür. Yüksek seviyeli diller insana yakın 
    alçak seviyeli diller makineye yakın dillerdir. Seviyelerine göre diller yüksekten alçağa kategorik olarak genellikle 
    şöyle sınıflandırılmaktadır:

    - Çok Yüksek Seviyeli Diller 
    - Yüksek Seviyeli Diller
    - Orta Seviyeli Diller
    - Sembolik Makine Dilleri
    - Saf Makine Dilleri ve Arakodlar

    C ortra seviyeli (middle level) bir programlama dilidir. Ancak Java, C#, Python gibi diller yüksek seviyeli diller 
    olarak gruplanmaktadır. Çok yüksek seviyeli dillerde artık algoritma da ortadan kalkmaktadır. Genellikle bu tür diller 
    "belli bir alana yönelik (domain specific)" biçimdedirler. Saf makike dilleri ve arakodlar 1'lerden ve 0'lardan oluşmaktadır. 
    Bunların sembolik biçimlerine "sembolik makine dilleri (assembly languages)" denilmektedir. Sembolik makine dilleri, 
    saf makine dilleri ve arakodlara da "alçak seviyeli diller" denir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Programlama dillerinin uygulama alanlarına göre sınıflandırılması "hangi tür uygulamalar için hangi dillerin daha 
    uygun bir araç olacağı" ile ilgilidir. Bu bakımdan pek çok alt sınıflandırma yapılabilmektedir. Aşağıda birkaç önemli 
    alt sınıf verilmiştir:

    - Bilimsel ve Mühendislik Diller (Scientific and Enginnering Languages): Fortran, C, C++, Pascal, Java, C#, ...
    - Veritabanı Dilleri (Database Languages): SQL, Clipper, ...
    - Web Dilleri (Web Languages): Java Script, PHP, Ruby, Java, C#, Python, ...
    - Yapay Zeka Dilleri (Artificial Intelligence Langauges): Lisp, Prolog, Python, C, C++,...
    - Görsel ve Animasyon Dilleri (Visual and Animation Languages): Action Script, ...
    - Sistem Proramlama Dilleri (System Programming Languages): C, C++, Sembolik Makine Dilleri, Rust, Go
    - Genel Amaçlı Diller (General Purpose Languages): C, C++, Java, C#, Pascal, ...

    C Programlama Dİli bilimsel ve mühendislik alanlarda kullanılan, genel amaçlı, uzmanlığı sistem programlama olan bir 
    dildir. 

    Sistem programlama "bilgisayar donanımı ile arayüz oluşturan, uygulama programlarına çeşitli bakımlardan hizmet veren, 
    aşağı seviyeli temel yazılımların oluşturulması amacıyla yapılan programlama faaliyetlerine denilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Programlama modeli (programming paradigm) programlama yaparken kullandığımız genel yöntemleri ve biçimleri anlatan bir 
    kavramdır. Programlama dilleri belli programlama modellerini uygulayabilmek için özel tasarlanmıştır. Bu bakımdan dilleri 
    tipik olarak aşağıdaki gibi sınıflara ayırabiliriz:

    - Prosedürel Diller (Procedural Languages): Bu dillerde programlar altprogramların (bunlara fonksiyon, prosedür ya da 
    "subroutine" de denilmektedir) birbirlerini çağırması ile oluşturulmaktadır. Fortran, C, Basic, Pascal gibi 90 öncesi 
    klasik programlama dillerinin büyük bölümü böyledir. 

    - Nesne Yönelimli ve Nesne Tabanlı Diller (Object Oriented Languages): İçerisinde sınıf kavramı geçen diller. Bunlar 
    programların sınıflar kullanılarak yazılmabilmesine olanak sağlamaktadır. 

    - Fonksiyonel Diller (Functional Languages): Bunlar adeta "formül yazar gibi program yazmaya olanak sağlayan" dillerdir. 
    Aslında bu diller de kendi aralarında bir spektrum oluşturmaktadır. Fonksiyonel dil demekle genellikle yüksek oranda 
    fonksiyonal olan (pure functional) diller kastedilmektedir. 

    - Imperative Diller (Imperative Languages): Programların deyim deyim çalıştırıldığı mantıksal, görsel ve fonksiyonel 
    dillerin dışındaki diller genel olarak imperative diller olarak da bilinmektedir. 

    - Mantıksal Diller (Logical Languages): Mantıksal ifadelerin ve sonuç çıkartma işlemlerinin yoğun kullanıldığı dillerdir. 
    Lisp ve Prolog gibi.

    - Görsel Diller (Visual Languages): Fare hareketleriyle tamamne görsel biçimde program akışının oluşturulduğu çok 
    yüksek seviyeli dillerdir. Programlama eğitiminde kullanılan Scratch buna örnek verilebilir.

    - Çok Modelli Diller(Multiparadigm Languages): Yukarıdaki modellerden birden fazlasını belli ölçülerde destekleyen 
    dillerdir. Örneğin C++'ta biz C gibi kod yazabiliriz. Ama sınıflar kullanarak da kod yazabiliriz. Fonksiyonel bazı 
    dil özelliklerini C++11'den sonra C++'ta kullanabilmekteyiz. O zaman C++ çok modelli bir programlama dilidir. Yeni 
    tasarlanan diller zaten genel olarak çok modelli olma eğilimindedir. Bu yeni diller hem nesne yönelimli hem de
    fonksiyonel özellikleri bünyesinde barındırmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C Programalama Dili orta seviyeli, prosedürel, imperative, genel amaçlı, bilimsel ve mühendislik çalışmalarda kullanılan 
    ancak uzmanlık alanı "sistem programlama" olan bir dildir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Uygulama geliştirmeyi kolaylaştıran, kendi içerisinde editörü olan, menüleri olan, genellikle debugger'ları olan, başka 
    birtakım araçları bulunan	yazılımlara "IDE (Integrated Developmen Environment)" denilmektedir. IDE derleyici değildir. 
    IDE derleyiciyi barındırmaz. IDE'de derleme işlemi yapılırken IDE derleyiciyi dışarıdan çalıştırmaktadır. Derleyiciler 
    genel olarak komut satırında çalışan GUI arayüzü olmayan araçlar biçiminde oluşturulmaktadır: 

    C için kullanılabilecek önemli IDE'ler şunlardır:

    - Windows sistemleri için Microsoft'un "Visual Studio" IDE'si. Kursumuzda bunun "Community-2022" parasız versiyonunu 
    kullanacağız.
    
    - QtCreator IDE'si. Cross platform bir IDE'dir. Aslında "Qt" denilen framework için yazılmıştır. Ancak genel amaçlı 
    C/C++ IDE'si olarak da kullanılabilmektedir. macOS ve Linux sistemlerinde iyi bir alternatiftir.
    
    - Eclipse IDE'si. Bu IDE bir Java IDE'si olarak çıkmıştı. Ondan sonra pek çok programlama dili için "plugin" yöntemiyle 
    kullanılabilmeye başlandı. Bu IDE'nin C/C++ versiyonu doğrudan indirilebilir. 
    
    - CLion IDE'si. Bu JetBrains firmasının C/C++ IDE'sidir. Ancak paralıdır ve community versiyonu yoktur. 
    
    - Visual Studio Code IDE'si Aslında bu yazılım IDE ile editör arasında bir yerdedir. Ancak cross platform özelliği 
    vardır. Kullanımı biraz dah zahmetli olsa da çok az yer kaplamaktadır (lightweight IDE). Plug-in'lerle (extensions)
    işlevleri genişletilebilmektedir.
    
    - XCode IDE'si. Bu IDE Apple firmasının temel IDE'sidir. Dolayısıyla adeta Visual Studio IDE'sinin Apple versiyonu 
    gibi düşünülebilir. Her ne kadar Apple'ın temel dili Swift ve Objective C olsa da XCode C/C++ çalışmasını 
    desteklemektedir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                5. Ders - 07/06/2022 - Salı 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Biz 10'luk sistemi (decimal system) kullanmaktayız. 10'luk sistemde sayıları ifade etmek için 10 sembol vardır:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9

    10'luk sistemde sayının her bir basamağı 10'nun kuvvetleriyle çarpılıp toplanmaktadır. Örneğin:

    123.25 = 3 * 10^0 + 2 * 10^1 + 1 * 10^2 + 2 * 10^-1 + 5 * 10^-2

    Halbuki bilgisayarlar 'lik sistemi (binary system) kullanmaktadır. ikilik sistemde sayıları ifade etmek için 2 sembol 
    kullanılmaktadır:

    0 
    1

    ikilik sistemde sayının her bir basamağına "bit ("binary digit" sözcüklerinden kısaltma)" denilmektedir. ikilik sistemde 
    sayının her basamağı 2'nin kuvvetiyle çarpılarak sayı elde edilir. Bit en küçük bellek birimidir. 8 bite 1 byte denilmektedir.
    Genellikle bitler 4'erli gruplanarak yazılırlar. Örneğin:

    1010 0010

    Burada 1 byte'lık bir bilgi vardır. Byte temel bellek birimidir. 

    Byte da küçük bir birimdir. Kilo diğer bilimlerde "1000 katı" anlamına gelmektedir. Ancak bilgisayarlar ikilik sistemi 
    kullandığj için 1000 katı iyi bir kat değildir. Bu nedenle genellikle olarak Kilo byte için 2'nin 10'uncu kuvveti olan 
    1024 kat kullanılır. Yani 1KB (kısaca 1K) 1024 byte'tır. Mega diğer bilimlerde kilonun 1000 katıdır. Dolayısıyla milyar 
    kat anlamına gelmektedir. Ancak bilgisayar bilimlerinde genellikle mega kilonun 1024 katı olarak alınır. Bu durumda 
    1 MB = 1020 * 1024 (2^20) KB'dir. Giga ise meganın 1024 katıdır. Bu durumda 1 GB = 1024 * 1024 * 1024 byte'tır ( 2^30). 
    Giga'dan sonra tera, tera'dan sonra peta, ondan sonra da exa gelmektedir. Bazı kaynaklar kilo, mega ve giga byte 
    terimlerini	diğer bilimlerde olduğu gibi 10^3, 10^6 ve 10^9 biçiminde kullanmaktadır. Bu konudaki karışıklık daha 
    sonraları "International System of Units (SI)" tarafından giderilmeye çalışılmıştır. SI'ye göre artık 1 Kilobyte 1000
    byte, 1 Kikibyte ise 1024 byte'tır. Ancak Microsoft gibi bazı kurumlar Kilobyte tanımını 1024 olarak kullanmaya devam 
    etmektedir.

    1 byte içerisinde yazılabilecek en küçük ve en büyük sayılar şöyledir:

    0000 0000 ---> 0
    1111 1111 ---> 255

    1 byte içerisinde 1 ve 0'ların bütün permütasyonları 256 tanedir. 2 byte içerisinde en büyük sayıyı yazacak olsak şöyle 
    olurdu:

    1111 1111 1111 1111 ---> 65535

    Biz burada ikilik sistemde tamsayıları ifade ettik. Ama bütün sayıları pozitif kabul ettik. Pekiyi negatif tamsayılar 
    nasıl ifade edilmektedir?

    Bugün negatif sayıların ifade edilmesi için "ikiye tümleyen (two's complement)" sistemi denilen bir sistem kullanılmaktadır. 
    Bu sistemde pozitif ve negatif sayılar birbirlerinin ikiye tümleyenidirler. ikiye tümleyen bire tümleyene bir eklenerek 
    elde edilmektedir. Bir sayının bire tümleyeni sayıdaki 0'ların 1, 1'lerin 0 yapılmasıyla elde edilir. Bu durumda ikiye 
    tümleyen şöyle hesaplanır. Örneğin aşağıdaki sayının ikiye tümleyenini bulmaya çalışalım:

    0101 0110

    Sayının bire tümleyenine bir ekleyeceğiz. Sayının bire tümöleyeni şöyledir:

    1010 1001

    Şimdi ona 1 ekleyelim:

    1010 1001
    0000 0001
    ---------
    1010 1010

    Aslında ikiye tümleyeni bulmanın kolay bir yolu da vardır: Sayıda sağdan sola ilk 1 görene ilk 1 dahil olmak üzere aynısı 
    yazılarak ilerlenir. Sonra 0'lar 1, 1'ler 0 yapılarak devam edilir. Örneğin:

    0101 0110

    sayının ikiye tümleyenini tek hamlede bulalım:

    10101010

    Negatif sayıları ifade edebilmek için kullanılan ikiye tümleme sisteminde en soldaki bir işaret bitidir. Bu bit 0 ise 
    sayı pozitif, 1 ise negatiftir. Negatif ve pozitif sayılar birbirlerinin ikiye tümleyenidir. Örneğin bu sistemde +10 
    yazmak isteyelim. Bunu işaret bitini 0 yaparak yazabiliriz:

    0 000 1010		---> +10

    Şimdi -10 yazmak isteyelim. Bunun için +10'un ikiye tümleyenini alalım:

    1 111 0110       ---> -10

    Bu sistemde +n ile -n toplandığında 0 elde edilir:

    0 000 1010   ---> +10
    1 111 0110   ---> -10
    ---------------------
      0 000 0000   ---> 0

      Bu sistemde tek bir sıfır vardır. O da tüm bitleri 0 olan sıfırdır. Bu sistemde 1 byte içerisinde yazılabilecek en büyük 
    pozitif sayı şöyledir:

    0 111 1111    ---> +127

    Şimdi bunun ikiye tümleyenini alalım:

    1 000 0001    ---> -127

    Pekiyi bu sistemde en küçük negatif sayı nedir? Bu sistemde bir tane sıfır olduğuna göre 255 tane permütasyon eşit bölünemez. 
    Demek ki ya pozitif sayılar ya negatif sayılar bir tane daha fazla olmak zorundadır. Bu sistemde ikiye tümleyeni olmayan iki 
    sayı vardır:

    0000 0000 
    1000 0000

    Birincisi 0'dır. İkinci sayı -127'den bir eksik olan sayıdır. O halde bu sayının -128 kabul edilmesi daha uygundur. 

    Demek ki bu sistemde n byte içerisinde yazılabilecek en büyük pozitif sayı ilk biti 0 olan diğer tüm birleri 1 olan sayıdır. 
    En küçük negatif sayı ise ilk biti 1 olan diğer tüm bitleri 0 olan sayıdır. Örneğin bu sistemde iki byte ile yazabileceğimiz 
    en büyük pozitif sayı şöyledir:

    0111 1111 1111 1111		---> +32767

    En küçük negatif sayı ise şöyledir:

    1000 0000 0000 000   	---> -32768

    Bu sisteme ilişkin tipik sorular ve yanıtları şöyledir:

    SORU: Bu sistemde +n sayısını nasıl yazarsınız?
    YANIT: En soldaki bit 0 yapılıp n sayısı ikilik sistemde yazılır.

    SORU: Bu sistemde -n nasıl yazarsınız?
    CEVAP: Yazabiliyorsanız doğrudan yazın. Ancak doğrudan yazamıyorsanız önce +n değerini yazın ve ikiye tümleyenini alın. 
    Örneğin bu sistemde -1 yazalım. Önce +1 yazalım:

    0000 0001   ---> +1

    Şimdi bunun ikiye tümleyenini alalım:

    1111 1111   ----> -1

    SORU: Bu sistemde bir sayının kaç olduğu bize sorulsa bunu nasıl yanıtlarız?
    CEVAP: Eğer en soldaki bit 0 ise sayının değeri doğrudan hesplanır. Eğer en soldaki bit 1 ise bu sayının negatif olduğunu 
    gösterir. Bu durumda sayının ikiye tümleyeni alınır. Pozitifinden hareketle negatifi bulunur. Örneğin 1110 1110 sayısı 
    kaçtır? Burada işaret biti 1 olduğuna göre sayı negatiftir. Negatif ve pozitif sayılar birbirlerinin ikiye tümleyenidirler. 
    O zaman bu sayının ikiye tümleyenini alıp pozitifinden faydalanarak sayıyı bulalım:

    0001 0010  ---> +18

    o zaman bize sorulan sayı -18'dir.

    Bu sistemde örneğin 1 byte içerisinde yazılabilecek en büyük pozitif sayıya 1 toplayalım:

    0111 1111   ---> +127
    1000 0000   ---> -128

    Demek ki bu sistemde bir sayıyı üst limitten taşırırsak yüksek bir negatif sayıyla karışılaırız. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Tamsayılar ikilik sistemde "işaretsiz (unsigned)" ya da "işaretli (signed)" sistemde yorumlanabilirler. İşaretsiz 
    sistemde sayının en soldaki biti olarak yorumlanmaz. Sayı herzaman sıfır ya da pozitiftir. İşaretli sistemde ise 
    sayının en solundaki bit işaret bitidir. Sayı ikiye tümleyen aritmetiğine göre yorumlanır. 

    Pekiyi sayının işaretli mi işaretsiz mi olduğuna nasıl karar verilmektedir? Programcı sayıyı tutacağı değişkeni C'de 
    işaretli ya da işaretsiz tamsayı türü olarak belirleyebilir. İşlemciler aslında genellikle işaretli ve işaretsiz 
    ayırımını yapmazlar. Çünkü bu tür de aslında aynı biçimde işleme sokulmaktadır. Sonucun yorumu değişmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi noktalı sayılar ikilik sistemde nasıl ifade edilmektedir? İşte insanlar noktalı sayıları ifade etmek için iki 
    format geliştirmişlerdir. Bunlardan birine "sabit noktalı formatlar (fixed point formats)" diğerine "kayan noktalı 
    formatlar (floating point formats)" denilmektedir. Sabit noktalı formatlar eski devirlerde basit bir mantıkla tasarlanmıştır. 
    Bu formatlar bugün hala kullanılıyor olsa da büyük ölçüde artık bunların çağı kapanmıştır. Bugün kayan noktalı format 
    denilen formatlar kullanılmaktadır. 

    Sabit noktalı formatlarda noktalı sayı için n byte yer ayrılır. Noktanın yeri önceden bellidir. Örneğin sayı 4 byte 
    ile ifade edilsin. Noktanın yeri de tam ortada olsun. Bu durumda syının tam kısmı 2 byte ile noktalı kısmı 2 byte ile 
    ifade edilir. Ancak sayının noktalı kısmı 2'nin negatif kuvvetleriyle kodlanmaktadır. VBöylece iki sabit noktalı sayıyı 
    paralel toplayıcılarla kolay bir biçimde toplayabiliriz: Örneğin bu sistemde 5.25 ile 6.25 sayılarını ifade edip toplayalım:

    0000 0000 0000 0101 . 0100 0000 0000 0000    ---> 5.25
    0000 0000 0000 0110 . 0100 0000 0000 0000    ---> 6.25
    -------------------------------------------------------
    0000 0000 0000 1011 . 1000 0000 0000 0000    ---> 11.5

    Pekiyi bu yöntemin ne dezavantajı vardır? Yöntemin en önemli dezavantajı dinamik olmamasıdır. Yani sayının tam kısmı
    düşükse tam kısım için gereksiz bir yer ayrılmış olur. Benzer biçimde sayının noktadan sonraki kısmı düşükse noktadan
    sonrakşi kısım için gereksiz bir biçimde yer ayrılmış olur.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                6. Ders - 09/06/2022
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Sabit noktalı formatların dinamik olmaması nedeniyle kayan noktalşı formatlar geliştirilmiştir. Bu formatlarda noktanın 
    yeri sabit değildir. Noktanın yeri format içerisinde ayrıca tutulmaktadır. Noktalı sayının noktası yokmuş gibi ifade 
    edilmesi durumunda sayının bu haline "mantis (mantissa)" denilmektedir. İşte kayan formatlarda sayı için ayrılan alanın 
    bir bölümünde mantis bir bölümünde de "noktanın yeri" tutulmaktadır. Noktanın yerini belirleyen kısma "üstel kısım 
    (exponential part)" denilmektedir. Tabii bir de sayının başında işaret biti bulunur. Bu durumda kayan noktalı bir 
    sayının format aşağıdakine benzerdir:

    [işaret biti] [mantis] [noktanın yeri (exponential)]

    Bugün ağırlıklı kullanılan kayan noktalı format IEEE 754 denilen formattır. Bu formatın üç farklı genişlikte biçimi 
    vardır:

    IEEE 754 - Short Real Format (4 byte)
    IEEE 754 - Long Real Format (8 byte)
    IEEE 754 - Extended Real Format (10 byte)
    
    Bugün Intel, ARM, MIPS, Alpha, Power PC gibi yaygın işlemciler donanımsal olarak bu formatı desteklemektedir. Aynı 
    zamanda bu format yaygın olarak Reel Sayı Ünitesi olmayan mikrodenetleyicilerdeki derleyiciler tarafından da kullanılmaktadır.

    Kayan noktalı formatların (örneğin IEEE 754 formatının) en ilginç ve problemli tarafı "yuvarlama hatası (rounding error)" 
    denilen durumdur. Yuvarlama hatası noktalı sayının tam olarak ifade edilemeyip onun yerine ona yakın bir sayının ifade 
    edilmesiyle oluşan hatadır. Yuvarlama hatası sayıyı ilk kez depolarken de oluşabilir, aritmetik işlemlerin sonucunda 
    da oluşabilir. Tabii noktalı sayıların bir bölümü bu formatta hiçbir yuvarlama hatasına maruz kalmadan ifade edilebilmektedir. 
    Ancak bazı sayılarda bu hata oluşabilmektedir. Bu hatayı ortadan kaldırmanın yolu yoktur. Tabii sayı için daha fazla 
    alan ayrılırsa yuvarlama hatasının etkisi de azalacaktır. 

    Yuvarlama hatalarından dolayı programlama dillerinde iki noktalı sayının tam eşitliğinin karşılaştırılması anlamlı 
    değildir. Örneğin aşağıdaki işlemde yuvarlama hatasından dolayı sayılar sanki eşit değişmiş gibi ele alınacaktır.  

    0.3 - 0.1 == 0.2 (false)
    
    Pekiyi yuvarlama hatasının önemli olduüu ve bunun istenmediği tarzda uygulamalarda (örneğin finansal uygulamalarda, 
    bilimsel birtakım uygulamalarda) ne yapak gerekir? İşte bunun tek yolu noktalı sayıları kayan noktalı formatta tutmamak 
    olabilir. Bazı programlama dillerinde noktalı sayıyı kayan noktalı formatta tutmayan böylece yuvarlama hatalarına 
    maruz bırkmayan özel türler (örneğin C#'taki decimal) vardır. Ancak bu türler işlemciler tarafından desteklenmediği 
    için yapay türlerdir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yazılar da aslında bilgisayar belleğinde ikilik sistemde sayılar biçiminde tutulmaktadır. Bir yazıyı oluşturan elemanlara 
    "karakter" denilmektedir. İşte bir yazıda her bir karakter ikilik sistemde bir sayı ile ifade edilir. Böylece yazı 
    aslında ikilik sistemde bir sayı dizisi gibi tutulmaktadır. İşte bir karakter için hangi sayının karşı geldiğini 
    belirten tablolara "karakter tabloları" denilmektedir. Karakter tablosundaki karakter şekillerine "glyph" denilmektedir. 
    Her karaktere tabloda bir sıra numarası verilmiştir. Buna da "code point" denilmektedir. Dünyanın ilk standart karakter 
    tablosu "ASCII (American Standard Code Information Interchange)" denilen tablodur. ASCII tablosu aslında 7 bit bir 
    tablodur. Dolayısıyla tabloda 128 tane glyph için code point bulundurulmuştur. ASCII dışında IBM EBCDIC tablosunu 
    geliştirmiştir. Wang firması WISCII tablosunu kullanmıştır. ASCII tablosu Amerikalılar tarafından yalnızca İngilizce
    karakterleri ifade etmek için oluşturulmuştur. Bilgisayarlar yaygınlaşmaya başladığında farklı karakterlere sahip 
    olan Türkiye gibi, Yunanistan gibi, Almanya gibi ülkeler bu ASCII tablosunu 8 bite çıkartıp elde edilen 128'lik yeni 
    alanı kendi karakterlerini ifade etmek için kullanmışlardır. ASCII tablosunun ilk yarısı (yani [0, 128] numaraları 
    karakterleri) standarttır. Ancak ikinci yarısı "code page" adı altında farklı ülkeler tarafından farklı yerleşimler 
    yapılarak kullanılmaktadır. DOS zamanlarında Türkçe karakterler için OEM 857 denilen code page kullanılıyordu. Daha 
    sonra Microsoft Windows sistemlerinde Türkçe karakterler için 1254 code page'i düzenledi. ISO bu code page'leri standart
    hale getirmiştir. Bugün Türkçe karakterler ISO tarafından ASCII 8859-9 Code page'i ile düzenlenmiştir.

    ASCII tablosu ve onların code page'leri uzun süre kullanılmış ve hala kullanılmakta olsa da maalesef karışıklıklara 
    yol açmaktadır. İşte son 20 yıldır artık karakterleri 2 byte içerisinde ifade ederek dünyanın bütün dillerinin ve ortak 
    sembollerinin tek bir tabloya yerleştirilmesi ile ismine Unicode denilen bir tablo oluşturulmuştur (www-Unicode.org).
    Unicode tablo ISO tarafından 10646 ismiyle de bazı farklılıklarla standardize edilmiştir. Unicode tablonun
    ilk 128 karakteri standart ASCII karakterleri, ikinci 128 karakteri ISO 8859-9 code page'indeki karakterlerdir. 

    Bir karakter tablosundaki code point'lerin ikilik sistemde ifade edilme biçimine "encoding" denilmektedir. ASCII code 
    page'lerinde encoding doğrudan code point'in 1 byte'lık sayı karşılığıdır. Ancak Unicode tablonun değişik encoding'leri 
    kullanılmaktadır. Unicode tablonun klasik encoding'i UTF-16'dır. Burada code point doğrudan 16 bir bir sayı biçiminde 
    ifade edilir. UTF-32 encoding'inde ise code point 32 bitlik bir sayı biçiminde ifade edilmektedir. (Unicode tablodaki 
    karakterler için başlangıçta 2 byte yetiyordu. Ancak daha sonraları tabloya pek çok karakter eklendi ve 2 byte yetmez 
    hale geldi.) Ancak Unicode tablonun en yaygın kullanılan encoding'i UTF-8 encoding'idir. UTF-8 kodlamasında standart 
    ASCII karakterler 1 byte ile, diğer karakterler 2 byte, 3 byte, 4 byte ve 5 byte kodlanabilmekedir. Türkçe karakterler 
    UTF-8 encoding'inde 2 byte yer kaplamaktadr. UTF-8 encoding'i Unicode bir yazının adeta sıkıştırılmış bir hali gibi 
    düşünülebilir. 

    Bugün pek çok programlama editörleri default durumda dosyayı Unicode UTF-8 encoding'ine göre saklamaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bilgisayar dünyasında çok kullanılan diğer bir sayı sistemi de 16'lık sistemdir. 16'lık sisteme İngilizce "hexadecimal 
    system" denilmektedir. 16'lık sistemde syaıları ifade etmek için 16 sembol bulunmaktadır. İlk 10 sembol 10'luk sistemdeki 
    sembollerden alınmıştır. Sonraki 6 sembol alfabetik karakterlerden alınmıştır:

    0
    1
    2
    3
    4
    5
    6
    7
    8
    9
    A
    B
    C
    D
    E
    F

    16'lık sistemdeki her bir basamağa "hex digit" denilmektedir. Örneğin:

    1FC8

    Burada 4 hex digit'lik bir sayı vardır. 16'lık sistemdeki bir sayıyı 10'luk sisteme dönüştürmek için her hex digit 
    16'lık kuvvetleriyle çarpılıp toplanır. Ancak 16'lık sistemdeki sayı kullanım gereği bakımından aslında 10'lu sisteme 
    pek dönüştürülmez. 16'lık sistemdeki her bir hex digit 4 bit ile ifade edilebilmektedir:

    0		0000
    1		0001
    2		0010
    3		0011
    4		0100
    5		0101
    6		0110
    7		0111
    8		1000
    9		1001
    A		1010
    B 		1011
    C 		1100
    D 		1101
    E 		1110
    F       1111

    16'lık sistemden ikilik sisteme dönüştürme yapmak çok kolaydır. Tek yapılacak şey bir hex digit'e karşılık yandaki 
    tablodaki 4 biti getirmektir. Örneğin:

    1FC9  =  0001 1111 1100 1001
    FA3D  =  1111 1010 0011 1101

    ikilik sistemdeki bir sayı da 16'lık sisteme çok kolay dönüştürülür. Tek yapılacak şey sayıyı dörderli gruplayıp ona 
    karşı gelen hex digit'i yazmaktır. Örneğin:

    1010 0001 1110 1000 0011 0101 = A1E835
      A   1     E    8    3    5

    Bilgisayar dünyasında 16'lık sistem aslında ikilik sistemin yoğun bir gösterimi olarak kullanılmaktadır. Yani ikilik 
    sistem çok yer kapladığı için kişiler ikilik sistem yerine 16'lık sistemi kullanırlar. Bu nedenle belleğin, dosyanın 
    içeriğini görüntüleyen programlar bunları ikilik sistem yerine 16'lık sistemde görüntülemektedir. 

    1 byte 2 hex digit ile ifade edilmektedir. Örneğin:

    1A 23 5C 78

    Burada 4 byte'lık bir bilgi vardır. Örneğin 2 byte içerisinde yazılabilecek en küçük negatif işaretli sayının hex 
    karşılığı 8000 biçimindedir. Örneğin bir byte'lık işaretli sistemde yazılabilecek en büyük pozitif sayı 7F biçimindedir. 
    İşareti tamsayı sisteminde 4 byte içerisinde -1 sayısı FFFFFFFF biçimindedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Eskiden daha fazla kullanılıyor olsa da toplamda oldukça seyrek kullanılan dğer bir sayı sistemi de 8'lik sayı sistemidir. 
    Bu sisteme İngilizce "octal system" denilmektedir. 8'lik sayı sistemindeki her bir basamağa "octal digit" denir. Octal 
    digit sembolleri olarak 10'luk sistemin ilk 8 sembolü kullanılmaktadır:

    0
    1
    2
    3
    4
    5
    6
    7

    Her octal digit 3 bit ile ifade edilebilir:

    0	000
    1	001
    2	010
    3	011
    4	100
    5	101
    6	110
    7	111

    Bu durumda bir octal sayı ikilik sisteme kolay bir biçimde dönüştürülebilir:

    476		100 111 110
    741		111	100	001

    Benzer biçimde ikilik sistemdeki bir sayı da sağdan sola üçer bir gruplandırılarak 8'lik sisteme dönüştürülebilmektedir. 
    Örneğin:

    1011 1011	=  273
    0111 1110	=  176

    8'lik sistem de ikilik sistemin yoğun bir gösterimi olarak kullanılmaktadır. Ancak 8'i tam ortalayamadığı için kullanımı 
    seyrektir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                            7. Ders - 14/06/2022-Salı										
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Klavyeden bastımız tuşlara ilişkin karakterlerin İngilizce isimleri şöyledir:

    +		plus
    -		minus, dash, hyphen
    *		asterisk
    /		slash
    \		back slash
    %		percent sign
    ()		paranthesis (left, right, opening, closing)
    {}		(curly) brace (left, right)
    []		(square) bracket (left, right)
    =		equal sign
    #		sharp, number sign
    '		single quote
    ""		doueble quote
    _		underscore
    ^		caret
    &		ampersand
    !		exclamation mark
    ,		comma
    :		colon
    ;		semicolon
    |		pipe
    <		less than
    >		greater than
    .		period
    ?		question mark
    `		back tick
    ~		tilde
    @		at
    ...		ellipsis
    $		dollar sign
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Ekrana Merhaba Dunya yazısını çıkartan örnek C programı aşağıdaki gibidir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    printf("Hello World\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir C programı en yalın olarak önce bir text editör ile yazılır ve diske uzantısı ".c" biçiminde kaydedilir. Sonra 
    komut satırından C derleyicisi ile derlenir. Derleyiciler genel olarak komut satırından çalıştırılacak biçimde yazılırlar. 
    En çok kullanılan C derleyicileri şunlardır:

    - Microsoft C Derleyicisi
    - gcc Derleyicisi
    - clang Derleyicisi
    - Intel C derleyicisi

    Bu derleyicilerin dışında daha pek çok C derleyicisi vardır ve çeşitli kesimler tarafından kullanılmaktadır. Windows'ta 
    ağırlıklı olarak Microsoft'un C derleyicileri kullanılmaktadır. UNIX/Linux ve macOS sistemlerinde ise çoğu kez "gcc" 
    ya da "clang" derleyicileri tercih edilmektedir. "gcc" derleyicisinin ve clang derleyicisinin Windows versiyonu da 
    vardır. "gcc" derleyicisinin Windows için gerçekleştirimine (Windows port;'una) "mingw" denilmektedir. 

    C programını C derleyicisi ile derledikten sonra eğer hiçbir hata yoksa derleyici "relocatable object module" denilen
    bir dosya oluşturmaktadır. Bu dosyaya biz Türkçe "amaç dosya" da diyeceğiz. Amaç dosya (relocatable object module) 
    daha sonra "bağlayıcı (linker)" denilen bir programa sokulur. Bu bağlayıcı programı da "çalıştırılabilir (executable)" 
    dosya üretir. Biz de nihayetinde bu dosyayı çalıştırırız. 

    .c ---> C Derleyicisi ---> Amaç Dosya (Object file) ---> Bağlayıcı (Linker) ---> Çalıştırılabilir (executable) Dosya

    Bağlayıcı (linker da diyeceğiz) aslında bir grup amaç dosyayı alıp tek bir çalıştırılabilir dosya oluşturabilmektedir. 
    Bir amaç dosyanın içerisinde derlenmiş kodların yanı sıra bağlayıcnın birleştirme yapabilmesi için çeşitli bilgiler 
    de vardır. 

    Windows'ta bağlayıcı olarak genellikle Microsoft'un "link.exe" isimli programı kullanılmaktadır. UNIX/Linux sistemlerinde 
    de ağırklıklı olarak GNU'nun "ld" isimli bağlayıcı ya da LLVM projesindeki "ldd" bağlayıcısı kullanılır. 

    Derleyicinin ürettiği amaç dosyanın uzantıları Windows sistemlerinde ".obj", UNIX/Linux ve macOS sistemlerinde ise 
    ".o" biçimindedir.

    Bağlayıcının ürettiği "çalıştırılabilir" dosya ise Windows sistemlerinde ".exe" uzantılıdır. UNIX/Linux ve macOS 
    sistemlerinde dosyanın çalıştırılabilir olup olmadığı uzantı ile değil dosya erişim hakları ile ("x" hakkı ile) 
    belirlenmektedir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Windows sistemlerinde Merhaba Dünya programının komut satırından derlenip çalıştırışması şöyle yapılır:

    1) Program bir editörde yazılır ve ".c" uzantılı biçimde saklanır. Biz bunun "sample.c" olduğunu varsayalım.
    
    2) Daha sonra komut satırı programı çalıştırılır ve dosayanın saklandığı dizine gidilir. Komut satırı programı olarak 
    "cmd.exe" programını doğrudan kullanmayınız. Çünkü bu program gerekli "path ayarlarına" sahip değildir. Bunun yerine 
    komut satırına geçmek için "Developer Command Prompt for VS 2022" programını kullanınız. Bu kısa yolu masaüstüne 
    taşırsanız rahat edersiniz. 

    3) Microsoft'un C derleyicisi "cl.exe" isimli programdır. Bu program en basit olarak şöyle çalıştırılır:

    cl <kaynak dosya ismi>

    Örneğin:

    cl sample.c

    "cl.exe" programı da derlemeyi yaptıktan sonra zaten bağlayıcıyı (linker) da kendisi çalıştırmaktadır. 

    4) Artık "cl.exe" derleme işlemini yapıp bağlayıcı programı da çalıştırdığı için çalıştırılabilir dosya oluşturulmuş 
    olur. Tek yapacağmız şey çalıştırılabilir programın ismini yazarak ENTER tuluna basmaktır. 

    "cl.exe" derleyicisinin yalnızca derleme yapmasını ancak bağlayıcıyı çalıştırmamasını istiyorsak "/c" seçeneğini 
    (switch) kullanmamız gerekir. Örneğin:

    cl /c sample.c

    Şimdi artık derleyici linker programını çalıştırmayacaktır. Yalnızca ".obj" dosyayı oluşturacaktır. Biz istersek 
    bağlayıcıyı da bağımsız olarak çalıştırabiliriz. Microsoft'un bağlayıcı programı "link.exe" isimli programdır.

    link sample.obj

    Baradan "sample.exe" programı elde edilecektir. cl.exe derleyicisinde çalıştırılabilir dosyanın ismini değiştirebilmek 
    için /Fe:<dosya ismi> seçeneği kullanılmaktadır. Örneğin:

    cl /Fe:test.exe sample.c

    Artık çalıştırılabilir dosyanın ismi "sample.exe" değil "test.exe" olacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Linux sistemlerinde Merhaba Dünya programının derlenerek çalıştırılması da şöyle yapılmaktadr:

    1) Yine önce bir editörde program yazılır ve ".c" dosyası olarak kaydedilir. Biz kaynak dosyamıza "sample.c" ismini 
    vermiş olalım.

    2) Komut satırından kaynak dosyanın bulunduğu dizine geçilir. gcc derleyicisi ile clang derleyicilerinin komut satırı 
    seçenkleri tamamen aynıdır. Bu derleyicilerle erleme işlemi şöyle yapılır:

    gcc <kanak dosya ismi>
    clang <kaynak dosya ismi>

    Örneğin:

    gcc sample.c

    gcc de tıpkı cl.exe programında olduğu gibi önce derleme işlemini yapar. Sonra bağlayıcı programı çalıştırıp çalıştırılabilen 
    dosyayı oluşturur. gcc derleyicisi derlemeyi bitirip bağlayıcıyı çalıştırdıktan sonra "object dosyayı" silmektedir. 
    Bu biçimde oluşturulan çalıştırılabilen dosya "a.out" ismindedir. Bu dosyanın çalıştırılması şöyle yapılmalıdır:

    ./a.out

    Windows sistemlerinde çalıştırılabilir dosyanın yalnızca isminin yazılması yeterlidir. Ancak UNIX/Linux ve macOS sistemlerinde 
    ./isim biçiminde çalıştırma yapılır. gcc derleyicisinde çalıştırılabilir dosyaya isim vermek için "-o isim" çeneği 
    kullanılmaktaır. Örneğin:

    gcc -o sample sample.c

    Burada "sample.c" dosyası derlenir ve "sample" isimli çalıştırılabilir dosya oluşturulur. Tabii istersek gcc derleyicilerinde 
    de yalnızca derleme yapıp bağlayıcıyı çalıştırmayabiliriz. Bunun için "-c" seçeneği kullanılmaktadır. Örneğin:

    gcc -c sample.c

    Burada derleme işlemi yapılır, "sample.o" object dosyası oluşturulur ancak başlayıcı çalıştırılmaz.  
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Micrsoft Visual Studio IDE'sinde bir C programının derlenip çalıştırılabilmesi için tipik olarak şunlar yapılmalıdır:

    1) Önce Visual Studio IDE'si çalıştırılır. (Kurs yapıldığı sırada kullanılan IDE Visual Studio 2022 Community Edition 
    biçimidedir.) IDE açlıştırıldıktan sonra bir giriş sayfası gözükür. Oradan "Continue without code" seçilerek ana ekrana 
    geçilir. 

    2) Visual Studio IDE'sinde bir çalışma yapmak için bir proje yaratılmalıdır. Ancal projeler de "çözüm (solution)" denilen 
    kapların içerisindedir. O halde aslında bir proje yaratmak için bir çözüm de yaratılmaktadır. Bir çözüm aslında birdne 
    fazla projeyi tutan bir kap gibidir. Proje yaratmak için "File/New/Project"	seçilir. Proje türü olarak "C++ Empty Project" 
    seçilir. 

    3) Bundan sonra projeye bir isim verilir. Visual Studio proje bilgilerini burada ismi verilen bir dizin yaratarak onun 
    içerisine yerleştirmektedir. "Location" proje dizininin hangi dizinin altında yaratılacağını belirtir. "Place solution 
    and project in the same directory" checkbox'ı çarpılanmalıdır. Sonra proje yaratılır. Artık elimizde içi boş bir proje 
    vardır. Bir proje yaratıldığında aynı zamanda bir "çözüm (solution)" de yaratılmış olur. Çözümü idare etmek için 
    "Solution Explorer" denilen pencereden faydalanılmaktadır. 

    4) Artık sıra projeye bir kaynak dosya eklemeye gelmiştir. Bu işlem "Project/Add New Item" menüsü ile ya da "Solution 
    Explorer"da proje üzerinde bağlam menüsünü açıp "Add/New Item" seçilerek de yapılabilir. Artık karşımıza başka bir 
    diyalog penceresi çıkacaktır. Burada "C++ File" seçilip dosya ismi uzantısı ".c" olacak biçimde	seçilmelidir. Microsoft 
    C++ demekle aynı zamanda C'yi de kastetmektedir. Aslında "cl.exe" derleyicisi hem C hem de C++ derleyicisidir. Bu 
    derleyici kaynak kodun uzantısına bakarak hangi dile göre derleme yapacağına karar verir. Dolayısıyla bizim kesinlikle 
    dosya uzantısını ".c" biçiminde girmemiz gerekir. 

    5) "Kaynak dosya" projeye eklendikten sonra kod yazılır.

    6) Build/Compile seçilirse dosya yalnızca derlenir. "Build" kavramı C/C++ dünyasında "derleme ve bağlama işlemini" 
    anlatmaktadır. "Build/Build Solution" seçilirse solution içerisindeki tüm projeler derlenip link edilir. Build/Build XXX"
    seçilirse (burada XXX aktif projenin ismidir) bu durumda yalnızca aktif proje derlenerek bağlanır.

    7) Programı çalıştırmak için "Debug/Start Without Debugging" seçilir. Bunun kısa yol tuşu "Ctrl + F5"tir. Zaten
    Ctrl+F5'e bastığımızda dosyada bir değişiklik varsa build işlemi yapılmaktadır. O halde aslında bizim tek yapacağımız 
    şey Ctrl+F5 tuşlarına basmaktır. 

    Projeyi (ve çöüzümü) kapatma işlemi File/Close Solution menüsü ile yapılabilir. Bir projeyi açmanın en kolay yolu 
    giriş ekranındaki "Open recent"	listesinden son projelerden birini seçmektir. Dğer yolu "File/Open-Project" menüsünü 
    seçip buradan solution dizininne gelip uzantısı ".sln" olan dosyayı seçmektir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                            8. Ders - 16/06/2022-Perşembe
----------------------------------------------------------------------------------------------------------------------*/


/*----------------------------------------------------------------------------------------------------------------------
    C/C++ için çok tercih edilen diğer bir IDE de "Qt Creator" denilen IDE'dir. Buradaki çalışma biçimi ana hatlarıyla 
    Visual Studio'ya benzemektedir. Önce yine bir proje yaratılmalıdır. Bunun için "File/New File or Project" menüsü 
    seçilir. Açılan diyalog penceresinden template olarak "None Qt Project" seçilir. Buradan da "Plain C Application"
    seçilir. Projeye isim verilir ve projenin yaratılacağı dizin belirtilir. Qt Creator IDE'si bu seöenekle bir ".c" 
    dosyasını projeye ekleyip onun içerisine birkaç satırlık "Merhaba Dünya" programını yazmaktadır. Derleme ve link 
    işlemi ve çalıştırma işlemi tek tuşla (Ctrl+R) yapılabilir. Yine bu işlem GUI ekranındaki çalıştır düğmesine tıklanarak 
    da yapılabilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Kod derleyici tarafından derlenirken derleyiciler bazı sorunlar karşısında "tanı mesajları (diagnositic messages)" 
    ile bu sorunları programcıya iletirler. Derleyicilerin verdiği tanı mesajları üçe ayrılmaktadır:

    1) Uyarılar (Warnings)
    2) Hatalar (Errors)
    3) Ölümcül Hatalar (Fatal Errors)

    Uyarılar amaç kod oluşumunu engellemeyecek derecede mantıksal hatalar için verilmektedir. Yani derleyici kodu 
    anlamlandırmakla birlike programcının yapmış olabileceği olası bazı mantık hatalarına dikkat çekmek için uyarı 
    vermektedir. Hatalar amaç dosyanın (object file) oluşumunu engelleyecek derecede olan ciddi hatalardır. Genellikle 
    dilin kurallarına uyulmaması yani kodun hatalı bir biçimde yazılması sonucunda oluşur. Programcının mutlaka bu tür 
    hataları düzeltmesi gerekir. Ölümcül hatalar (fatal errors) derleme işleminin bile devam ettirilmesini engelleyecek 
    derecede önemli hatalardır. Normal olarak bir hata ile karşılaşıldığında tüm tanı mesajlarının topluca listelenmesi 
    için derleme işlemine devam edilir. Ancak bir ölümcül hata ile karşılaşıldığında artık derleme işlemi bile sonlandırılır. 
    Ölümcül hatalar genellikle sistemdeki önemli sorunlar yüzünden ortaya çıkmaktadır. Örneğin diskin tamamen dolu 
    olması, derleyicinin işlemine devam edebilmesi için gereken ana belleğin yetersiz olması gibi durumlar tipik ölümcül 
    hata gerekçeleridir.

    Derleyiciler genellikle tanı mesajllarında ilgili sorunun kaynak kodun neresinde eolduğunu da belirtirler. Pek çok 
    derleyici hata mesajlarına içsel birer numara vermektedir. Bu numara yoluyla tanı mesajı hakkında daha fazla bilgi 
    elde edilebilmektedir. Tanı mesajları standardize edilmemiştir. Derleyiciden derleyiciye tanı mesajlarının içeriği
    değişebilmktedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir programlama dilinde kendi başına anlamlı olan en küçük birime "atom (token)" denilmektedir. Örneğin aşağıdaki gibi 
    C'de yazılmış bir kod parçası olsun:

    if (a > 10)
        x = 10;
    else
        y = 20;

    Bu kodu şöyle atomlarına ayırabiliriz:

    if
    (
    a
    > 
    10
    )
    x
    =
    10
    ;
    else
    y
    =
    20
    ;

    Burada koddaki tüm öeğelerin en küçük anlamı birimlere ayrıştırıldığına dikkat ediniz. "Merhana Dünya" programı
    da şöyle atomlarına ayrılabilir:

    #
    include
    <
    stdio.h
    >
    int
    main
    (
    void
    )
    {
    printf
    (
    "Hello World\n"
    )
    ;
    return
    0
    ;
    }

    Gerçekten de derleyiciler derlemenin ilk aşamasında kaynak kodu bu biçimde atomlarına ayırmaktadır. Derlyicilerin 
    bu işi yapan modüllerine "scanner" ya da "lexical analyzer" ya da "tokenizer" denilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Atomlar tipik olarak 6 gruba ayrılmaktadır:

    1) Anahtar Sözcükler (Keywords/Reserved Words): Dil için özel anlamı olan, değişken olarak kullanılması yasaklanmış 
    sözcüklerden oluşan atomlardır. Örneğin "if" gibi, "return" gibi, "int" gibi. Bu sözcükler doğrudan derleyici tarafından 
    tanınmaktadır.

    2) Değişkenler (Identifers): İsmini programcının istediği gibi verebildiği atomlardır. Örneğin bir programdaki i, count, 
    a, b gibi atomlar tipik olarak değişken atomlardır. "Merhaba Dümya" programındaki printf ve main anahtar sözcük değildir.
    Değişken atom statüsündedir. Bir atomun anahtar sözcük olması için derleyicinin onu gördüğünden değişkenden farklı 
    bir işlem uygulaması gerekir. 

    3) Sabitler (Literals): Bir sayı ya bir değişkenin içerisindedir ya da program içerisinde doğrudan kullanımıştır. İşte 
    program içerisinde doğrudan yazılmış olan sayılara "sabit" denilmektedir. Örneğin:

    a = b + 10;

    Burada a ve b değişken atomdur, ancak 10 sabit atomdur.

    4) Operatörler (Operators): Bir işleme yol açan ve işlem sonucunda bir değer üretilmesini sağlayan + gibi - gibi, * 
    gibi atomlara operatör denilmektedir. Örneğin:
    
    a = b + c * 3

    Bu ifadedeki atomlar ve türleri şöyledir:

    a	değişken
    =	operatör
    b	değişken
    + 	operatör
    c	değişken
    *	operatör
    3	sabit

    5) Stringler (String literals): İki tırnak içerisindeki yazılar iki tırnaklarıyla birlikte tek bir atom belirtir. 
    Bunlara "string" denilmektedir. C standartlarında string'ler de sabit atom olarak sınıflandırılmaktadır.

    6) Ayıraçlar (Delimiters/Punctuators): Yukarıdaki grupların dışında kalan ifadeleri birbirinden ayırmak için kullanılan 
    tüm atomlar ayıraç grubundadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Değişkenlerin, operatörlerin ve sabitlerin her bir kombinasyonuna "ifade (expression)" denilmektedir. Örneğin:

    a + b
    a + b - 2
    10
    3 * 4
    3 - 2 * a
    foo()
    a = b * c

    birer ifadedir. Tek başına bir değişken ve tek başına bir sabit ifade belirtir. Ancak tek başına bir operatör ifade 
    belirtmez. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bellekte yer kaplayan ve erişilebilen bölgelere "nesne (object)" denilmektedir. Programlama dillerindeki değişkenler 
    genellikle nesne durumundadır. Örneğin:

    a = 10

    gibi bir ifadede a bir nesne durumundadır. Biz bu a ismiyle a'nın bellek bölgesine erişebilmekteyiz. Bir olgunun 
    nesne belirtmesi için yalnızca bellekte yer kaplaması yetmez. Aynı zamanada "erişilebilir" olması da gerekir. 
    Örneğin sabitler de bellekte yer kaplarlar. Ancak erişilebilir olmadıkları için nesne değillerdir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir ifade ya nesne belirtir ya da nesne belirtmez. Nesne belirten ifadelere "sol taraf değeri (lvalue)", nesne 
    belirtmeyen ifadelere "sağ taraf değeri (rvalue)" denilmektedir. Örneğin:

    a ifadesi nesne belirtir. Sol taraf değeridir. 
    b[i] ifadesi nesne belirtir, sol taraf değeridir.
    a + b ifadesi nesne belirtmez, sağ taraf değeridir.
    10 ifadesi nesne belirtmez, sağ taraf değeridir.
    printf("Helo World") ifadesi nesne belirtmez, sağ taraf değeridir. 

    Sol taraf değeri (left value) ismi tipik olarak bu tür iafadelerin atama operatörünün soluna getirilebilmesi nedeniyle 
    verilmiştir. Sağ taraf değeri (right value) atama operatörünün soluna getirilemeyen ifadelere denilmektedir. Tabii 
    bunlar tipik olarak atama operatörünün sağına da getirilirler. Ancak atama operatörünün sağına getirilen her şey 
    sağ taraf değeri değildir. Soluna getirilemeyenler sağ taraf değeridir.  
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Sentaks açıklamak için çeşitli "meta diller (meta languages)" oluşturulmuştur. Bunların en çok kullanılanı "BNF 
    (Backus Naur Form)" denilen notasyondur. BNF notasyonu "EBNF (Extended BNF)" biçiminde ISO tarafından standardize de 
    edilmiştir. Gerçekten de programlama dillerinin standartları genellikle BNF notasyonu ile ya da onun bir türevi ile 
    açıklanmaktadır. Ancak biz kurusumuzda "açısal parantez-köşeli parantez" tekniğini kullanacağız. Bu teknikte açısal 
    parantezler içerisinde öğeler zorunlu öğeleri, köşeli parantezler içerisindeki öğeler "isteğe bağlı (optional)" öğeleri 
    belirtmektedir. Bunların dışındaki tüm atomların belirtilen konumda bulundurulması gerekir. Örneğin if deyimini 
    bu teknikle şöyle ifade edilebiliriz:

    if (<ifade>)
        <deyim>
    [
    else
        <deyim>
    ]
----------------------------------------------------------------------------------------------------------------------*/
                                                
/*----------------------------------------------------------------------------------------------------------------------
                                                9. Ders 21/06/2022-Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Boşluk oluşturmak için kullanılan karakterlere "boşluk karakterleri (white space)" denilmektedir. Boşuk karakterli 
    şunlardır:

    SPACE (32)
    LF (Line Feed) (10)
    CR (Carriage Return) (13)
    TAB (9)
    VTAB (11)

    TAB karakter aslında tek bir karakterdir. Bu karakteri gören editörler imleci belli bir miktarda ilerletirler. Bazı 
    editörler biz TAB tuşuna bastığımızda dosyaya TAB karakter basmaz bunun yerine editörün ayarlarında belirtildiği 
    miktarda SPACE karakteri basar. Bunun nedeni kaynak kod başka bir TAB ayarına ayarlanmış bir editörde açıldığında 
    aynı biçimde gözükmesini sağlamaktır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'nin yazım kuralı iki madde ile özetlenebilir: 

    1) #'li satırlar hariç atomlar arasında istenildiği kadar boşluk karakterleri bırakılabilir. Örneğin aşağıdaki program 
    geçerlidir:

    #include <stdio.h>

    int 

            main
            (    void
    )
                {
        printf
                (
        "Hello World\n"
                )
        ;
        
        return 
                    0
    ;
    }
    
    2) #'li satırlar hariç atomlar istenildiği kadar bitişik yazılabilirler. Ancak anahtar sözcüklerle değişkenler ve 
    sabitler btişik yazılamazlar. Merhaba Dünya programıını aşağıdaki gibi kompakt bir biçimde de yazabilirdik:

    #include <stdio.h>

    int main(void){printf("Hello World\n");return 0;}

    Tabii programcının kodunu güzel gözükecek ve iyi okunabilecek biçimde yazması gerekir. C'de çeşitli yazım stilleri 
    vardır. En yaygın kullanılan yazım stili Dennis Ritchie ve Brian Kernighan'ın "The C Programming Language" kitabında 
    uyguladığı yazım biçimidir. Buna "Ritchie Kernighan Tarzı" denilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki Merhaba Dünya programının açıklaması şöyledir:

    Burada #include <stdio.h> satırı "stdio.h" isimli bir dosyanın kaynak koda dahil edildiğini belirtmektedir. Yani bu 
    satır "stdio.h" dosyasının içeriğinin oraya "paste edileceği" anlamına gelmektedir. Başka bir deyişle biz "stdio.h" 
    dosyasını bu komutun bulunduğu yere yerleştirip bu komuttu silsek tamamen aynı durum oluşacaktır. Programda main isimli 
    bir fonksiyon tanımlanmıştır. Bir fonksiyonun tanımlanması onun bizim tarafımızdan yazılması anlamına gelir. Yani bu 
    programda biz main isimli bir fonksiyon yazmış durumdayız. Bir fonksiyonu tanımlamanın (yani yazmanın) genel biçimi 
    şöyledir:

    [fonksiyonun geri dönüş değerinin türü] <fonksiyonun ismi> ([parametre bildirimi])
    {
        /* ... */
    }

    Fonksiyonun geri dönüş değerinin türü C90'da yazılmayabiliyordu. Bu durumda C90 bunun int olarak yazılmış olduğunu 
    varsayıyordu. Ancak C99 ile birlikte fonksiyonlarda geri dönüş değerinin belirtilmesi zorunlu tutulmuştur. main 
    fonksiyonun geri dönüş değerinin türü standartlara göre int olmak zorundadır. 
    
    C'de "main" özel bir fonksiyondur. C programları her zaman "main" isimli fonksiyondan çalışmaya başlar. Programlama 
    dillerinde programın çalışmaya başladığı fonksiyonlara "entry point" denilmektedir. Bir fonksiyonun parametreleri 
    olabilir ya da olmayabilir. Eğer fonksiyonun parametresi yoksa parametre parantezi boş bırakılabilir ya da oraya 
    void anahtar sözcüğü yazılabilir. Tanımlama sırasında boş bırakmakla void anahtar sözcüğünün yazılması arasında bir 
    fark yoktur. 
    
    Her fonksiyonun bir ana bloğu olmak zorundadır. C'de iki küme parantezi arasındaki bölgeye "blok (block)" denilmektedir. 
    Bir fonksiyon çalıştırıldığında fonksiyonun ana bloğundaki deyimler	sırasıyla çalıştırılır. Ana blok bittiğinde 
    fonksiyon sonlanmış olur. main programı bittiğinde dolayısıyla tüm program sonlanmış olacaktır. "Merhaba Dünya" 
    programında main fonksiyonun ana bloğunun içerisinde printf isimli bir fonksiyon çağrılmıştır. Bir fonksiyonun 
    çağrılması (call) demek onun çalıştırılması demektir. Bir fonksiyon çağrıldığında akış fonksiyona gider, fonksiyonun 
    içerisindeki deyimler tek tek çalıştırılır. Fonksiyon bitince akış çağırma noktasından devam eder. printf fonksiyonu 
    çağrıldığında iki tırnak içerisindeki yazıları ekrana (stdout dosyasına) basmaktadır. Ekranda bir imleç (cursor) 
    vardır. Yazı bu imlecin bulunduğu itibaren ekrana yazdırılır. Sonra imleç yazının sonunda bırakılır. İmleç program 
    çalışmaya başladığında satırın başındadır. printf fonksiyonunda iki tırnak içerisindeki "\n" karakteri "imleci aşğı 
    satırın başına geçir" biçiminde özel bir anlama gelmektedir. Yani bundan sonra biz bir daha printf fonksiyonunu 
    çağıracak olsak artık o yazı aşağı satırın başından itibaren yazılacaktır. printf bir standart C fonksiyonudur. 
    Standart C fonksiyonları derleyicileri yazanlar tarafından yazılmış (tanımlanmış) olan hazır fonksiyonlardır. 
    main fonksiyonun sonunda return deyimi bulunmak zorunda değildir. Bu deyim ileride açıklanacktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    printf("Hello World\n");
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'nin C90, C99, C11, C17 ve C23 standartlarının olduğunu belirtmiştik. Derleyiciler genel olarak bu standartlara uygun 
    olacak biçimde derleme yapabilmektedir. Visual Studio IDE'sinde derleyicinin uygulayacağı standardı ayarlamak için proje 
    seçeneklerine gelinir. C-C++/Language/C Language Standart sekmesinden standart belirlenir. Biz kursumuzda burada "ISO C17" 
    standardını aktif hale getireceğiz. Aynı şey Qt Creator IDE'sinde ".pro" dosyasının içerisine aşağıdaki satırın eklenmesiyle 
    yapılabilmektedir:

    CONFIG += c17

    gcc ve clang derleyicilerinde komut satırında derleme yaparken -std=c90, -std=c99, -std=c11, -std=c17 seçenekleriyle 
    derleme standardı ayarlanabilir. Örneğin:

    gcc -std=c17 -o sample sample.c

    Ayrıca Microsoft derleyicilerinde proje seçeneklerinden C-C++ sekmesinde "SDL Checks" "No" yapılarak kapatılmalıdır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Programlama dillerinde "tür (type)" bir nesnenin bellekte kapladığı alanı, onun içerisindeki 1'lerin ve 0'ların nasıl 
    yorumlanacağını, o nesnenin hangi operatörlerle işleme sokulabileceğini belirten önemli bir bilgidir. C'de her nesnenin 
    ve her ifadenin bir türü vardır. Türler çeşitli anahtar sözüklerle	ifade edilirler. Biz de aşağıda C'deki temel türleri 
    açıklayacağız.

    [signed] int: Bu tür işaretli bir tamsayı türüdür. int türünün kaç byte yer kaplayacağı standartlarda derleyicileri 
    yazanların isteğine bırakılmıştır. Ancak standartlara göre int türü minimum 2 byte olmalıdır. Buügn 32 bie ve 64 bir 
    Windows ve UNIX/Linux ve macOS sistemlerindeki derleyicilerde int türü 4 bye (32 bit) uzunluktadır. Dolayısıyla int 
    türden bir nesne bu sistemlerde [-2147483678, 2147483647] aralığında tamsayı değerler tutabilir. Bazı mikrodenetleyici 
    derleyicilerinde ise int 2 byte (16 bit) uzunluğunda olabilmektedir. Derleyicileri yazanlar genellikle int türünü o 
    sistemdeki CPU yazmaçlarının uzunluğu kadar ya da o uzunlukla ifade edilebilecek kadar almaktadır. Bu tür belirtilirken 
    int demekle signed int demek arasında ya da int signed demek arasında bir fark yoktur.

    [unsigned] int: Her işaretli tamsayı türünün bir de işaretsiz biçimi vardır. signed int türünün işaretsiz biçimi 
    unsigned int türüdür. Tamsayı türlerinin işaretli biçimleri ile işaretsiz biçimleri aynı miktarda yer kaplarlar. 
    Aralarındaki tek fark işaret bitinin yorumudur. Dolayısıyla bu tür de 32 bit ve	64 bit UNIX/Linux ve macOS sistemlerinde 
    4 byte yer kaplamaktadır. unsigned int türünden bir nesne içerisine bu sistemlerde yerleştirilebilecek sayı sınırı 
    [0, +4294967295] biçimindedir. Bu türü biz unsigned biçiminde ya da unsigned int biçiminde ya da int unsigned biçiminde 
    ifade edebiliriz. 

    [signed] long [int]: long türü int türünden uzun olabilir ya da int türüyle aynı uzunlukta olabilir. Ancak int türünden 
    daha kısa olamaz. Standratlara göre long türü en az 4 byte (32 bit) uzunlukta olmak zorundadır. long türü de işaretli 
    bir tamsayı türüdür. Buradaki long ismi "int türünden uzun olabilen" anlamına gelmektedir. 32 bit ve 64 bit Windows 
    sistemlerindeki derleyicilerde long türü int türüyle aynı uzunluktadır (yani 4 byte). Ancak 32 bit UNIX/Linux ve 
    macOS sistemlerindeki derleyicilerde long türü 4 byte iken, 64 bit UNIX/Linux ve macOS sistemlerindeki derleyicilerde 
    long türü 8 byte (64 bit) uzunluğundadır. long türünü biz en kısa biçimde "long" olarak ifade edebiliriz. Ancak 
    signed long int, long int, signed int long gibi anahtar sözcükler yer değiştirerek de ifade edebiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                        10. Ders 23/06/2022-Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    unsigned long [int]: Bu tür long türünün işaretsiz biçimidir. Dolayısıyla sistemlerde long türüyle aynı uzunlukta 
    yer kaplar ancak sayının başındaki bit işaret biti olarak ele alınmaz. 32 ve 64 bit Windows Sistemlerinde ve 32 bit 
    UNIX/Linux ve macOS sistemlerinde bu tür long türünde olduğu gibi 4 byte (yani 32 bit) yer kaplamaktadır. Dolayısıyla 
    bu sistemlerdeki sınırı [0, +4294967295] biçimindedir. 

    [signed] short [int]: Bu tür int türünden küçük olabilen ya da int türü ile aynı uzunlukta olabilen işaretli bir tamsayı 
    türüdür. Standartlara göre short türü en az 2 byte (yani 16 bit) olmak zorundadır. 32 bit ve 64 bit Windows ve UNIX/Linux 
    ve macOS sistemlerinde short türü 2 byte (yani 16 bit) uzunluktadır. Dolayısıyla bu sistemlerde bu türden bir nesnesinin 
    içerisine biz [-32768, +32767] sınırları içerisinde bir tamsayı yerleştirebiliriz. 

    unsigned short [int]: Bu tür signed short türünün işaretsiz biçimidir. Dolayısıyla short türü kadar yer kaplar. short 
    türünün 2 byte olduğu sistemlerde bu türden bir nesnenin içerisine biz [0, +65535] arasında tamsayı değerler yerleştirebiliriz. 

    signed char: C standartlarında "byte" sözcüğü pek az yerde kullanılmıştır. Çünkü "byte" genellikle 8 bit için kullanılan 
    bir terimdir. Oysa bazı	bilgisayar sistemlerinde RAM'deki adreslenebilen birimler 8 bit değil 10, 11 bit değerlerde 
    olabilmektedir. Tabii bu sistemler son derece seyrektir. Ancak standartlar bu uç durumu da dikkate almaktadır. C 
    standartlarında char terimi "RAM'de adreslenebilen en küçük birimin büyüklüğünü" temsil etmektedir. Örneğin tipik 
    olarak eğer adreslenebilen en küçük birim 8 bit ise char 8 bittir. Ancak 10 bit ise char 10 bittir. Görüldüğü gibi 
    eğer C standartlarında char yerine byte terimi kullanılsaydı byte 8 bit olduğu için bu uç durum temsil edilemeyebilirdi. 
    Tabii bugünkü yaygın sistemlerin hemen hepsinde adreslenebilen en küçük birim 8 bittir. Dolayısıyla yaygın sistemlerin 
    hepsinde gerçekten char türü 8 bit yani bir byte uzunluğundadır. Zaten C standartlarında "byte" terimi tamamen bit 
    uzunluğu farklı olabilen yani "adreslenebilen en küçük birim" anlamında kullanılmaktadır. Başka bir dyişle bu tanımla 
    "char" ile "byte" aynı anlamdadır. Ayrıca standartlar ilgili sistemdeki adreslenebilen en küçük birimdeki bit sayısının 
    kaç bit olduğunu (yani char türünün kaç bitten oluştuğunu) <limits.h> dosyası içerisinde CHAR_BITS sembolik sabitiyle 
    derleyicinin belirtmesini zorunlu tutmaktadır. 

    Mademki char türü yaygın sistemlerin hepsinde 8 bitten oluşmaktadır, o halde signed char türünün de bu sistemlerdeki 
    sınırları [-128, +127] arasındadır. Özetle signed char bir byte'lık işaretli tamsayı türünü belirtmektedir. 

    unsigned char: Bu tür char türünün işaretsiz biçimidir. Dolayısıyla bu türün de bellekte kapladığı alan ilgili sistemdeki 
    adreslenebilen en küçük birimin	bit uzunluğu kadardır. 8 bitlik yaygın sistemlerde unsigned char türünden bir nesneye 
    [0, +255] arasında değerler yerleştirilebilir. 

    char: Yalnızca char denildiğinde bunun signed char mı yoksa unsigned char mı anlamına geleceği C standartlarında 
    derleyicileri yazanların isteğine bırakılmıştır. Microsoft C derleyicileri, gcc ve clang derleyicileri char türünü 
    default olarak signed char kabul etmektedir. Fakat başka derleyiciler unsigned char	kabul edebilirler. Aslında 
    Microsoft derleyicilerinde, gcc ve clang derleyicilerinde char denildiğinde default durum derleyici ayarlarından da 
    değiştirilebilmektedir. Her ne kadar char türü ilgili sistemde signed char ya da unsigned char anlamına geliyorsa da 
    char, signed char ve unsigned char  ne olursa olsun farklı türler gibi değerlendirilmektedir. Bunun önemi başka 
    konularda ortaya çıkacaktır.

    [signed] long long [int]: Bu tür C99 ile birlikte standartlara dahil edilmiştir. Dolayısıyla C90 uyumlu eski C 
    derleyicilerinde bu türü kullanamayabilirsiniz. long long türü long türünden uzun ya da long türüyle aynı uzunlukta 
    olabilen işaretli bir tamsayı türüdür. Standartlarda minimum 8 byte (yani 64 bit) olabileceği belirtilmiştir. Şu 
    andaki yaygın derleyicilerin hepsinde long long türü 8 byte uzunluktadır. 8 byte uzuluk için long long türünden bir 
    nesneye yerleştirilebilecek sayı sınırı [-9223372036854775808, +9223372036854775807] (katrilyar mertebesinde, 8 
    exabyte) biçimindedir. 

    unsigned long long [int]: Bu tür de long long türünün işaretsiz biçimidir. Dolayısıyla yaygın sistemlerin hepsinde 
    8 byte (yani 64 bit) uzunluktadır. unsigned long long türünden bir nesneye yerleştirilecek sayı sınırı da 
    [0, +18446744073709551615] (16 exabyte) biçimindedir.
/*----------------------------------------------------------------------------------------------------------------------

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki tüm türlere C'nin tamsayı türleri denilmektedir. C'de ayrıca üç tane de gerçek sayı (noktalı sayı) türü 
    vardır: float, double ve long double. Gerçek sayı türlerinin işaretli ve işaretsiz biçimleri yoktur. Bunlar zaten 
    doğuştan işaretlidir. 

    float: Bu tür 4 byte uzunluğunda gerçek sayı türüdür. Her ne kadar standartlar kullanılacak gerçek sayı formatını açıkça 
    belirtmiş olmasa da "Implementation Limits" kısmında gerçek sayı türleri için belirtilen limitler "IEE 754" standardını 
    ima etmektedir. Bu durumda float türü hemen her sistemde 4 byte uzunluktadır. float türünün yuvarlama hatalarına direnci 
    zayıftır. Bu nedenle float türü aslında C programcıları tarafından az tercih edilen bir gerçek sayı 
    türüdür. 

    double: Standartlara göre double türü float türü ile aynı olabilir ya da ondan daha duyarlıklı olabilir. Yaygın sistemlerin 
    büyük çoğunluğunda double türü 8 byte uzunluktadır ve "IEEE 754 LLong Real Format" biçiminde temsil edilmektedir. Ancak 
    bazı mikrodenetleyici derleyicilerinde double türü float ile tamamen aynı uzunlukta olabilmektedir. C programcılarının 
    en fazla tercih ettiği gerçek sayı türü double türüdür. Çünkü bu türün yuvarlama hatalarına direnci float türünden çok 
    daha iyidir. 

    long double: long double standartala göre double ile aynı duyarlılıkta ya da double türünden daha duyarlıklı olabilen 
    bir türdür. Bugün Microsoft C derleyicilerinde,	gcc ve clang derleyicilerinde long double türü tamamen double türüyle 
    aynı özelliktedir. Yani bu tür de bu derleyicilerde "IEEE 754 Long Real Format" biçiminde ifade edilmektedir. Fakat 
    bazı derleyicilerde (Örneğin eski Borland firmasının C derleyicilerinde) long double 10 byte'lık "IEEE 754 Extended 
    Real Format" biçiminde de derleyiciler tarafından alınabilmektedir. 

    Bir C derleyicisinde aslında float, double ve long double türlerinin hepsi 4 byte uzunlukta olabilir. 
/*----------------------------------------------------------------------------------------------------------------------

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte C'ye ikil değerler tutmak için _Bool isminde yeni bir tür daha eklenmiştir. (Bu tür isminin bu biçimde 
    size tuhaf gelecek şekilde isimlendirilmesinin amacı geçmişe doğru uyumu koruyabilmek içindir. C99 çıktğında "bool" 
    gibi bir ismi programcılar kendi programlarında kullanmış olabileceklerinden dolayı bu türü temsil etmek için "reserved
    " isimlerden biri tercih edilmiştir. C'de başı '_' ile başlayan ve ilk harfi büyük harf olan isimlerin zaten kullanılması 
    yasaklanmış durumdaydı.) _Bool türü için standartlar 0 ve 1 değerlerini tutabilen bir büyüklükte olması gerektiğini 
    belirtmişlerdir. Dolayısıyla _Bool türü aslında herhangi bir tamsayı türünün uzunluğu kadar olabilir. Tabii derleyiciler 
    bu türden nesneler için genel olarak 1 byte yer ayırmaktadır. 

    _Bool türü <stdbool.h> dosyası içerisinde "bool" ismiyle de typedef edilmiştir. Dolaysıyla programcı isterse <stdbool> 
    başlık dosyasını include edip _Bool yerine bool ismini de kullanabilir. Genellikle bool türünün olduğu diğer programlama 
    dillerinde true ve false biçiminde anahtar sözcükler de bulundurulmaktadır. Ancak C99'da bu biimde anahtar sözcüler 
    yoktur. Ancak <stdbool.h> içerisinde true 1 olarak, false 0 olarak define edilmiştir.  Dolayısıyla eğer <stdbool.h> 
    dosyası include edilirse true ve false sözcükleri 1 ve 0 yerine kullanılabilir. 

    C'nin üzerinde çalışılan son sürümü olan C23'te artık bool isminin de true ve false değerlerinin de anahtar sözcük 
    yapılması planlanmıştır. Yani artık C23 ile birlikte tıpkı C++'ta olduğu gibi bool bir anahtar sözcük durumundadır. 
    Ancak C23'te C99	uyumunun korunması için _Bool türü de muhafaza edilmektedir. 

    Son olarak C99 ile birlikte C'ye karmaşık sayı (complex number) türü de eklenmiştir. Karmaşık sayı belirtmek için 
    _Complex tür ismi anahtar sözcük olarak dile eklenmiştir. Ancak _Complex tek başına kullanılamaz. float, double 
    ve long double tür isimleriyle birlikte kullanılabilir. Yani C99 ile birlikte üç karmaşık sayı türü dile eklenmiş 
    durumdadır:

    float _Complex
    double _Complex
    long double _Complex

    Karmaşık sayılar gerçek ve sanal kısımları float olan, double olan ve long double olan iki bileşenli sayılardır. 
    Karmaşık sayı için i sayısı C99'da _COMPLEX_I anahtar sözcüğü ile temsil edilmiştir. Dolaysyıyla örneğin double 
    _Complex türünden 
    bir z değişkenine biz 3.2 + 2.4i değerini şöyle yazarız:

    z = 3.2 + 2.4 * _COMPLEX_I

    Ayrıca yazım kolaylığı için <complex.h> dosyası içerisinde _COMPLEX anahtar sözcüğü "complex" ismiyle de typedef 
    edilmiştir. Yani biz eğer <complex.h> dosyasını	include edersek _COMPLEX yerine complex sözcüğünü de kullanabiliriz. 
    Benzer biçimde <complex.h> içerisinde I isimli sembolik sabit de _COMPLEX_I olacak biçimde define edilmiştir. 
    Yani biz <complex.h> dosyasını include etmiş isek i sayısı için _COMPLEX_I yerine I harfini de kullanabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bu kadar çok tür varken aslında programcılar özel bir neden olmadıktan sonra tamsayı türü olarak hep int türünü, 
    gerçek sayı türü olarak da double türünü tercih ederler. C programcıları bir değişkenine içerisine küçük tamsayı değerleri 
    yerleştirecek olsalar bile o dğeişkeni char, short olarak değil yine int olark tanımlarlar. Fakat örneğin bir nicelik 
    int türünün sınırları içerisine sığmıyorsa artık daha büyük türler seçilmelidir. int türünden küçük türler programcılar 
    tarafından tekil nesneler için değil büyük diziler için tercih edilmektedir. Örneğin bir kişinin yaşını bir değişkende 
    tutacak olalım. Biz yine bu ndeğişkeni int türden almalıyız. Ancak bir milyon kişinin yaşını tutacaksak artık bu bir 
    milyonluk diziyi int türünden değil char türünden oluşturabiliriz. Aynı durum double türü için de geçerlidir. Programcı 
    ancak çok miktarda noktalı sayıyı tutacaksa float türünü tercih etmelidir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                            11. Ders 28/06/2022-Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C/C++, Java, C# gibi statik tür sistemine sahip programlama dillerinde bir değişkeni kullanmadan önce onun derleyiciye 
    tanıtılması gerekir. İşte değişkenlerin kullanılmadan önce derleyiciye tanıtılması işlemine "bildirim (declaration)" 
    denilmektedir. Bildirim işleminin basit genel biçimi şöyledir:

    <tür> <değişken_listesi>;

    Buradaki değişken listesi aralarına ',' atomu getirilmiş bir ya da birden fazla değişkenden oluşabilir. Örneğin:

    int a;
    long b, c, d;
    double weight;

    Aslında bildirim işleminin genel biçimi biraz daha ayrıntılıdır. Burada biz basit bir genel biçim verdik. 

    C "büyük harf küçük harf duyarlılığı olan (case sensitive)" bir programlama dilidir. Yani C'de değişken isimlendirmesinde 
    büyük harflerle küçük harfler farklı karakterler olarak ele alınırlar. Değişken isimlerndirmesinde pek çok programlama 
    dilindeki benzer kuralalr geçerlidir:

    - Değişken isimleri nümerik karakterlerle başlatılamaz. Ancak alfabetik karakterlerle başlatılıp nümerik karakterlerle 
    devam ettirilebilir. 
    - Değişken isimleri boşluk içeremez, operatör içeremez. 
    - Değişkenler anahtar sözcüklerdne oluşturulamazlar.
    - Alt tire karakteri alfabetik karakter gibi kullanılabilmektedir. 
    - Değişken isimlerindeki karakterler İnglizce küçük harfler, İngilizce büyük harfler ve alt tire karakterleri ve sayısal 
    karakterler olabilir. Türkçe gibi diğer dillerin karakterlerinin kullanılıp kullanılmayacağı derleyicileri yazanların 
    isteğine bırakılmıştır. Yani bazı derleyiciler örneğin Türkçe karakterlerden oluşan isimleri kabul ederken bazı derleyiciler 
    etmeyebilirler. 
    - Değişkenlerdeki karakter sayısı istenildiği kadar uzun olabilir. Ancak derleyiciler uzun değişken isimlerinin ilk n 
    karakterini dikkate alırlar. Bu n değeri derleyiciden derleyiciye değişebilmektedir. Bugün kullanılan yaygın derleyiciler 
    en az 256 karakteri desteklemektedir. Bu da zaten oldukça uzundur.

    C'de başı iki alt tire ile başlayan ve başı bir alt tire ve büyük harfle başlayan tüm isimler "reserved" yapılmıştır. 
    Programcıların bu biçimde isimlendirme yapmaması gerekir. Eğer programcılar böyle isimlendirme yaparsa kodları derlenir 
    ancak çalışma sırasında olumsuzluklar gözükebilir. (Bu duruma "tanımsız davranış (undefined behavior)" denilmektedir. 
    Bu kavram ileride açıklanacaktır.) Başı tek alt tire ile başlayan tüm isimler global faaliyet alanında "reserved" 
    yapılmıştır. (Yani örneğin biz _count gibi bir ismi global değişken olarak kullanamayız. Ancak yerek değişken olarak 
    kullanabiliriz. Global ve yerel faaliyet alanları konusu ileride ele alınacaktır.)
    
    C'de ağırlıklı bir biçimde küçük harfli isimlendirmeler tercih edilmektedir. Değişken isimlerinin anlamlı ve telefaffuz 
    edilebilir olması tavsiye edilir. Birden çok sözcükten oluşan değişken isimlerinde sözcüklerin ayrımsanması için üç 
    harflendirme biçimi (casting) kullanılmaktadır:

    1) Klasik C Tarzı Harflendirme: Buna "yılan notasyonu (snake casting)" de denilmektedir. Burada sözcüklerin arasında 
    alt tire bulundurulur. Örneğin:

    number_of_students
    total_count
    weight

    2) Deve Notasyonu (Camel Casting): Burada ilk sözcüğün tamamı küçük harflerle yazılır. Ancak sonrakşi sözcüklerin 
    yalnızca ilk harfleri büyük yazılır. Örneğin:

    numberOfStudents
    totalCount
    weight

    3) Pascal Notasyonu (Pascal Casting): Burada da her sözcüğün ilk harfi büyük yazılır. Örneğin:

    NumberOfStudents
    CreateWindow
    SetWindowText
    Sample

    Biz kursumuzda ağırlıklı olarak klasik C tarzı yazımı (yılan notasyonu) kullanacağız.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir bildirim işlemiyle eğer derleyici bildirilen değişken için bellekte yer ayırıyorsa o bildirime aynı zamanda 
    "tanımlama (definition)" da denilmektedir. Örneğin:

    int a;

    Bu bir bildirimdir. Ama aynı zamanda tanımlamadır. Çünkü derleyici bu bildirimde bildirilen a değişkeni için aynı 
    zamanda bellekte yer de ayırmaktadır. Her tanımalama bir bildirimdir ancak her bildirim bir tanımlama değildir. Tabii 
    bildirim olup da tanımlama olmayan durumlar seyrektir. Örneğin:

    int a;				/* bu hem bir bildirimdir hem de bir tanımlamadır */
    extern int b;		/* bu bir bildirimdir ama tanımlama değildir, tabii extern gibi bir konu henüz görülmedi */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir değişkene bildirim sırasında değer atayabiliriz. Bu işleme "ilkdeğer verme (initializtion)" denilmektedir. Örneğin:

    int a = 10, b, c = 20;

    Burada a ve c değişkenlerine ilkdeğer verilmiştir. Ancak b değişkenine ilkdeğer verilmemiştir. İlkdeğerverme ile 
    değişkene ilk kez değer atama aynı şey değildir. Örneğin:

    int a;

    a = 10;

    Buradaki atama bir ilkdeğer verme değildir. İlkdeğer verme bildirim sırasında değer atama anlamına gelmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aslında C standartlarında "ekran" ve "klavye" kavramları hiç geçmemektedir. Örneğin C standartları printf fonksiyonun 
    "ekrana yazdığını" söylememektedir. Standartlara göre printf fonksiyonu "stdout" denilen bir dosyaya yazmaktadır. Ancak 
    bu dosya bir aygıta yönlendirilmiş olabilir. Örneğin klasik bilgisayar sistemlerimizde stdout ekranı kontrol eden terminal
    aygıt sürücüsüne yönlendirilmiş durumdadır. Dolayısıyla printf stdout dosyasına yazar ancak yazılanlar ekranda görülür. 
    Tabii bir sistemde stdout başka aygıtlara ya da dosyalara yönlendirilmiş olabilir. Örneğin stdout seri porta yönlendirilmişse 
    artık printf fonksiyonu ile yazdırılanlar seri porta aktarılır. Aynı durum klavye için de geçerlidir. Aslında klavyeden 
    okumak diye de bir kavram yoktur. stdin dosyasından okumak diye bir kavram vardır. stdin dosyası da klasik bilgisayar 
    sistemlerinde genellikle klavye ile ilişkilendirilmiştir. Ancak örneğin stdin seri porta yönlendirilmişse artık seri 
    porttan gelen bilgiler okunur. Görüldüğü gibi "stdout" ve "stdin" aslında değişik donanımsal birimleri temsil ediyor 
    olabilir. Biz  kurusumuzda bazen "ekran" ve "klavye" laflarını edeceğiz. Burada tenik olarak "stdout" ve "stdin" 
    dosyaları anlaşılmalıdır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    printf aslında oldukça kapsamlı bir fonksiyondur. printf fonksiyonunda iki tırnak içerisindeki karakterler ekrana 
    (yani stdout dosyasına) basılır. Ancak printf % karakterini gördüğünde % karakterini ve onun yanındaki bir ya da 
    iki karakteri ekrana yazdırmaz. % karakterinin yanındaki bazı özel karakterlere format karakterleri denilmektedir. 
    % karakteri ve format karakterleri "yer tutucu" belirtir. Bu yer tutucular printf'in iki tırnak argümanından sonraki 
    argümanlarla sırasıyla eşleştirilmektedir. Böylece aslında format karakterleri değil de bu argümanların değerleri 
    yer tutucu yerine yazdırlır. Örneğin:

    int a = 10, b = 20;

    printf("a = %d, b = %d\n", a, b);

    Burada %d yer tutucudur. İlk %d yerine a'nın değeri, ikinci %d yerine b'nin değeri yazdırılır. 
----------------------------------------------------------------------------------------------------------------------*/
                                                    
/*----------------------------------------------------------------------------------------------------------------------
                                                            12. Ders 30-06-2022
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Format karakterleri eşleşen argümanın türünü ve yazdırma için kullanılan tabanı belirtecek biçimde farklılık göstermektedir. 
    En çok kullanılan format karakterleri şunlardır:

    %d  ---> signed char, short ve int türlerini 10'luk sistemde yazdırmak için
    %x  ---> işaretli ve işaretsiz char, short ve int türlerini 16'lık sistemde yazdırmak için
    %o  ---> işaretli ve işaretsiz char, short ve int türlerini 8'lik sistemde yazdırmak için
    %ld ---> long türünü 10'luk sistemde yazdırmak için
    %lld ---> long long türünü 10'luk sistemde yazdırmak için
    %lx ---> long türünü 16'lık sistemde yazdırmak için
    %llx ---> long long türünü 16'lık sistemde yazdırmak için
    %lo ---> long türünü 8'lik sistemde yazdırmak için
    %llo ---> long long türünü 8'lik sistemde yazdırmak için
    %u  ---> unsigned char, unsigned short ve unsigned int türlerini 10'luk sistemde yazdırmak için
    %lu ---> uunsigned long türünü 10'luk sistemde yazdırmak için
    %f  ---> float ve double türlerini 10'luk sistemde yazdırmak için (default durumda noktadan sonra 6 basamak 
    yuvarlanarak yazdırılır)
    %e  ---> float ve double türlerini üstel niçimde yazdırmak için.
    %c  ---> char, short ve int türlerini karakter görüntüsü olarak yazdırmak için
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 97;
    double b = 12.45;
    unsigned int c = 32503456;
    int d = 100;
    double e = 12000.345678;

    printf("%d - %c\n", a, a);		/* 97 - a */
    printf("%f\n", b);				/* 12.450000 */
    printf("%u\n", c);				/* 32503456 */
    printf("%x\n", d);				/* 64 */
    printf("%e\n", e);				/* 1.200035e+04 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    printf fonksiyonunda % karakterinden sonra fakat format karakterinden önce bir sayı belirtilirse ilgili argüman o 
    sayı ile belirtilen genişlikte bir alan ayrılarak o alanda yazılır. Default durum sağa dayalı olarak yazdırılmasıdır. 
    Sola dayalı yazdırmak için bu genişlik belirten sayının önüne ayrıca bir de '-' karakteri eklenir. Özellikle sütunsal 
    hizalamalar için "%-nd" gibi (burada n yerine bir sayı geitirilmelidir) format karakterleri kullanılmaktadır. Eğer g
    enişlik belirten sayı yazdırılacak sayının basamak sayısından az ise sayının hepsi yazdırılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

main(void)
{
    int a = 10;
    int b = 7;
    int c = 121;
    int d = 1234567;

    printf("%-20d%f\n", a, sqrt(a));
    printf("%-20d%f\n", b, sqrt(b));
    printf("%-20d%f\n", c, sqrt(c));
    printf("%-20d%f\n", d, sqrt(d));

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    float ve double sayılarda sayının toplam genişliği ve noktadan sonraki kısmın genişliği ayrı ayrı belirtilebilmektedir. 
    Örneğin "%10.2f" toplam 10 alan içerisinde sayı noktadan sonra iki basamak olacak biçimde yazdırılır. Burada yalnızca 
    noktanın sağ tarafının kaç basamak yazdırılacağı da belirtilebilir. Örneğin "%.3f" sayının tam kosmının tam olarak 
    yazılacağı ancak noktadan sonraki kısmın üç basamak biçiminde yuvarlanarak yazdırılacağı anlamına gelir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

main(void)
{
    double a = 12.346;

    printf("___%10.2f___\n", a);		/*___     12.35___*/
    printf("___%-10.2f___\n", a);		/*___12.35     ___*/
    printf("___%.10f___\n", a);			/*___12.3460000000___*/
    printf("___%.0f___\n", a);			/*___12___*/

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de bildirimler üç yerde yapılabilir:

    1) Fonksiyonların içerisinde. Fonskiyonların içerisinde bildirilen değişkenlere "yerel değişkenler (local variables)" 
    denilmektedir. 
    2) Fonksiyonların dışında. Fonksiyonların dışında bildirilen değişkenlere "global değişkenler (globaş variables)" 
    denilmektedir. 
    3) Fonksiyonların parametre parantezleri içerisinde. Fonksiyonların parametre parantezleri içerisinde bildirilen 
    dğeişkenlere "parametre değişkenleri (parameters)" denilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int x;				/* global deişken */

void foo(int n)		/* Parametre değişkeni */
{
    int a;			/* yerel değişken */
    /* ... */
}

int y;				/* global değişken */

int main(void)
{
    int b;			/* yerel değişken */
    /* ... */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de küme parantezleri arasındaki bölgeye "block (block)" denilmektedir. Bir fonksionun ana bir bloğu olma zorundadır. 
    Ancak o ana bloğun içeriside istenildiği kadar çok iç içe ya da ayrık blok bulundurulabilir. Örneğin:

    void foo(void)
    {
        ...
        {
            ...
        }
        {
            ...
            {
                ...
            }
        }
        ....
    }
        
    C90'da yerel değişkenler blokların başında bildirilmek zorundaydı. Burada blokların başı demekle blokların ilk işlemi 
    olacak biçimde bildirim yapma kastedilmektedir. Ancak bu kural C99 ve ötesinde değiştirilmiştir. C99 ve ötesinde yerel 
    değişkenler blokların herhangi bir yerinde bildirilebilirler. Örneğin:

    int main()
    {
        printf("this is a test\n");
        int a;			/* C90'da geçersiz! C99 ve ötesinde geçerli */

        {
            int b;		/* C90'da da geçerli */
            printf("this is a test\n");
        }

        int c;			/* C90'da geçersiz! C99 ve ötesinde geçerli */
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir tamsayı 10'luk, 16'lık ve 8'lik sistemde bir sabit biçiminde belirtilebilmektedir. Default sistem 10'luk 
    sistemdir. Ancak bir tamsayı 0x ile ya da 0X ile başlanarak yazılırsa bu durumda sayının 16'lık sistemde yazılmış 
    olduğu kabul edilir. Eğer bir sayı başına 0 getirilerek yazılırsa bu da sayının 8'lik sistemde yazılmış olduğu anlamına 
    gelir. Örneğin:

    100			(onluk sistemde 100)
    0x64		(16'lık sistemdeki 64 yani 10'luk sistemde 100)
    012			(8'lik sistemde 12 yani 10'luk sistemde 10)

    Tabii biz tamsayı değeri kaçlık sistemde yazarsak yazalım aslında bellekte her zaman bu sayı ikilik sistemde tutulmaktadır. 

    C'de bir tamsayıyı ikilik sistemde yazmanın bir yolu yoktur.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

main(void)
{
    int a;

    a = 100;
    printf("%d\n", a);		/* 100 */

    a = 0x64;			
    printf("%d\n", a);		/* 100 */

    a = 012;
    printf("%d\n", a);		/* 10 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir noktalı sayı üstel biçimde de yazılabilir. Bunun için sayıdan sonra 'e' ya da 'E' karakteri ve üs sayısı 
    belirtilir. Buradaki üs 10'un kaçıncı kuvveti olduğunu belirtmektedir. Örneğin:

    a = 1.23e20;
    b = 1.23E-12
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

main(void)
{
    double a;

    a = 123.456e5;
    printf("%f\n", a);			/* 12345600.000000 */

    a = 1.23e-5;
    printf("%f\n", a);			/* 0.000012 */

    a = 1e20;
    printf("%f\n", a);			/* 100000000000000000000.000000 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bu bölümde C'de temel türlere ilişkin sabitlerin nasıl iafde edileceği üzerinde duracağız. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Doğrudan yazılan sayılara "sabit (literal)" denilmektedir. C'de yalnızca değişkenlerin değil sabitlerin de türleri 
    vardır. Bir sabitin türü onun nasıl yazıldığına ve niceliğine bakılarak belirlenmektedir. Bir C programcısının da 
    bir sabiti gördüğünde onun türünü tespit edebilmesi gerekir. 

    1) Sayı nokta içermiyorsa ve sonunda bir ek de yoksa eğer sayı 10'luk sistemde yazılmışsa sırasıyla sayı "int", "long" 
    ve ""long long" türlerinin hangisinin içerisinde ilk kez kalıyorsa sabit o türdendir. Örneğin:

    0		int türden sabit
    123		int türden sabit
    -123	Bu bir sabit değildir. Burada sabit olan 123'tür. Sayının başındaki '-' bir operatördür.

    Şimdi çalıştığımız sistemde int ve long türünün 4 byte ancak long long türünün 8 byte olduğunu varsayalım. Bu durumda:

    3000000000		long long türden sabit
    10000000000000	long long türden sabit

    2) Sayı nokta içermiyorsa sonunda da ek yoksa ancak 16'lık sistemde ya da 8'lik sistemde yazılmışsa sayı sırasıyla int, 
    unsigned int, long, unisgned long, long long ve unsigned long long sınırlarının hangisinin içerisinde ilk kez kalıyorsa 
    sabit o türdendir. Çalıştığımız sistemde int ve long türünün 4 byte ancak long long türünün 8 byte olduğunu varsayalım.

    0x10						int türden sabit
    0xFC123478					unsigned int türden sabit
    0x321223123123;				long long türden sabit

    3) Sayı nokta içermiyorsa ancak sayının sonunda ona yapışık bir biçimde 'u' ya da 'U' eki varsa sayı 10'luk, 16'lık, 
    8'lik sistemde yazıldığında sabit sırasıyla unsigned int, unsigned long int ve unsigned long long int türlerinin 
    hangisinin sınırları içeirsinde ilk kez kalıyorsa sabit o türdendir. Örneğin:

    123U				unsigned int türünden sabit
    0u					unsigned int türdne sabit
    30000000000000U		unsigned long long türünden sabit
    0x1234u				unsigned int türünden sabit
    01234U				unsigned int türünden sabit

    4) Sayı nokta içermiyorsa ve sayının sonunda onunla yapışık bir biçimde 'l' ya da 'L' harfi varsa, sayı 10'luk sistemde 
    yazılmışsa sabit long ve long long türlerinin hangisinin sınırları içerisinde ilk kez kalıyorsa o türdendir. Örneğin:

    1L					long türden bir sabit
    1234567890123L		long long türden sabit

    5) Sayı nokta içermiyorsa ve sayının sonunda onunla yapışık bir biçimde 'l' ya da 'L' harfi varsa sayı 16'lık ya da 
    8'lik sistemde yazılmışsa sabit long, unsigned long, long long ve unsigned long long türlerinin hangisinin sınırları 
    içerisinde ilk kez kalıyorsa sabit o türdendir. Örneğin:

    0x12L			long türden sabit
    0123L			long türden sabit

    6) Sayı nokta içermiyorsa ve sayının sonunda onunla yapışık "ul ya da lu" varsa ('u' ya da 'l' ler büyük ya da küçük 
    olabilir) sayı 10'luk sistemde, 16'lık sistemde ya da 8'lik sistemde yazıldığında sabit sırasıyla unsigned long ve 
    unsigned long long sınırlarının hangisinin içerisinde ilk kez kalıyorsa o türdendir. 
    Örneğin:

    12LU				unsigned long int türden sabir
    1234567890123ul		unsigned long long türden sabit

    7) Sayı nokta içermiyorsa ve sayının sonunda "ll" ya da "LL" soneki varsa sayı 10'luk sistemde yazıldığında long long 
    türden sabit belirtir. Örneğin:

    1LL			long long türden sabit
    100ll		long long türden sabit

    8) Sayı nokta içermiyorsa ve sayının sonunda "ll" ya da "LL" soneki varsa ve sayı 16'lık ya da 8'lik sistemde yazılmışsa 
    long long ve unsigned long long türlerinin hangisinin sınırları içerisinde ilk kez kalıyorsa sabit o türdendir. Örneğin:

    0x12LL			long long türden sabit

    9) Sayı nokta içermiyorsa ve sayının sonunda "ull" ya da "llu" "soneki varsa (burada 'u' ve "ll" büyk harf ya da jüçük 
    harf olablir) bu durumda sabit unsigned long long türündendir. Örneğin:

    1uLL		unsigned long long türdne sabit

    10) Sayı nokta içeriyorsa ve sayının sonunda bir ek yoksa sabit double türdendir. Örneğin:

    1.2		double türden sabit
    0.2		double türden sabit

    Noktanın solunda bir şey yoksa ve noktanın sağında bir şey yoksa orada 0 olduğu kabul edilmektedir. Bu Fortran zamanından 
    beri kullanılan bir gelenektir. Örneğin.

    .12		double türden sabit, 0.12 ile aynı anlamda
    12.		double türden sabit, 12.0 ile aynı anlamda

    Sayı üstel biçimde yazılmışsa sayı nokta içermese bile double türden olur. Örneğin:

    1e3		Bu sayı 1000 anlamına geliyor olsa da üstel biçimde yazıldığı için double türden sabit belirtmektedir. 

    11) Sayı nokta içeriyorsa ve sayının sonunda 'f' ya da 'F' soneki varsa sabit float türdendir. Örneğin:

    12.3f		float türden sabit
    .1F			float türden sabit
    12.F		float türden sabit

    Sayı nokta içermiyorsa sayının sonuna 'f' ya da 'F' soneki getirilemez. Örneğin:

    12F			geçersiz sabit!
    1e3F		geçerli, burada noktaya gerek yok, çünkü sayı üstel biçimde yazılmış

    12) Sayı nokta içeriyorsa ancak sayının sonunda 'l' ya da 'L' varsa sabit long double türden olur. Örneğin:

    12L		long türden sabit
    12.3	double türden
    12.3L	long double türden
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                13. Ders 05/07/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    13) C'de tek tırnakla iki tırnak arasında çok fark vardır. (Halbuki bazı dillerde tek tırnak ile iki tırnak arasında 
    farklılık yoktur.) C'de bir karakter tek tırnak içerisine alınırsa bu ilgili karakterin karakter tablosundaki sıra 
    numarasını (code point) belirten bir sayı anlamına gelir. Örneğin C'de 'a' ifadesi aslında eğer ASCII karakter tablosu 
    kullanılıyorsa 97 sabiti ile aynı anlamdadır. 

    C'de bir karakter tek tırnak içerisine alınırsa bu ifade int türden sabit kabul edilir. Bu biçimdeki ifadelere "int 
    türden karakter sabitleri (integer character constants) de denilmektedir". Örneğin:

    #include <stdio.h>

    int main(void)
    {
        int ch;

        ch = 'a';

        printf("ch = %d, ch = %c\n", ch, ch);		/* ch = 97, ch = a */
    
        return 0;
    }

    Ancak karakter tablolarındaki bazı karakterlerin görüntü karşılığı yoktur. Yani bu karakterleri ekrana yazdırmak 
    istediğimizde bir şey görmeyiz. Ancak bazı eylemler gerçekleşir. Bu tür karakterlere "görüntülenemeyen karakterler 
    (non-printable characters)" de denilmektedir. İşte bu görüntülenemeyen bazı karakterlere ilişkin karakter sabitleri 
    özel bir biçimde ifade edilmektedir. ASCII karakter tablosunun (dolayısıyla Unicode karakter tablosunun da) ilk 32 
    karakteri görüntülenemeyen özel kontrol karakterinden oluşmaktadır. İşte çok kullanılan bazı görüntülenemeyen karakterler 
    tek tırnak içerisinde "önce bir ters bölü sonra özel bazı karakterler ile" temsil edilmektedir. Bu karakter sabitlerine 
    "ters bölü karakter sabitleri (escape sequnces)" denilmektedir. Bunların listesi şöyledir:

    '\a'		alert (7 numaralı ASCII karakteri), beep sesi çıkar
    '\b'		back space (8 numaralı ASCII karakteri), sanki back space tuşuna basılmış etkisi oluşur
    '\f'		form feed (12 numaralı SCII karakterş),  bir sayfa atar
    '\n'		line feed (10 numaralı ASCII karakteri), imleç aşağı satırın başına geçer
    '\r'		carriage return  (13 numaralaı ASCII karakteri), imleç bulunduğu satırın başına geçer)
    '\t'		tab (9 numaralı ASCII karakteri), imle. bir tab ileri gider
    '\v'		vertical tab (11 numaralı ASCII karakteri), imleç düşey olarak kaydırılır.

    Burada önemli olan nokta '\n' gibi bir karakter sabitinin her ne kadar tırnak içerisinde iki karakter bulunuyorsa da 
    aslında tek bir karaktere ilişkin karakter sabiti belirttiğidir. Yani '\n' karakter sabiti ne ters bölü karakterinin 
    ne de 'n' karakterinin karakter sabitidir. Tamamen başka bir karakter olan "LF (line feed)" denilen karakterin karakter 
    sabitidir. 

    Ters bölü karakter sabitleri iki tırnak içerisinde tek bir karakter olarak ele alınmaktadır. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        printf("ali\aveli\nselami\tfatma\nsacit\n");

        return 0;
    }

    Ters bölü karakterinin kendisine ilişkin karakter sabiti '\' biçiminde yazılamaz. Eğer biz böyle bir şey yazarsak 
    derleyici bunu "sanki ters bölü karakter sabitlerinden birisini yazmak istiyormuşuz da onu yazamamışız gibi" değerlendirir. 
    Ters bölü karakterinin kendisine ilişkin karakter sabitini '\\' biçiminde yazabiliriz. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        char ch;

        ch = '\\';
        printf("%c\n", ch);
    
        return 0;
    }
    
    Benzer biçimde iki tırnak içerisinde de ters bölü karakterinin kendisini yazdırmak istiyorsak iki ters bölü karakteri 
    kullanmalıyız. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        printf("c:\temp\a.dat\n");		/* yanlış yazım */
        printf("c:\\temp\\a.dat\n");	/* doğru yazım */

        return 0;
    }

    Tek tırnak karakterine ilişkin karakter sabiti ''' biçiminde yazılamaz. Bu durumda derleyici durumu "sanki tek tırnağın 
    içerisine bir şey yazılmamış gibi"  değerlendirecektir. Tek tırnak karakterinin karakter sabiti '\'' biçiminde yazılmalıdır. 
    Örneğin:

    #include <stdio.h>

    int main(void)
    {
        char ch;

        ch = '\'';
        printf("%c\n", ch);

        return 0;
    }

    İki tırnağın içerisinde tek tırnak karakterini ters bölüsüz de yazabilriz. Yani iki tırnak içerisindeki tek tırnak 
    karakterleri bir soruna yol açmamaktadır. Örneğin:

    printf("Izmir'in merkezi\n");		/* geçerli */

    Tabii istersek yine de bu tek tırnağı ters bölü karakteri biçiminde de yazabilirdik:

    printf("Izmir\'in merkezi\n");		/* geçerli, yukarıdaki ile aynı */

    Benzer biçimde iki tırnak içerisinde iki tırnak karakteri de doğrudan yazılamaz. Örneğin:

    printf(""Ankara"");			/* geçersiz! */

    İki tırnak içerisinde iki tırnak karakteri \" biçiminde belirtilmelidir. Örneğin:

    printf("\Ankara\"");		/* geçerli "Ankara" yazısı çıkacak.

    Tebii tek tırnak içerisinde iki tırnak karakteri de sorunsuz olarak kullanılabilir. Örneğin:

    ch = '"';		/* geçerli, sorun yok */

    Ancak sorun yaratmıyor olsa da biz istersek tek tırnak içerisinde iki tırnak karakterini yine \" biçiminde de yazabiliriz. 
    Örneğin:

    ch = '\"';

    Aslında C'de tek tırnak içerisine tek bir karakterin yerleştirilmesi zorunlu değildir. Tek tırnak içerisine int türünün 
    byte uzunluğu kadar karakter yerleştirilebilir (örneğin int türü 4 byte ise 4 karakter, 8 byte ise 8 karakter yerleştirilebilir). 
    Tek tırnak içerisine birden fazla karakter yerleştirildiğinde bunlara "multibyte karakterler" denilmektedir. Multibyte 
    karakterlerin ne belirttiği derleyicileri yazanların isteğine bırakılmıştır. Biz bu multibyte karakter kavramını ileride 
    yeniden ele alacağız. 

    Bir karakter sabitinin başına onunla yapışık bir L harfi (L harfi büyük harf olmak zorundadır) getirilebilir. Bu tür 
    karakter sabitlerine "geniş	karakter sabitleri (wide character constants)" denilmektedir. Örneğin:
    
    L'a'

    Geniş sabitleri wchar_t türündendir. Bu konu ileride ele alınacaktır. 

    C11 ile birlikte karakter sabitlerinin önüne yine onunla yapışık 'u' ve 'U' getirilebilmektedir. Örneğin:

    u'a'
    U'b'

    'u' öneki getirilmiş karakter sabitleri Unicode UTF-16 encoding'ini, 'U' öneki getirilmiş karakter sabitleri de Unicode 
    UTF-32 encoding'ini belirtir. Bunlar sırasıyla char16_t ve char32_t türündendir. (char16_t ve char32_t türleri <uchar.h> 
    içerisinde typedef edilmiş türlerdir.) Bu konu da ileride ele alınacaktır. Aslında C23'e kadar C standartlarında 'u' ve 
    'U' önekli karakter	sabitlerinin açıkça Unicode UTF-16 ve Unicode UTF-32 encoding'ine ilişkin olduğu belirtilmemiştir.
    Bu türden karakter sabitlerinin hangi encoding'e göre code point belirttiği derleyiciyi yazanların isteğine bırakılmıştı.
    Ancak C23'te artık açıkça bu sabitlerin Unicode UTF-16 ve Unicode UTF-32 türünden sabit belirttiği ifade edilmiştir.

    Bugün Microsoft'un C derleyicileri ve gcc ve clang derleyicileri geniş karakter sabitlerini Unicode UTF-16 ya da Unicode
    UTF-32 olarak ele almaktadır. Dolayısıyla geniş karakter sabitlerinin hangi karakter tablosu ve hangi encoding'e 
    ilişkin olduğu derleyiciden derleyiciye değişebilmektedir. Halbuki artık C23 ile birlikte 'u' ve 'U' öneki sayesinde 
    taşınabilir bir biçimde Unicode UTF-16 ve Unicode UTF-32 karakter sabitleri oluşturulabilmektedir. 

    14) C'de int türden küçük türlerin sabitleri yoktur. Yani C'de char, signed char, unsigned char, short ve unsigned 
    short türünden sabitler yoktur. En küçük sabit int türündendir. Tek tırnak içerisine yazılmış karakter sabitlerinin 
    de aslında int türden olduğunu anımsayınız.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir C programında Türkçe karakterlerin düzgün görünmemesinin birkaç nedeni olabilir. C standartlarına göre derleyici 
    için iki karakter kümesi söz konusudur: "Kaynak karakter kümesi (source character set)" ve "çalıştırma karakter kümesi
    (execution character set)". Kaynak karakter kümesi derleyicinin kaynak dosyadaki karakterleri anlamlandırdığı karakter
    kümesidir. Örneğin derleyicimizin kaynak karakter kümesi "Unicode UTF-8" ise bu durumda derleyici kaynak dosyanın 
    "Unicode UTF-8" olarak kodlanmış bir dosya olduğunu varsayacaktır. Türkçe için çeşitli bir byte'lık karakter kodlamaları 
    da vardır. Örneğin Micsosoft 1254, ISO 8859-9, OEM 857 gibi. Çalıştırma karakter kümesi ise derleyicinin amaç kodu 
    oluştururken anaç kod için kullandığı karakter kümesidir. Derleyicilerde genellikle default durumda bu iki karakter 
    kümesi aynıdır. Örneğin:

    int ch;

    ch = 'ş';

    Buradaki program parçasını biz Unicode UTF-8 olarak save etmişsek buradaki 'ş' karakteri iki byte ile kodlanacaktır. 
    Örneğin eğer derleyicimizin kaynak karakter kümesi Microsoft 1254 ise derleyici bu iki byte'ıuü sanki tek tırnak 
    içerisinde birden fazla karakter yazmışız gibi yani "multibyte" olarak ele alacaktır. Bu da Türkçe 'ş' karakterini 
    derleyicinin tanıyamaması anlamına gelecektir. Halbuki derleyicimizin kaynak karakter kümesi de "Unicode UTF-8" 
    olsaydı derleyicimiz dosyadaki bu iki byte'ın aslında Türkçe 'ş' karakteri olduğunu anlayacaktı. Buradan şu sonucu 
    çıkartabiliriz: Eğer biz Türkçe karakterleri derleyicinin tanımasını istiyorsak editörümüzün karakter kodlaması ile 
    derleyicimizin kaynak karakter kodlamasını aynı yapmalıyız. Pekiyi derleyicimizin Türkçe 'ş' harfini düzgün anladığinı 
    varsayalım. Bu durumda derleyicimiz yukarıdaki örnekte ch değişkenin içerisine hangi byte'ları yerleştirecektir? 
    İşte bu da "çalıştırma karakter kümesi" ile ilgilidir. Eğer derleyicimizin çalıştırma karakter kümesi de "Unicode 
    UTF-8" ise bu durumda derleyicimiz ch değişkenin içerisine 'ş' karakteri için yine aynı byte'ları kodlayacaktır. Durumun
    böyle olduğunu varsayalım. Şimdi çalılan kodda ch değişkenin içerisinde iki byte'lık Unicode UTF-8 Türkçe 'ş' karakteri 
    vardır. İşte biz bu karakteri stdout dosyasına yazdırdığımızda ekranda yine de 'ş' karakterini göremeyebiliriz. Çünkü
    terminal aygıt sürücüsünün de beklediği bir karakter kodlaması vardır. Terminal aygıt sürücüsü eğer bizden örneğin 
    Windows 1254 karakter kodlaması bekliyorsa bu durumda bizim gönderdiğimiz iki byte'ı terminal aygıt sürücüsü 
    iki ayrı karakter olarak yorumlayacaktır. Biz de muhtemelen 'ş' yerine anlamsız iki karakter göreceğiz. Bu durumda 
    terminal aygıt sürücüsünün karakter kodlamasının da bizimkiyle uyumlu olması gerekmektedir. 

    Linux ortamında genellikle her şeyin default karakter kodlaması "Unicode UTF-8"dir. Eğer biz Linux'ta "Unicode 
    UTF-8" dosyasında Türkçe karakterler kullanırsak tüm yukarıdaki öğeler aynı ayarda olduğu için sorun çıkmayacaktır. 

    Windows'ta Visual Studio IDE'sinde ayarlamalar biraz daha karmaşıktır. Eğer bilgisayarımızın bölgesel ayarları Türkçe 
    ise Microsoft buradaki editörün default karakter kodlamasını, derleyicinin kaynak ve çalıştırma karakter kümesini 
    "ASCII 1254 Code Page" olarak ayarlamaktadır. Eğer terminal aygıt sürücüsü de aynı ayardaysa bir sorun oluşmayacaktır. 
    Ancak eğer terminal aygıt sürücüsü "Unicode UTF-8" olarak ayarlanmışsa bu durumda bu uyumsuzluğu gidermek gerekir. 
    Şimdi terminal aygıt sürücüsünün kodlamasının UTF-8 olduğunu varsayalım. Bu durumda bizim IDE tarafında uyumu 
    koruyabilmemiz için kaynak dosyanın "Unicode UTF8" olarak kodlanması, derleyicinin kaynak ve çalıştırma karakter 
    kodlamalarının da "Unicode UTF-8" olarak ayarlanması gerekir. Microsoft C derleyicilerinde kaynak ve çalıştırma karakter 
    kodlamaları  "/source-charset:<karakter_kodlaması>" ve "/execution-charset:<karakter_kodlaması>" komut satırı seçenekleri 
    ile değiştirilebilmektedir. Aynı ayar "gcc" ve "clang" derleyicilerinde de "-finput-charset=<karakter_kodlaması>" ve 
    "-fexec-charset=<karakter_kodlması>" komut satırı argümanlarıyla yapılabilmektedir. Tabii bu komut satırı argümanlarına
    istediği
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de karakter sabitleri sayısal işlemlere sokulabilir. Çünkü zaten onlar birer sayı belirtmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int result;

    result = 'a' + 1;
    printf("%c, %d\n", result, result);		/* b, 98 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    putchar fonksiyonu bizden int türden bir değer alır. O değere karşı gelen karakter numarasına ilişkin karakterin 
    görüntüsünü ekrana (stdout dosyasına) yazar. Yani putchar(ch) çağrısıyla printf("%c", ch) çağrısı işlevsel olarak 
    tamamen eşdeğerdir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    a = 48;

    putchar(a);		        /* 0 */
    putchar('\n');	
    putchar('?');	        /* ? */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    getchar fonksiyonu adeta putchar fonksiyonun tersini yapmaktadır. Bu fonksiyonun parametresi yoktur. Fonksiyon 
    klavyeden (stdin dosyasından) bir karaktere basılıp ENTER tuşuna basılmasını bekler. getchar bu karakterin karakter 
    tablosundaki sıra numarasına geri döner. getchar bize int türden bir değer vermektedir. Örneğin:

    int ch;

    ch = getchar();

    getchar fonksiyonunu yanlışlıkla aşağıdaki gibi kullanmaya çalışmayınız:

    getchar(ch);

    getchar fonksiyonun parametresi yoktur. Fonksiyonun geri dönüş değerini bir değişkene yerleştirmelisiniz:

    ch = getchar();
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int ch;

    ch = getchar();
    putchar(ch);
    putchar('\n');
    printf("ch %d, ch = %c\n", ch, ch);

    return 0;
}
/*----------------------------------------------------------------------------------------------------------------------
    Aslında klavyeden (stdin dosyasından) okumalar bir tapon eşliğinde yapılmaktadır. Biz getchar fonksiyonunda birden 
    fazla karakter girebiliriz. Bu durumda girilen tüm karakterler önce bir "tampona (buffer)" yerleştirilir sonra o 
    tampondan alınarak verilir. getchar için basılan ENTER tuşu da tampona '\n' karakteri olarak eklenmektedir. getchar 
    (ve stdin dosyasından okuma yapan diğer fonksiyonlar) eğer tamponda zaten karakter varsa bizden karakter istemezler. 
    stdin tamponunda karakter yoksa yeniden okuma talep ederler. Örneğin:

    int ch;

    ch = getchar();
    putchar(ch);

    ch = getchar();
    putchar(ch);

    Biz burada ilk getchar için 'a' karakterine basıp ENTER tuşuna basmış olalım. Bu durumda taponun içeriği şöyle olacaktır:

    Tampon => a\n

    İlk getchar tampondaki sıradaki karakter olan 'a' okuyacaktır. Ancak ikinci getchar tapon dolu olduğu için klavyeden 
    yeni bir giriş istemeyecektir. Tampondaki '\n' karakterini alıp geri dönecektir. Ancak bir tane daha getchar çağrısı 
    yaparsak artık o getchar tampon boş olduğu için klavyeden okuma isteyecektir. 
----------------------------------------------------------------------------------------------------------------------*/
<BURADA KALDIK>
/*----------------------------------------------------------------------------------------------------------------------
    printf fonksiyonunun klavyeden (stdin dosyasından) okuma yapan scanf isimli kardeşi vardır. scanf temel olarak printf gibi kullanılmaktadır. Ancak 
    scanf fonksiyonundaki format karakterleri çıktı ile ilgili değil yapılan giriş ile ilgili bilgi verir. Örneğin printf fonksiyonunda %d "int bir değeri 10'luk
    sistemde ekrana yaz" anlamına gelirken scanf fonksiyonunda %d "int bir nesne için "10'luk sistemde giriş yap" anlamına gelmektedir. scanf fonksiyonunda 
    iki tırnaktan sonraki değişkenlerin önümne & operatörü getirilir. (Bu operatör ileride ele alınacaktır). Örneğin:

    int a;

    scanf("%d", &a);

    Burada klavyeden girilen sayı a nesnesinin içerisine yerleştirilir. scanf fonksiyonundaki iki tırnak içerisine format karakterlerindne başka bir şey 
    yazmayınız. Buraya yazdığınız başka karakterler başka anlamlara gelmektedir. scanf buradaki karakterleri ekrana yazdırmaz. Ekrana bir şey yazdırmak istiyorsanız
    printf fonksiyonunu kullanmalısınız. Örneğin:

    int a;

    scanf("%x", &a);

    Burada %x klavyedne girilen değerin 16'lık sistemde girilmiş olduğunu varsayarak a nesnesine yerleştirecektir. printf fonksiyonuyla scanf fonksiyonu arasındaki
    format karakterleri aynı biçimdedir. Ancak birkaç istisna vardır. printf fonksiyonunda hem float hem de double %f ile yazdırılır. Ancak scanf fonksiyonunda 
    float %f ile double %lf ile okunmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    double a;

    printf("Bir sayi giriniz:");
    scanf("%lf", &a);		/* double %lf ile okunmalıdır */

    printf("%f\n", a);		/* printf fonksiyonunda %lf diye bir formak karakteri yoktur */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tek bir scanf ile birden fazla nesne için okuma yapılabilir. Burada format karakterlerinin dışında şimdilik başka bir karakter bulundurmayınız. 
    Girişler sırasında istenildiği kadar boşluk karakteri (SPACE, TAB, ENTER) bulundurulabilir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b;

    printf("Iki deger giriniz:");
    scanf("%d%d", &a, &b);

    printf("a = %d, b = %d\n", a, b);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
Aşağıdaki programda klavyeden (stdin dosyasından) iki int değe rokunmuş bunların çarpımı ekrana (stdout dosyasına) yazdırılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b;

    printf("Iki deger giriniz:");
    scanf("%d%d", &a, &b);

    printf("%d\n", a * b);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yeni öğrenen tarafından yanlışlıkla scanf'teki format karakterlerinin sonuna \n konulabilmektedir. Bu tamamen başka bir anlama gelir. Böyle yapmayınız. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int ain(void)
{
    int a, b;

    printf("Iki deger giriniz:");
    scanf("%d%d\n", &a, &b);		/* dikkat! yanlışlıkla \n konulmuş! */

    printf("%d\n", a * b);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii scanf ile biz getchar gibi karakter de okuyabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char ch;

    scanf("%c", &ch);
    putchar(ch);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                    14. Ders 07/07/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir işleme yol açan, işlem sonucunda bir değer üretilmesini sağlayan atomlara "operatör" denilmektedir. Bir operatörün işleme soktuğu ifadeleri ise 
    "opeand (operand)" denir. Örneğin a + b ifadesinde + bir operatördür. a ve b bu operatörün operand'larıdır. 

    Operatör konusunu iyi anlayabilmek için operatörleri sınıflandırmak gerekir. Operatörler genel olarak üç biçimde sınıflandırılmaktadır:

    1) İşlevlerine Göre
    2) Operand Sayılarına Göre
    3) Operatörün Konumuna Göre

    İşlevlerine göre sınıflandırmada operatörün hangi amaçla kullanıldığına göre sınıflandırma yapılır. Tipik sınıflandırma şöyle yapılmaktadır:

    1) Artirmetik Operatorleer (Arithmetic Operators): Bunlar toplama, çarpma gibi klasik operatörlerdir. 
    2) Karşılaştırma Operatörleri (Comparision Operators): Bunlar >, <, >=, <=, ==, != gibi iki değeri karşılaştırmak için kullanılan operatörlerdir. Bu 
    operatörlere "ilişkisel operatörler (relational operators)" da denilmektedir 
    3) Mantıksal Operatörler (Logical Operators): Bunlar AND, OR, NOT işlemleri yapan operatörlerdir. 
    4) Gösterici Operatörleri (Pointer Operators): Adreslerle işlemler yapan operatörlerdir. Bunlar her programlama dilinde bulunmazlar.
    5) Bit Operatörleri (Bitwise Operators): Bit operatörleri de pek çok dilde bulunmaktadır. Bunların sayıların karşılıklı bitlerini işleme sokan 
    operatörlerdir. 
    6) Özel Amaçlı Operatörler (Special Purpose Operators): Değişik konulara ilişkin işlem yapan yukarıdaki gruplar içerisine girmeyen operatörlerdir. 

    Operand sayılarına göre operatörler üç grubu ayrılmaktadır:

    1) İki operand'lı Operatörler (Binary Operators): Bunlar iki operand alırlar. Yani bir şeyle bir şeyi işleme sokarlar. Örneğin '+', '*', '/', '-'
    operatörleri iki operand'lı operatörlerdir. 
    2) Tek operand'lı Operatörler (Unary Operators): Bunlar tek bir değeri işleme sokarlar. Örneğin NOT operatörü programlama dillerinde bir değerin NOT'ını
    alır, iki değerin NOT'ını almaz. Ya da örneğin -5 ifadesindeki '-' operatörü çıkartma operatörü değildir. İşaret eksi operatördür ve tek operand'lı bir operatördür. 
    3) Üç operand'lı operatörler (Ternary Operators): Üç operand'lı operatörler aslında çok seyrek bulunurlar. Örneğin C'de üç operand'lı tek bir operatör vardır. 

    Operatörler operatörün operan'larına göre konumuna göre de üçe ayrılmaktadır:

    1) Araek Operatörler (Infix Operators): Bu operatörler iki operand'lıdır ve operand'larının arasına getirilerek kullanılmaktadır. Örneğin a + b işleminde 
    '+' operatörlerinin araek bir operatör olduğuna dikkat ediniz. 
    2) Önek Operatörler (Prefix Operators): Bunlar operand'larının önüne getirilerek kullanılırlar. Örneğin !a gibi bir kullanımda ! operatörü operand'ının önüne 
    getirilmiştir. 
    3) Sonek Operatörler (Postfix Operators): Bunlar da operand'larının sonuna getirilerek kullanılırlar. Örneğin foo() gibi bir ifadede parantezler operatör 
    görevindedir. foo ise bu operatörün operandıdır. Burada operatör operand'ının sonuna getirilmiştir. 

    Bir operatör ele alınırken önce yukarıdaki üç sınıflandırmada da operatörün nereye düştüğü ifade edilmelidir. Sonra operatöre ilişkin başka özellikler belirtilmelidir. 
    Örneğin, "/ operatörü iki operand'lı araek (binart infix) bir artimetik operatördür." Ya da örneğin "! operatörü tek operand'lı öncek (unary prefix) bir mantıksal operatördür". 
    Ya da örneğin "& operatörü iki operand'lı araek bir bit operatörüdür". 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir ifadede birden fazla operatör kullanıldığında bunlar birbirlerine göre belli bir sırada yapılırlar. Bu duruma "operatörler arasındaki öncelik 
    ilişkisi (operator precedency)" denilmektedir. Operatörlerin arasındaki öncelik ilişkisi "operatörlerin öncelik tablousu" denilen bir tabloyla
    betimlenmektedir. Bu tablo satırlardan oluşur. Üst satırrdaki operatörler alt satırdaki operatörlerden daha önceliklidir. Aynı satırdaki operatörler 
    eşit öncelikli biçimde bulunurlar. Ancak aynı satırdaki operatörlerin önceliği "soldan sağa (left to right)" ya da "sağdan sola (right to left)" biçimde
    olabilir. Soldan sağa öncelik demek o satırda bulunanlardan ifade içerisinde hangisi soldaysa o önce yapılır demektir. Sağdan sola öncelik de benzerdir. 
    Aşağıda operatmrlerin öncelik tablosunun iskelet hali verilmiştir:

    ()				Soldan-Sağa
    * /				Soldan Sağa
    + -				Soldan Sağa
    =				Sağdan Sola

    Buradaki () operatörü öncellik parantezini ve fonksiyon çağırma operatörünü anlatmaktadır. Örneğin:

    a = b - c * d;

    İ1: c * d
    İ2: b - İ1
    İ3 a = İ2

    Burada aslında b'dn c * d'nin çıkartıldığına dikkat ediniz. Örneğin:

    a = b / c * d

    Burada / ve * soldan-sağa eşit önceliklidir. İfade içerisinde (öncleik tablosunda değil) solda / olduğu için önce / sonra * yapılacaktır:

    İ1: b / c
    İ2: İ1 * d
    İ3: a = İ2

    Örneğin:

    a = b + c + d;

    Burada solda olan '+' önce yapılacaktır:

    İ1: b + c
    İ2: İ1 + d
    İ3: a = İ2

    Örneğin:

    a = b = c;

    Atama operatörünün sağdan-sola grupta olduğuna dikkat ediniz:

    İ1: b = c
    İ2: a = İ1

    Öncelik tablosundaki satırlarda bulunan operatörler o satırda değişik sırada yazılabilirler. Çünkü aynı satırdaki operatörlerin o satırdaki sırasının 
    bir önemi yoktur. "Soldan-sağa" ya da "sağdan-sola" ifade içerisindkei duruma ilişkindir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    *, /, + ve - operatörleri "iki operand'lı araek (binary infix)" aritmetik operatörlerdir. Bunlar klasik dört işlemi yaparlar. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    % operatörü iki operand'lı araek bir aritmetik operatördür. Bu operatör sol taraftaki operandın sağ taraftaki operanda bölümünden elde edilen kalan değerini 
    üretir. Bu operatörün her iki operandı da tamsayı türlerine ilişkin olmak zorundadır. Öncelik tablosunda * ve / ile soldan sağa eşit öncelik grupta bulunur. 
    Negarit sayının pozitif sayıya bölümünden elde edilen kalan nagtiftir. Pozitif sayının negatif sayıya bölümündne elde edilen kalan pozitiftir. 

    ()				Soldan-Sağa
    * / %			Soldan Sağa
    + -				Soldan Sağa
    =				Sağdan Sola
    
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int result;

    result = 10 % 4;
    printf("%d\n", result);			/* 2 */

    result = -10 % 4;
    printf("%d\n", result);			/* -2 */

    result = 10 % -4;
    printf("%d\n", result);			/* 2 */

    result = -10 % -4;
    printf("%d\n", result);			/* -2 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    + ve - sembolleri hem toplama ve çıkartma operatörü hem de işaret - ve işaret + operatörünü temsil etmektedir. İşaret + ve işaret - operatörleri 
    "tek operand'lı öncek (unary prefix)" operatörlerdir. İşaret - operatörü operand'ının negatif değerini üretir. İşaret + operatörü ise operand'ı ile aynı 
    değeri üretmektedir. (Yani aslında işaert + operatörü bir şey yapmamaktadır). Bu iki operatör öncelik tablosunun ikinci düzeyinde sağdan-sola gruğta bulunurlar:

    ()				Soldan-Sağa
    + -				Sağdan-Sola
    * / %			Soldan Sağa
    + -				Soldan Sağa
    =				Sağdan Sola
    
    Örneğin:

    a = b - - - c;

    İ1: -c
    İ2: -İ1
    İ3: b - İ2
    İ4: a = İ3

    Burada işl - sembolün "çıkartma" diğerlerinin "işaret -" olduğuna dikkat ediniz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int result;
    int a = -4;

    result = 10 - - - - -a;
    printf("%d\n", result);		/* 14 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de programın atomlarına ayrılma aşamasında yan yana en uzun karakter topluluğundan atom yapılmaya çalışılır. C'de sonraki konuda göreceğiniz gibi ++ ve 
    -- operatmrleri de vardır. Dolayısıyla ++ ve -- yan yana yazılırsa iki ayrı işaret + ve işaret - operatörü değil ++ ve -- operatörleri anlaşılır
    Benzer biçimde a>=3 gibi bir ifadede a, >= ve 3 biçiminde üç farklı atom vardır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    ++ ve -- operatörleri "tek operand'lı, öncek ve sonek olarak kullanılabilen" operatörlerdir. Yani biz bu operatörleri ++a gibi de a++ gibi de kullanabiliriz. 
    Bu operatörlerin önek ve sonek kullanımlarında semantik farklılık vardır. ++ operatörüne "artırma (increment)", -- operatörüne "eksiltme (decrement)" operatörleri 
    denilmektedir. ++ operatörü "operandı içerisindeki değeri 1 artır, -- operatörü operandı içerisindeki değeri 1 eksilt anlamına gelir."
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    a = 3;

    ++a;						/* a = a + 1 */;
    printf("%d\n", a);			/* 4 */

    a++;			
    printf("%d\n", a);			/* 5 */

    a = 3;

    --a;						/* a = a - 1 */
    printf("%d\n", a);			/* 2 */

    a--;						
    printf("%d\n", a);			/* 1 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    ++ ve -- operatörleri öncelik tablosunun ikinci düzeyinde sağda-sola grupta bulunmaktadır:

    ()				Solda-Sağa
    + - ++ --		Sağdan-Sola
    * / %			Soldan Sağa
    + -				Soldan Sağa
    =				Sağdan Sola

    Aslında C'nin tek operand'lı (unary) bütün operatörleri zaten öncelik tablosunun ikinci düzeyinde sağdan-sola gruba yerleştirilmiştir. 

    ++ ve -- operatörleri her zaman tablodaki öncelikte yapılır. Ancak sonraki işleme eğer operatörler önek olarka kullanılmışsa artırılmış ya da eksiltilmiş değer, 
    sonek olarak kullanılmışsa artırılmamış ya da eksiltilmemiş değer sokulmaktadır. Örneğin:

    a = 3;
    b = ++a * 2;

    Burada 3 operatör vardır. En önceliklisi ++ operatördür. O halde a 1 artırılacak ve 4 olacaktır. Sonraki işlem * işlemidir. O halde * işlemine 
    artırma öncek yapıldığı için artırılmış değer olan 4 sokulacaktır. Bu duurmda 4 değişkeni 4 değerine olurken b değişkeni 8 olacaktır. Şimdi aynı işlemi
    sonek olarak yapalım:

    a = 3;
    b = a++ * 2;

    Burada da a önce artırılır 4 olur. Ancak sonraki işlem olan * işlemine a'nın artırılmış değeri olan 3 sokulur. Bu durumda a 4 olurken b ise
    6 olacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b;

    a = 3;

    b = ++a * 2;
    printf("a = %d, b = %d\n", a, b);		/* a = 4, b = 8 */

    a = 3;

    b = a++ * 2;
    printf("a = %d, b = %d\n", a, b);		/* a = 4, b = 6 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Önek ve sonek etki aşağıdaki basit örnekle de daha iyi anlaşılabilir:

    a = 3;
    b = --a;

    Burada önce a eksiltilir, 2 olur. Sonraki işlem atama işlemidir. O halde b'ye a'nın eksiltilmiş değeri atanır. Yani b de 2 olacaktır. Fakat örneğin:

    a = 3;
    b = a--;

    Burada yine a bir eksiltilir ve 2 olur. Ancak sonraki işlem olan atama işlemine a'nın eksiltilmemiş değeri olan 3 sokulur. Böylece b 3 olur. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b;

    a = 3;

    b = --a;
    printf("a = %d, b = %d\n", a, b);	/* a = 2, b = 2 */

    a = 3;

    b = a--;		
    printf("a = %d, b = %d\n", a, b);	/* a = 2, b = 3 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii ++ ve -- operatörleri tek başlarına kullanılıyorsa bunların önek ve sonek kullanımları arasında bir fark oluşmaz yani örneğin:

    ++a;

    ile 

    a++;

    arasında bir fark yoktur. Fark ifadede başka operatörler varsa ortaya çıkmaktadır. Örneğin:

    a = 3;
    b = 2;

    c = ++a * b--;

    Burada önce b eksiltilir 1 olur. Sonra a artırılır 4 olur. Çarpma işlemine a'nın artırılmış değeri ancak b'nin eksiltilmemiş değeri sokulur. Bu durumda
    c'ye 8 atanacaktır. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b, c;

    a = 3;
    b = 2;

    c = ++a * b--;
    printf("a = %d, b = %d, c = %d\n", a, b, c);		/* a = 4, b = 1, c = 8 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                15. Ders 19/07/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Tabii başka bir hiçbir operatör yoksa ++ ve -- operatörlerinin önek ve sonek kullanımları arasında bir fark oluşmaz. Örneğin:

    ++a;

    ile 

    a++;

    arasında bir farklılık yoktur. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    a = 3;
    ++a;

    printf("a = %d\n", a);		/* a = 4 */

    a = 3;
    a++;

    printf("a = %d\n", a);		/* a = 4 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    ++ ve -- operatörlerinin operand'larının nesne belirtmesi yani sol tarafa değeri olması gerekir. Örneğin aşağıdaki gibi bir ifade geçerli değildir:

    ++3;		/* geçersiz! */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C (ve C++) standartlarında "tanımsız davranış (undefined behavior)" denilen bir kavram vardır. Tanımsız davranış terimi standartlarda semantik bir tanımın 
    yapılmadığı kodlar için kullanılmaktadır. Tanımsız davranışa yol açan kodlar sentaks bakımdan geçerlidir. Dolayısıyla başarılı bir biçimde derlenirler. 
    Ancak programın çalışma zamanı sırasında sorunlar ortaya çıkabilmektedir. Bu sorunlar "programın çökmesi", "umulmadık biçimde programın çalışması", 
    "hatalı birtakım değerlerin ortaya çıkması" biçiminde olabilir. Bazen tanımsız davranışa yol açam kodlar görünüşte bir soruna yol açmayabilir. Ancak
    programın değişik zamanlarda çalışmtırılması sırasında tutarsızlıklar oluşturabilmektedir. Sonuç olarak bir kod eğer "tanımsız davranışa" yol açıyorsa
    programcının o kodu kullanmaması gerekir. Kullanırdsa artık programın sağlıklı çalışması gaeranti olmaz. Tanımsız davranışların "derleme aşamasına ilişkin değil",
    "prıogramın çalışma zamanına ilişkin" oluşsuzluklar doğrabildiğine dikkat ediniz.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C (ve C++) standartlarında karşılaşılan diğer bir kavram da "derleyiciye bağlı davranış (implementation depnedent (defined) behavior)" kavramıdır. 
    Standratlarda bazı durumlarda ilgili özelliğin derleyiciden derleyiciye değişebileceği belirtilmiştir. Yani ilgili özellik için açık bir belirleme yapmak yerine
    standartlar bu belirlemenin derleyicileri yazanlar tarafından yapılacağını belirtmektedir. Örneğin int türünün (ve char dışındaki diğer türlerin)
    uzunlukları derleyiden derleyiciye değişebilmektedir. Bu uzunluklar "derleyiciye bağlı bir davranışa" yol açmaktadır. Ancak derleyiciye bağlı davranışların
    ilgili derleyicinin dokümantasyonunda dokümante edilmiş olması gerekmektedir. Yani derleyicilerin bir referans gibi kitapları olmalıdır. Orada standartlarda 
    belirtilen "derleyiciye bağlı davranışların" o derleyicide nasıl ele alındığının belirtilmesi gerekmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C (ve C++) standartlarında geçen diğer önemli bir kavram da "belirsiz davranış (unspecified behavior)" kavramıdır. Belirsiz davranışta sınırlı sayıda seçenek 
    söz konusudur. Ancak bu seçeneklerin hangisinin uygulnadığı derleyiciden derleyiciye değişebilir. Bu seçeneklerin hiçbiri normal koşullarda programın 
    çökmesine ya da hatalı sonuçların oluşmasına yol açmamaktadır. Derleyiciler belirsiz davranışlarda hangi seçeneği seçtiklerini dokümante etmek zorunda değillerdir. 
    Belirsiz davranışın tanımsız davranıştan en önemli farkı tanımsız davranışın tamamen patolojik bir durum olması ancak belirsiz davranışın patolojik bir durum olmamasıdır. 
    Belirsiz davranışının derleyiciye bağlı davranıştan en önemli farkı, belirsiz davranış için derleyicilerin bunları dokümante etme zorunluluklarının olmamasıdır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C (ve C++) standartlarındaki önemli bir durum da şudur: C standartları dilin sentaks ve semantik kısıtlarına uyulmadığı durumlarda bu durum için derleyicilerin
    en az bir hata mesajı vermesi gerektiğini belirtmektedir. Ancak standartlara göre geçerli bir program derleyici tarafından başarılı bir biçimde 
    derlenmek zorundadır ancak geçersiz bir program derleyici tarafından yine de başarılı bir biçimde derlenebilir. Yani standartlar geçersiz programların 
    başarılı bir biçimde derlenip derlenmeyeceği konusunda bir yargıda bulunmamaktadır. Gerçekten de pek çok derleyici bazı geçersiz kodları birer uyarı vererek 
    başarılı bir biçimde derlemektedir. Ancak bu durum kodun geçerli olduğu anlamına gelmemektedir. (Dolayısıyla C'de bizim bir durumun geçerliliği hakkında
    derleyicinin kodu başarılı bir biçimde derleyip derlemediğine bakarak karar vermemeiz gerekir. Çünkü derleyiciler geçersiz kodları da başarılı bir biçimde derleyebilmektedir.)
    Tabii bizim dilin kuralarrına tamamen uymamız gerekir. Çünkü bir derleyici geçersiz programı derliyor olsa da diğer bir derleyici onu derlemeyebilir. 
    Ancak kodumuz geçerliyse her derleyici kodumuzu derlemek zorundadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir nesne bir ifadede ++ ya da -- operatörleriyle kullanılmışsa artık o ifadede bir daha o nesne kullanılmamalıdır. Eğer kullanılırsa bu durum
    tanımsız davranışa yol açmaktadır. Bu durumda aşağıdaki gibi kodların hepsi geçerli ancak tanımsız davranışa yol açan kodlardır:

    b = ++a + a;
    b = a++ + a;
    b = ++a + ++a;
    a = ++a;
    b = a + a--;

    Bu kodlarda nasıl bir sonuç elde edileceğinin bir garantisi yoktur. Ancak yukarıdaki kodlar örneğin Java ve C# gibi dillerde "tanımlı (well defined)" 
    kodlardır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de toplam 6 tane karşılaştırma operatörü vardır: 

    <, >, <=, >=
    == != 

    Öncelik tablosunda karşılaştırma operatörleri aritmetik operatörlerden daha düşük öncelikli biçimde bulunmaktadır:

    ()				Soldan-Sağa
    + - ++ --		Sağdan-Sola
    * / %			Soldan-Sağa
    + -				Soldan-Sağa
    <  > <= >=		Soldan-Sağa
    != ==			Soldan-Sağa
    =				Sağdan-Sola

    Karşılaştırma operatörlerinin de öncelik tablosunda iki farklı düzeyde bulunduuna dikkat ediniz. 

    C'de karşılaştırma operatörlerinin ürettiği değerler int türdendir. Eğer önerme doğruysa bu operatörler 1 değerini, yanlışsa 0 değerini üretirler.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int result;

    result = 3 > 1;
    printf("%d\n", result);		/* 1 */

    result = 3 == 1;
    printf("%d\n", result);		/* 0 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki ifadeye dikkat ediniz:

    b = 10 < a < 20;

    Bu ifade matematikte a'nın 10 ile 20 arasında olduğuna ilişkin bir anlama gelse de C'de böyle bir anlama gelmemektedir. C'de bu ifade şöyle ele alınmaktadır:

    İ1: 10 < a	(1 ya da 0 elde edilir)
    İ2: İ1 < 20
    İ3: b = İ2

    Karşılaştırma operatörleri aritmektik operatörlerden düşük önceliklidir. Örneğin:

    a + b > c + d

    Böyle bir işlemde a + b ile c + d karşılaştırılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int result;

    result = 1 + 2 < 3 + 4;
    printf("%d\n", result);		/* 1 */

    result = 1 + (2 < 3) + 4;
    printf("%d\n", result);		/* 6 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de üç mantıksal operatör vardır:

    !		NOT
    &&		AND 
    ||		OR

    && ve || operatörleri iki operand'lı arek operatörlerdir. Ancak ! operatörü tek operand'lı önek bir operatördür. Öncelik tablosunda ! operatörü
    diğer tek operand'lı operatörlerin bulunduğu ikinci düzeydedir. Ancak && ve || operatörleri karşılaştırma operatörlerinden daha düşük önceliklidir. 

    ()				Soldan-Sağa
    + - ++ -- !		Sağdan-Sola
    * / %			Soldan-Sağa
    + -				Soldan-Sağa
    <  > <= >=		Soldan-Sağa
    != ==			Soldan-Sağa
    &&				Soldan-Sağa
    ||				Soldan-Sağa
    =				Sağdan-Sola
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Mantıksal operatörler her zaman int türden değer üretirler. İşlem sonucu Doğru ise 1 değerini, yanlış ise 0 değerini üretmektedirler. Bu operatörler 
    önce operand'larını Doğru ya da Yanlış olarak ele alırlar. Eğer operand sıfır dışı (non-zero) herhangi bir değerdeyse Doğru olarak, sıfır değerindeyse
    Yanlış olarak ele alınmaktadır. Örneğin:

    -3 && 5.7

    Burada -3 Doğru olarak, 5.7 de Doğru olarak ele alınır. Doğru ve Doğru işlemi Doğru sonucunu verir. Doğru için 1 değeri üretilecektir. Örneğin:

    -1 || 0

    Buradan 1 değeri üretilir. Örneğin:

    0 && -8

    Buradan 0 değeri üretilir. 

    ! operatörü Doğruyu Yanlış, Yanlışı Doğru yapan bir operatördür. Öncelik tablosunun ikinci düzeyinde sağdan sola öncelikte bulunur. Örneğin:

    result = !3.5;

    Burada 3.5 Doğru olarak ele alınır. ! operatörü Yanlış değeri için 0 üretmektedir. Örneğin:

    result = !!!-3.2;

    İ1: !-3.2    ---> 0
    İ2: !İ1      ---> 1
    İ3: !İ2      ---> 0
    İ4: result = İ3

    Örneğin:

    result = !0 + 2

    İ1: !0  	---> 1
    İ2: İ1 + 2  ---> 3
    İ3: result = İ2

    && ve || operatörlerinin karşılaştırma operatörlerinden düşük öncelikli olması karşılaştırmanın sonuçlarının mantıksal işlemesokulacağı anlamına gelmektedir. Örneğin:

    result = a > 10 && a < 20;

    Burada iki koşulk da doğruysa 1 değeri diğer durumlarda 0 değeri elde edilecektir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;
    int result;

    printf("Bir deger giriniz:");
    scanf("%d", &a);

    result = a >= 10 && a <= 20;

    printf("%d\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    && ve || operatörlerinin "kısa devre (short circuit)" özelliği vardır. Bu operatörler klasik öncelik tablosu kuralına uymazlar. Bu operatörlerin 
    sağında ne kadar öncelikli operatör olursa olsun bunların önce sol tarafı yapılır. Eğer && operatöründe sol taraf sıfır ise sağ taraf hiç yapılmaz
    sonuç hemen 0 olarak belirlenir. Eğer && operatöründe sol taraf sıfır dışı bir değer ise bu durumda sağ taraf yapılmaktadır. Aynı dırım || operatörü için de 
    geçerlidir. Bu operatörün sol tarafı eğer sıfır dışı bir değerdeyse sağ tarafı hiç yapılmaz ve sonuç 1 olarak belirlenir. Eğer bu operatörün sol tarafı
    sıfır dışı bir değerdeyse bu durumda sağ tarafı yapılır. 

    Aşağıdaki program bu durumun anlaşılması için verilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b;
    int result;

    a = 1;
    b = 3;
    result = a > 10 && ++b > 2;

    printf("result = %d, b = %d\n", result, b);		/* result = 0, b = 3 */

    a = 20;
    b = 3;
    result = a > 10 && ++b > 2;

    printf("result = %d, b = %d\n", result, b);		/* result = 1, b = 4 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Her ne kadar henüz fonksiyonlar konusunu görmediysek de aşağıdaki örnekte bar fonksiyonu çağrılmayacaktır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int foo(void)
{
    printf("foo\n");

    return 0;
}

int bar(void)
{
    printf("bar\n");

    return 1;
}

int main(void)
{
    int result;

    result = foo() && bar();

    printf("%d\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                        16. Ders 21/07/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    && ve || operatörleri aynı ifadede kullanıldığında yine en soldaki operatörün sol tarafı önce yapılır. Aslında kısa devre özelliği yalnızca sonucun hızlı bir 
    biçimde bulunmasına yol açmaktadır. Yoksa kısa devre özelliğinin olmadığı durumla olduğu durum arasında bir sonuç farklılığı oluşmaz. Örneğin:

    ifade1 || ifade2 && ifade3

    Burada önce ifade1 yapılır. Eğer ifade1 sıfır dışı bir değerse başka hiçbir şey yapılmaz. Sonuç 1 olarak elde edilir. Eğer ifade1 sıfır ise bu durumda
    ifade2 yapılır. İfade2 de sıfır ise ifade3 yapılmaz. Burada tüm ifadelerin yapılması için ifade1'in sıfır, ifade2'nin sıfır dışı bir değer vermesi gerekir. 
    Örneğin:

    ifade1 && ifade2 || ifade3

    Burada yine ifade1 önce yapılır. İfade1 sıfır ise ifade2 yapılmaz. Ancak ifade3 yapılır. Eğer ifade1 sıfır dışı bir değerde ise bu durumda ifade2 yapılır. 
    Eğer ifade2 de sıfır dışı ise ifade3 yapılmaz. Aşağıdaki ifadede önce ifade'ün yapılması daha hızlı sonucun elde edilmesine yol açabileceği halde her zaman && ve || operatörlerinin sol tarafı 
    önce yapılmaktadır. Yani aşağıdaki örnekte yine ifade1 ince yapılacaktır.:

    ifade1 && ifade2 || ifade3

    Her ne kadar henüz fonksiyonları görmemiş olsak da aşağıdaki örnek kısa devre özelliğini incelemek amacıyla kullanılabilir. Tabii aslında parantezler de 
    işlemlerin yappılma sırası bakımından bir şeyi değiştirmeyecektir. Örneğin:

    ifade1 && (ifade2 || ifade3)

    Burada her ne kadar || işlemi paranteze alınmışsa da bu parantez içi önce yapılmaz. Çünkü önce yapılsaydı && operatörünün sağ tarafı önce yapılmış olurdu.
    Burada da yine önce ifade1 yapılır. İfade1 0 ise başka bir şey yapılmaz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int foo(void)
{
    printf("foo\n");

    return 0;
}

int bar(void)
{
    printf("bar\n");

    return 0;
}

int tar(void)
{
    printf("tar\n");

    return 1;
}

int main(void)
{
    int result;

    result = foo() || bar() && tar();
    printf("%d\n", result);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Atama operatörü iki operand'lı araek özel amaçlı bir operatördür. Bu operatörün sol tarafındaki operand'ın bir nesne belirtmesi gerekir. Yani sol taraf değeri 
    (LValue) olması gerekir. Atama operatörü de bir değer üretmektedir. Atama operatörünün ürettiği değer sol taraftaki nesneye atanmış olan değerdir. 
    Atama operatörü öncelik tablosunda düşük düzeyde sağdan sola grupta bulunmaktadır. 

    ()				Soldan-Sağa
    + - ++ -- !		Sağdan-Sola
    * / %			Soldan-Sağa
    + -				Soldan-Sağa
    <  > <= >=		Soldan-Sağa
    != ==			Soldan-Sağa
    &&				Soldan-Sağa
    ||				Soldan-Sağa
    =				Sağdan-Sola

    Bu durumda örneğin:

    a = b = 10;

    İ1: b = 10 --> 10
    İ2: a = İ1

    Böylece burada 10 hem b'ye hem de a!ya atanmış olur. Örneğin:

    a = b = 10 + 20;

    Burada a ve b'ye 30 atanmaktadır. Ancak örneğin:

    a = (b = 10) + 20;

    Burada parantez içi önce yapılacağına göre b'ye 10 atanacak ve bu işlemden 10 değeri elde edilecektir. Sonra bu 10 değeri 20 ile toplanıp a'ya atanacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b;

    a = b = 10 + 20;

    printf("a = %d, b = %d\n", a, b);		/* a = 30, b = 30 */

    a = (b = 10) + 20;

    printf("a = %d, b = %d\n", a, b);		/* a = 30, b = 10 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tanımlama sırasında tanımlanan değişkene '=' atomu ile ilkdeğer verilebiliyordu. İlkdeğer vermedeki '=' bir operatör olarak değerlendirilmemektedir. 
    Bu işlem bildirim işleminin bir parçasıdır. Dolayısıyla buradaki '=' bir operatör olarak ele alınmaz. Böyle olunca da buradaki '=' atomunun bir değer 
    üretmesi söz konusu değildir. Örneğin aşağıdaki gibi bir bildirim geçerli değildir:

    int a = b = 10;		/* geçersiz! Buradaki '=' bir operatör değil */

    Ancak aşağıdaki gibi bir bildirim geçerlidir:

    int a = 10, b = a;		/* geçerli */

    C'de bir değişken dekleratörden sonra (bu kavram ileride açıklanacaktır) ancak ilkdeğer vermeden önce faaliyet alanına sokulmuş olmaktadır. Dolayısıyla
    C'de aşağıdaki gibi bir bildirim geçerli ancak anlamsızdır. Örneğin:

    int a = a;

    Burada a yerel bir değişkense a'ya çöp değer, global bir değişkense 0 atanmaktadır. 

    Bazen programcı bir değeri önce atayıp, atanmış değeri başka bir değerle karşılaştırmak isteyebilir. Bunun için atama operatörüne öncelik vermek gerekir. 
    Örneğin:

    (ch = getchar()) != 'q'

    Burada önce getchar ile klavyeden (stdin dosyasından) okunan değer ch değişkenine atanmıştır. Sonra bu atanan değer karşılaştırma işlemine sokulmuştur.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir grup +=, -=, *=, /=, %=, ... biçiminde "bileşik atama operatörü (compund assignment operator)" vardır. Bu operatörlerin hepsi iki operand'lı 
    araek operatörlerdir. "op", +, -, *, / gibi bir operatör belirtmek üzere:

    a op= b

    işlemi tamamen,
    
    a = a op b

    işlemi ile eşdeğerdir. Örneğn:

    a += 2;

    ile

    a = a + 2;

    eşdeğerdir. Örneğin:

    a *= b;

    ile

    a = a * b;

    eşdeğerdir. 

    Bileşik atama operatörleri öncelik tablosunda atama operatör ile sağdan sola aynı grupta bulunmaktadır. 

    ()					Soldan-Sağa
    + - ++ -- !			Sağdan-Sola
    * / %				Soldan-Sağa
    + -					Soldan-Sağa
    <  > <= >=			Soldan-Sağa
    != ==				Soldan-Sağa
    &&					Soldan-Sağa
    ||					Soldan-Sağa
    =, +=, /=, *=,...	Sağdan-Sola

    Örneğin:

    a *= 2 + 3;

    Burada önce 2 ile 3 toplanır. Sonra *= işlemi yapılır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 2;

    a *= 2 * 3;		

    printf("%d\n", a);		/* 12 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bileşik atama operatörleri de değer üretmektedir. Bu operatörlerin ürettiği değerler yine sol taraftaki nesneye atabnuş olan değerlerdir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 2, b;

    b = (a *= 2) * 3;		

    printf("a = %d, b = %d\n", a, b);		/* a = 4, b = 12 */

    a = 2;
    b = a *= 2 * 3;

    printf("a = %d, b = %d\n", a, b);		/* a = 12, b = 12 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Virgül de iki oepardn'lı araek bir operatördür. Önceli tablosunun en düşük öncelikli operatörüdür.

    ()					Soldan-Sağa
    + - ++ -- !			Sağdan-Sola
    * / %				Soldan-Sağa
    + -					Soldan-Sağa
    <  > <= >=			Soldan-Sağa
    != ==				Soldan-Sağa
    &&					Soldan-Sağa
    ||					Soldan-Sağa
    =, +=, /=, *=,...	Sağdan-Sola
    ,					Soldan-Sağa

    Virgül operatörü aslında iki ifadeyi tek bir ifade biçiminde ifade edebilmek için düşünülmüştür. Tipik kullanım biçimi şöyledir:

    ifade1, ifade2

    Virgül operatörünün sağında ne kadar yüksek öncelikli bir operatör olursa olsun, önce onun sol tarafı tamamen yapılır birilir, sonra sağ tarafı 
    tamamen yapılır bitirilir. Virgül operatöründen elde edeilen değer sağ taraftaki ifadenin değeridir. Örneğin:

    a = 10; b = 20;

    Burada iki ayrı ifade vardır. Ancak örneğin:

    a = 10, b = 20;

    Burada tek bir ifade vardır. Bazen sentaks olarak tek bir ifadein gerektiği yerde birden fazla ifade kullanılabilmesi için bu iki ifadenin , operatörü 
    ile birleştirilmesi gerekebilmektedir. Virgül öncelik tablosunun en düşük öncelikli operatörüdür. Dolayısıyla örneğin:

    a = 10, b = 20;

    gibi bir işlem şu sırada yapılır:

    İ1: a = 10
    İ2: b = 20
    İ3: İ1, İ2

    Virgül operatörünün ürettiği değer sağ taraftaki ifadenin değeridir. Yani virgül operatörünün solundaki ifadenin değer üretmekte bir etkisi yoktur. 
    Örneğin:

    c = (a = 10, b = 20);

    Burada parantezler sayesinde en soldaki atama operatörü virgül operatöründen ayrıştrılmıştır. Burada önce parantez içi yapılacaktır. Parantez içerisinde
    virgül operatörü vardır. O zaman viegül operatörünün sol tarafı önce yapılacağında göre önce a = 10 işlemi sonra b = 20 işlemi yapılır. Virgül operatöründen
    elde edilen değer sağ taraftaki ifadenin değeri olduğuna göre buradan 20 elde edilecektir. İşte bu 20 aynı zamanda c'ye atanmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b, c;

    c = (a = 10, b = 20);
    printf("a = %d, b = %d, c = %d\n", a, b, c);		/* a = 10, b = 20, c = 20 */
    
    return 0;
}
/*----------------------------------------------------------------------------------------------------------------------
    Tabii her virgül virgl operatörü değildir. Örneğin bildirim yaparken kullandığımız virgüller bu bağlamda bir operatör belirtmezler. Buradaki virgüller
    ayıraç atom görevindedir. Örneğin:

    int a, b, c;		/* Buradaki virgüller operatör görevinde değil */

    Örneğin bir fonksiyon çağırırken argümanları ayırmak için kullandığımız virgül de ayrıraç atom görevindedir:

    foo(a, b, c);		/* Buradaki virgüller de operatör görevinde değil */

    Eğer argümandaki ',' atomumun virgül operatör olması isteniyorsa bu durumda parantezler kullanılmalıdır. Örneğin:

    foo(a, b);

    Buradaki ',' operatör görevinde değildir. Dolayısıyla foo fonksiyonunun iki parametresi vardır. Fakat örneğin:
    
    foo((a, b));

    Buradaki virgül artık paranteze alındığı için operatör görevinddir. Parantez içerisinden b'nin değeir elde edilecektir. Dolayısıyla fonksiyonun aslında
    tek parametresi vardır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b;

    printf("%d\n", (a = 10, b = 20));		/* tuhaf ama geçerli, b yazdırılıyor */
    printf("%d\n", a);		/* 10 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Virgül operatörü de soldan-sağa önceliklidir. Yani bir ifadede birden fazla virgül operatörü bulunabilir. Örneğin:

    ifade1, ifade2, ifade3

    Burada işlemler şöyle yütülür:

    İ1: ifade1, ifade2
    İ2: İ1, ifade3

    Yani burada sonuçta bu ifadeler soldan sağa sırasıyla yapılacaktır. Buradan elde edilen toplam sonuç en sağdaki ifadenin değeridir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    a = (10, 20, 30);		/* geçerli ama tuhaf, a'ya 30 atanır */

    printf("%d\n", a);		/* 30 */
    
    return 0;
}

*----------------------------------------------------------------------------------------------------------------------
    C'de ';' ifadeleri sonlandırmak için kullanılmaktadır. Bu görevdeki atomlara programlama dillerinde "sonlandırıcı (terminator)" denilmektedir. 
    Biz ifadenin sonuna ';' koyduğumuzda artık o ifadeyle sonraki ifadenin ayrı ifadeler olduğunu derleyiciye söylemiş oluruz. Eğer bir ifadenin sonundaki 
    ';' unutulursa derleyici önceki ifadeyle sonraki ifadeyi tek bir ifade olarak ele alır. Bu da sentaks hatasına yol açar. Örneğin:

    a = 10 
    b = 20;

    Burada muhtemelen a = 10'dan sonraki ';' atomu unutulmuştur. O halde derleyiciye göre burada tek bir ifade vardır. Ancak bu ifade geçerli değildir. 

    Bazı dillerde sonlandırıcı olarak LF karakteri kullanılmaktadır. Dolayısıyla o dillerde aynı satıra tek bir ifade yazılmak zorundadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Biz şimdiye kadar zaten var olan fonksiyonları çağırdık. Artık biz de fonksiyon yazacağız. Bir fonksiyonun yazılmasına C standartlarında "fonksiyonun
    tanımlanması (function definition)" denilmektedir. Fonksiyon tanımlamanın genel biçimi şöyledir:

    <fonksiyonun geri dönüş değerinin türü> <fonksiyon ismi> ([parametre bildirimi])
    {
        /* ... */
    }

    Örneğin:

    int foo()
    {
        /* ... */
    }

    Burada int fonksiyonun geri dönüş değerinin türüdür. foo ise fonksiyonun ismini belirtir. Fonksiyon parametre değişkenlerine sahip değildir. 
    Fonksiyonun geri dnüş değerinin türü klasik C'de (yani C90'da) yazılmak zorunda değildi. Bu duurmda sanki "int" yazılmış gibi işlem yapılıyordu. 
    Ancak C99 ile birlikte fonksiyonun geri dönüş değerinin türünün yazılması zorunlu hale getirilmiştir.

    bar()		/* C90'da geçerli C99 ve sonrasında geçerli değil */
    {
        /* ... */
    }

    Eğer fonksiyonun parametresi yoksa parametre parantezinin içi boş bırakılabilir ya da parametre parantezinin içerisine void yazılabilir. İkisi arasında
    hiçbir farklılık yoktur. Biz kursumuzda genel olarak parametresiz fonksiyonlarda parametre parantezinin içine void anahtar sözcüğünü yazacağız. 
    Ancak programcıların bir bölümü hiçbir şey yazmamayı tercih etmektedir. 

    Biz kurusumuzdki örneklerde "öylesine uydurulmuş fonksiyon isimleri" olarak foo, bar, tar, zar gibi isimleri kullanacağız. Bu isimlerin hiçbir özel 
    anlamı yoktur. Örneklerde öylesine uydurulmuş isimlerdir. 

    Tanımlanan her fonksiyonun bir ana bloğu vardır. Buna "fonksiyonun gövdesi (function body)" de denilmektedir. 

    C'de iç içe (nested) fonksiyon tanımlaması yapılamaz. Her fonksiyon biribirinin dışında ve global düzeyde tanımlanmak zorudadır. Örneğin:

    int foo()
    {
        int bar()			/* geçersiz! */
        {
            /* ...*/
        }
        /* ... */
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        17. Ders 26/07/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyon çağrıldıktan sonra onu çağıran fonksiyona ilettiği değere "geri dönüş değeri (return value)" denilmektedir. Fonksiyonun geri dönüş değerinin
    bir türü vardır. Bu tür fonksiyon isminin soluna yazılır. Geri dönüş değerini oluşturmak için return deyimi kullanılır. return deyiminin genel biçimi şöyledir:

    return [ifade]

    return deyimi hem fonksiyonu sonlandırır hem de geri dönüş değerini oluşturur. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int foo(void)
{
    printf("foo\n");

    return 100;
}

int main(void)
{
    int result;

    result = foo() * 2;
    printf("%d\n", result);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun geri dönüş değerinin olması onu kullanmayı zorunlu hale getirmez. Yani fonksiyonların geri dönüş değerlerini fonksiyonu çağıran 
    hiç kullanmayabilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int foo(void)
{
    printf("foo\n");

    return 100;
}

int main(void)
{
    foo();			/* foo çağrıldı ancak geri dönüş değeri kullanılmadı, tamamen geçerli */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon tanımlarken geri dönüş değerinin tütü yerine "void" anahtar sözcüğü yazılırsa bu durum "fonksiyonun bir değer geri föndürmediği" anlamına gelmektedir. 
    Böyle fonksiyonlar geri dönüş değerinin kullanıldığı bir ifadede kullanılamazlar. Örneğin:

    void foo(void)
    {
        printf("foo\n");
    }
    ...
    x = foo();			/* geçersiz! foo'nun geri dönüş değer yok */
    x = foo() * 2;		/* geçersiz! foo'nun geri dönüş değeri yok */
    foo();				/* geçerli */
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void foo(void)
{
    printf("foo\n");
}

int main(void)
{
    foo();			
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Geri dönüş değeri void olan fonksiyonlara "void fonksiyonlar" da denilmektedir. void fonksiyonlar da return deyimi kullanılabilir ancak return deyiminin yanına bir ifade 
    yazılamaz. Örneğin:

    void foo(void)
    {
        printf("foo\n");

        return;			/* geçerli */
    }

    void bar(void)
    {
        printf("foo\n");

        return 10;			/* geçersiz! void fonksiyon bir değerle geri döndürülemez */
    }

    Pekiyi o zaman void fonksiyonlardaki return deyimi ne işe yaramaktadır? İşte void fonksiyonlardaki return deyimleri fonksiyonu bir koşul altında 
    erken sonlandırmak için kullanılabilir. void fonksiyonlarda return kullanılmazsa akış fonksiyonun ana bloğunu bitirdiğinde zaten fonksiyon sonlanmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void foo(void)
{
    printf("foo\n");

    return;			/* geçerli */
}

int main(void)
{
    foo();			
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Geri dönüş değeri void olmayan fonksiyonlarda eğer akış return deyimini görmeden ana blok sonlanırsa bu durum C'de geçerlidir (halbuki C#, Java gibi dillerde
    geçersizdir). Bu durumda geri dönüş değeri olarak çöğ bir değer elde edilmektedir. Genellikle derleyiciler böylesi durumlarda bir uyarı mesajı ile programcıyı
    uyarmaktadır. Ancak geri dönüş değeri void olmayan fonksiyonlarda return deyiminde return ifadesinin mutlaka bulundurulması gerekir. Örneğin:

    int foo(void)
    {
        printf("foo\n");
    }					/* dikkat! fonksiyon çöp değerle geri dönüyor */

    int bar(void)
    {
        printf("bar\n");

        return;			/* geçersiz! return anahtar sözcüğünün yanında bir ifade olması gerekirdi */
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int foo(void)
{
    printf("foo\n");
}

int main(void)
{
    int result;

    result = foo();					/* dikkat! geçerli ama çöp değer elde ediliyor */
    printf("%d\n", result);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Mademki akış return deyimini göründe fonksiyon sonlanmaktadır. O halde return deyiminin altına deyim yazmanın bir anlamı yoktur. Bu durum anlamsız olsa da
    C'de geçerlidir. Örneğin:

    int foo(void)
    {
        printf("foo\n");

        return 100;

        printf("foo ends...\n");		/* unreachable code */
    }

    Akışın asla ulaşamayacağı erişilemeyen bölgelere İngilizce "unreachable code" denilmektedir. Derleyiciler erişilemeyen kodları tespit edip bir uyarı 
    mesajı ile programcıya bildirebilmektedir. Pek çok derleyici erişilemeyen kodları tamaman koddan çıkartarak bir optimizasyon yapmaktadır. Bu optimizasyon
    temasına "dead code elimination" denilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de main fonksiyonun geri dönüş değeri int türden olmak zorundadır. Ancak derleyiciler eğer isterlerse main fonksiyonunun başka türlerden geri 
    dönüş değerine sahip olmasına izin verebilirler. main fonksiyonundaki return deyimi aynı zamanda programı da sonlandırmaktadır. İşletim sistemleri dünyasında
    çalışmakta olan programlara "process" denilmektedir. main fonksiyonu sonlandığında return deyimindeki ifade işletim sistemine "exit code" olarak iletilmektedir. 
    İşletim sistemleri bu exit kodu alır, eğer başka bprosesler isterse belli koşullarda onlara verebilir. Ancak exit kodunun kaç olduğuyla ilgilenmez. 
    Fakat geleneksel olarak C'de başarılı ve mutlu sonlanmalar için exit kodu olarak 0, başarısız sonlanmalar için sıfır dışı değerler kullanılmaktadır.
    Biz örneklerimizde main fonksaiyonunu her zaman 0 ile geri döndüreceğiz. Aslında C standartlarında main fonksiyonuna özgü olarak, eğer main fonksiyonunda hiç 
    return kullanılmazsa sanki ana bloğun sonuna return 0 yazılmış gibi işlem uygulanmaktadır. Yani main fonksiyonunda biz hiç return yazmasak da zaten return 0 
    yazmış gibi bir durum oluşmaktadır. Tabii bu durum yalnızca main fonksiyonuna özgüdür. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyonların geri dönüş değerleri geçici nesne yoluyla onu çağıran fonksiyona iletilmektedir. Programın akışı return deyimini gördüğünde önce derleyici
    geri dönüş değeri türünden geçici bir nesne yaratır. Sonra return ifadesini bu geçici nesneye atar. Biz geri dönüş değerini kullandığımızda aslında o geçici 
    nesneyi kullanmış oluruz. Fonksiyonun çağrısı bittiğinde bu geçici nesne de derleyici tarafındna otomatik olarak yok edilmektedir. Örneğin:

    int foo(void)
    {
        /* ... */

        return ifade;
    }
    ...
    x = foo() * 2;

    Burada aslında arka planda aşağıdaki gibi işlemler gerçekleşmektedir:

    int temp = ifade;		/* akış return deyimine geldiğinde */
    ...
    x = temp * 2;
    /* temp yok ediliyor */

    Bir nesne yaratılırken ona değer atanmasına "ilkdeğer verme (initialization)" denildiğini anımsayınız. Fonksiyonun geri dönüş değerinin atanacağı
    geçici nesne return ifadesiyle yaratıldığı için aslında return işlemi geçici nesneye bir ilkdeğer verme işlemi olarak da alınmaktadır.

    O halde fonksiyonun geri dönüş değerinin türü aslında return işlemiyle yaratılacak olan geçici nesnenin türünü belirtir. Bizim ileride atama işlemi hakkında söyleceğimiz 
    her şey return işlemi için de geçerlidir. Derleyiciler genel olarak mümkün olduğunca return işlemi sırasındaki geçici nesneleri CPU yazmaçlarında 
    yaratmaktadır. 

    Fonksiyon çağrıları C'de her zaman sağ taraf değeri (rvalue) belirtmektedir. Yani return işlemiyle yaratılanm bu geçici nesneye biz atama yapamayız. Örneğin:

    foo() = 10;		/* geçersiz! */

    void fonksiyonlarda böyle bir geçici nesnenin hiç yaratılmayacağına da dikkat ediniz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyonların dış dünyadan aldıkları değerlere "parametre (parameter)" denilmektedir. C'de fonksiyon parametreleri parametre parantezinin içerisinde
    tür ve değişken ismi belirtilerek ve ',' atomu ile parametreler ayrılarak bildirilmektedir. Örneğin:

    void foo(int a, long b, double c)
    {
        /* ... */
    }

    void bar(double a, int b)
    {
        /* ... */
    }

    Parametreler aynı türden olsa bile tür belirten sözcüğünyeniden yazılması gerekmektedir. Örneğin:

    void foo(int a, b)		/* geçersiz! */
    {
        /* ... */
    }

    Yuklarıdaki tanımlama geçersizdir. Şöyle yapılması gerekirdi:

    void foo(int a, int b)
    {
        /* ... */
    }

    Parametreli bir fonksiyon parametre sayısı kadar "argümanla" çağrılmalıdır. Argümanlar herhangi birer ifade olabilir. Örneğin:

    void foo(int a, int b)
    {
        /* ... */
    }
    ...
    foo(10 + 20, 30 + 40);		/* geçerli */

    Argüman olan ifadeler yine ',' atomu ile ayrılmaktadır. Fonksiyonu çağırırken yazılan ifadelere "argümen (argument)" denilmektedir. 

    Parametreli bir fonksiyon çağrıldığında önce argümanların değerleri hesaplanır. Sonra argümanlardan parametre değişkenlerine karşılık bir atama 
    yapılır. Sonra da akış fonksiyona geçirilir. Yani C'de parametre aktarımı atama (ya da kopyalama) biçiminde yapılmaktadır. Örneğin:

    void foo(int a, int b)
    {
        /* ... */
    }
    /* ... */
    int x = 10, y = 20;

    foo(x + 1, y + 2)

    Burada foo fonksiyonu çağrıldığında önce x + 1 ve y + 2 ifadelerinin değerleri hesaplanacak sonra x + 1 değeri a'ya, y + 2 değeri ise b'ye atanacaktır. 
    Sonra da akış fonksiyona geçirilecektir. Parametre değişkenlerinin bağımsız ayrı nesneler olduğuna dikkat ediniz. Fonksiyon çağırma işlemi argümanlardan 
    parametre değişkenlerine yapılan gizli bir atama işlemini gerektirmektedir. 
    
    Aslında izleyen paragraflarda da göreceğimiz gibi fonkdiyonun parametre değişkenleri fonksiyon çağrıldığında yaratılmaktadır. Dolayısıyla 
    fonksiyon çağırma işlemi aslında parametre değişkenlerine ilkdeğer verme işlemi gibi de ele alınabilir.
    
    O halde C'de atama anlamına gelen üç durum vardır:

    1) Açıkça '=' operatörü ile yapılan atamalar
    2) return işlemi sırasında geçici nesneye yapılan atamalar
    3) Fonksiyon çağırma sırasında argümanlardan parametre değişkenlerine yapılan atamalar

    Biz ileride atama işlemi için söyleyeceğimiz her şey bu üç durum için de geçerli olacaktır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void foo(int a, int b)
{
    printf("a = %d, b = %d\n", a, b);
}

int main(void)
{
    int x = 100, y = 200;

    foo(10, 20);
    foo(10 + 1, 20 + 2);
    foo(x, y);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bazen fonksiyonlar parametreleriyle aldıkları değeri birtakım işlemlere sokup onu geri dönüş değeri olarak verebilirler.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int main(void)
{
    int result;

    result = add(10, 2);
    printf("%d\n", result);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir atama işleminde kaynak tür ile hedef tür farklı olabilir. Örneğin:

    a = b;

    Burada a ve b'nin türleri farklı olabilir. Ancak bu konu ileride özel olarak ele alınacaktır. Siz şimdilik bu konu ele alınana kadar atama işleminde 
    kaynak türle hedef türü aynı türden yapmaya özen gösteriniz. Örneğin:

    #include <stdio.h>

    double add(int a, int b)
    {
        int result;

        result = a + b;

        return result;		/* dikkat! farklı türler birbirlerine atanıyor, ileri ele alınacak */
    }

    int main(void)
    {
        double x = 2.3, y = 4.5;
        int result;

        result = add(x, y);		/* dikkat farklı türler biribirene atanıyor, ileride ele alınacak */
        printf("%d\n", result);

        return 0;
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de parametreleriyle aldıkğı değerler üzerinde işlemler yaparak sonucu geri dönüş değeri olarak veren bir standart matematiksel fonksiyon vardır. 
    Bu fonksiyonları kullanmadan önce <math.h> dosyası include edilmelidir. Örneğin sqrt fonksiyonun parametrik yapısı şöyledir:

    double sqrt(double x);

    Fonksiyon parametresiyle aldığı double sayının kareköküne geri dönmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

int main(void)
{
    double val, result;

    printf("Bir deger giriniz:");
    scanf("%lf", &val);

    result = sqrt(val);
    printf("%f\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    pow fonksiyonu bir sayının belli bir kuvvetine geri dönmektedir. Fonksiyonun parametrik yapısı şöyledir:

    double pow(double a, double b);

    Fonksiyon a üzeri b işlemine geri dönmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

int main(void)
{
    double a, b, result;

    printf("Taban:");
    scanf("%lf", &a);

    printf("Us:");
    scanf("%lf", &b);

    result = pow(a, b);
    printf("%f\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    sin, cos, tan, asin, acos, atan fonksiyonları trigonometrik işlemler yapmaktadır. Bu fonksiyonların parametreleri ve geri dönüş değerleri double 
    türdendir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

int main(void)
{
    double result, radian;

    result = sin(3.141592653589793238462643 / 2);
    printf("%f\n", result);

    radian = asin(result);
    printf("%f\n", radian);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    round fonksiyonu double bir değeri parametre olarak alıp ona en yakın tamsayıyı yine double bir değer olarak vermektedir. 
    Fonksiyonun parametrik yapısı şöyledir:

    double round(double x);
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

int main(void)
{
    double result;

    result = round(3.6);
    printf("%f\n", result);		/* 4 */

    result = round(3.4);
    printf("%f\n", result);		/* 3 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte roundf ve roundl fonksiyonları da standartlara eklenmiştir. Bunların parametrik yapıları şöyledir:

    float roundf(float x);
    long double roundl(float x);

    Yine C99 ile birlikte tamsayı değerlere geri dönen aşağıdakai fonksiyonlar da eklenmiştir. 

    long int lround(double x);
    long int lroundf(float x);
    long int lroundl(long double x);
    long long int llround(double x);
    long long int llroundf(float x);
    long long int llroundl(long double x);
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

int main(void)
{
    long result;

    result= lround(3.6);
    printf("%ld\n", result);		/* 4 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    floor isimli fonksiyon double bir sayıya en yakın ondan küçük ya da ona eşit tamsayıyı bize double türden vermektedir. ceil ise tam ters işlem yapar. Yani 
    bir double sayıdan büyük ya da ona eşit en yakın tamsayıyı double türden vermektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

int main(void)
{
    double result;

    result= floor(3.9);
    printf("%f\n", result);		/* 3 */

    result = floor(-3.9);
    printf("%f\n", result);		/* -4 */

    result = ceil(3.1);
    printf("%f\n", result);		/* 4 */

    result = ceil(-3.1);
    printf("%f\n", result);		/* -3 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte float ve long double için aşağıdaki fonksiyonlar da standartlara eklenmiştir:

    float floorf(float x);
    long double floorl(long double x);
    float ceilf(float x);
    long double ceill(long double x);
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    18. Ders 28/07/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bildirilen bir değişkenin kullanılabildiği program aralığına "faaliyet alanı (scope)" denilmektedir. C'de üç faaliyet alanı vardır:

    1) Blok Faaliyet Alanı (Block Scope): Yalnızca bir blokta o bloğun kapsadığı bloklarda tanınma aralığıdır.
    2) Dosya Faaliyet Alanı (File Scope): Tüm fonksiyonlarda yani her yerde tanınma aralığıdır. 
    3) Fonksiyon Faaliyet Alanı (Function Scope): Bir fonksiyonun her yerinde tanınma aralığıdır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de yerel değişkenler blok faaliyet alanı kuralına uyarlar. Yani bildirildikleri yerden bildirildikleri bloğun sonuna kadarki bölgede kullanılabilirler.
    Anımsanacağı gibi C90'da yerel değişkenler blokların başlarında yani blokların ilk işlemleri olacak biçimde bildirilmek zorundaydı. Bu kural C99'da
    kaldırıldı. Yerel değişkenlerin bloğun herhangi bir yerinde bildirilmeleri sağlandı. Örneğin:

    void foo(void)
    {
        printf("foo\n");
        
        int a;				/* C90'da geçersiz! C99'dan itibaren geçerli */
    }

    void bar(void)
    {
        int a;				/* C90'da da geçerli */

        printf("bar\n");
    }

    Bir fonksiyonun içerisinde içerisinde istenildiği kadar iç içe ve ayrık blok oluşturulabilir. Örneğin:

    void foo(void)
    {
        {
            printf("Ok\n");
            /* .... */
        }
        {
            printf("Ok\n");
        }
    }

    Yerel değişkenler bildirildikleri yerden itibaren bildirildikleri bloğun sonuna kadarki bölgede kullanılabilirler. Örneğin:

    void foo(void)
    {
        int a;

        {
            int b;

            a = 10;		/* geçerli, a faaliyet gösteriyor */
            b = 20;		/* geçerli, b faaliyet gösteriyor */
        }
        printf("%d\n", a);		/* geçerli, a faaliyet gösteriyor */
        printf("%d\n", b);		/* geçersiz! b burada faaliyet göstermiyor */
    }

    void bar(void)
    {
        a = 100;		/* geçersiz, a burada faaliyet göstermiyor */
    }

    Tabii C99 ve sonrasında bildirilen bir yeral değişken bildirim yerinden önce de kullanılamaz. Örneğin:

    void bar(void)
    {
        a = 10;		/* geçersiz! a faaliyet göstermiyor */

        int a;

        a = 20;		/* geçerli */
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de aynı faaliyet alanına ilişkin aynı isimli birden fazla değişken tanımlanamaz. Ancak farklı faaliyet alanlarına ilişkin aynı isimli değişkenler 
    tanımlanabilir. Aynı bloğun farklı yerlerinde tanımlanan değişkenler bu bakımdan aynı faaliyet alanı içerisinde kabul edilirler. Bu nedenle C'de aynı 
    blok içerisinde aynı isimli bir den fazla değişken tanımlanamaz. Ancak farklı bloklarda aynı isimli değişkenler tanımlanabilir. Örneğin:

    void foo(void)
    {
        int a;
        /* ....*/
        double a;		/* geçersiz! aynı blok içerisinde aynı isimli tek bir değişken tanımlanabilir */
    }

    Örneğin:

    void bar(void)
    {
        int a;
        {
            double a;		/* geçerli iç içe bloklarda aynı isimli değişkenler tanımlanabilir */
            /* ... */
        }
        /* ... */
    }

    void tar(void)
    {
        int a;		/* geçerli */
        /* ... */
    }

    Farklı bloklardaki aynı isimli değişkenler aslında tamamen farklı nesneler belirtirler, bunların yalnızca isimleri aynıdır. Örneğin:

    void foo(void)
    {
        int a;

        {
            int a;		/* Bu tamamen farklı bir a */
            /* ... */
        }
    }

    C'de aynı blokta birden fazla aynı isimli değişken faaliyet gösteriyorsa o blokta o değişken kullanıldığında her zaman "dar faaliyet alanına sahip olan"
    değişkenin kullanılmış olduğu kabul edilir. Örneğin:

    {
        int a;

        {
            int a;
        
            a = 10;		/* dar faaliyet alanına sahip olan iç bloktaki a'dır */
        }

        a = 20;		/* dış bloktaki a, zaten iç bloktaki a burada faaliyet göstermiyor */
    }

    İç içe bloklarda aynı isimli değişkenlerin bildirildiği durumda iç blokta dış bloktaki değişkene erişmenin herhangi bir yolu yoktur. Bu duruma 
    "iç bloktaki değişkenin dış bloktaki gizlemesi (hiding)" denilmektedir.

    C99 ve sonrasında bir yerel değişken bloğun herhangi bir yerinde bildirilebildiğine göre bildirim yerine kadar üst bloktaki dğeişken faaliyet gösteriyor
    durumdadır ve henüz üst bloktaki aynı isimli değişken gizlenmemiştir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        int a;

        {
            a = 20;		/* ana bloktaki a */

            int a = 30;

            printf("%d\n", a);		/* iç bloktaki a */
        }

        printf("%d", a);			/* ana bloktaki a */

        return 0;
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bildirimi fonksiyonların dışında yapılan global değişkenler "dosya faaliyet alanı (file scope)" kuralına uyarlar. Yani kaynak dosyanın her yerinde, 
    tüm fonksiyonların içerisinde biz global değişkenleri kullanabiliriz. Ancak C'de derleme işleminin de bir yönü vardır. Bu yön yukarıdan aşağıya 
    doğrudur. Bir değişken bildirilmeden önce kullanılamaz. Bu nedenle bir global değişkeni aşağıda bir yerde bildirirsek bildirim yerinde aşağıya kadar 
    her yerde kullanırız. Ancak genel olarak global değişkenler kaynak dosyanın tepesinde bildirilirler. Öneğin:

    int a;

    void foo(void)
    {
        a = 20;						/* global olan a */
    }

    int main(void)
    {
        a = 10;						/* global a */
        foo();
        printf("%d\n", a);			/* global a, 20 çıkacak */
    }

    Bir global değişken için en iyi tanımlama yeri kaynak kodun tepesidir. Örneğin:


    void foo(void)
    {
        a = 10;		/* geçersiz! henüz derleyici a'yı görmedi */
    }
    
    int a;
    
    void bar(void)
    {
        a = 20;		/* geçerli  */
    }

    void tar(void)
    {
        a = 30;		/* geçerli */
    }

    Bir global değişkenle aynı isimli bir yerel değişken tanımlanabilir. Çünkü bunların faaliyet alanları farklıdır. Bir blokta aynı isimli birden fazla
    değişken faaliyet gösteriyorsa o blokta dar faaliyet alanına sahip olan değişkene erişilmektedir. Örneğin:

    #include <stdio.h>

    int x;

    void foo(void)
    {
        double x;

        x = 20;		/* yerel x kullanılıyor */
    }

    int main(void)
    {
        x = 10;		/* global x */

        foo();
        printf("%d\n", x);		/* global x, 10 çıkacak */

        return 0;
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de istisnai olarak ilkdeğer verilmemiş birden fazla aynı isimli global değişken tanımlanabilir. Buna "tentative definition" denilmektedir. 
    Bu durumda aslında toplamda tek bir nesne yaratılır. Yani birden fazla tanımlama bu istisnasi durumda birden fazla nesne anlamına gelmez. 
    Aynı nesnenin gereksiz bir biçimde yeniden "tentative" olarak belirtlmesi anlamına gelir. Tabii bu durum yerel değişkenler için söz konusu değildir. 
    Tentative tanımlama tamamen istisnai bir durumdur ve yalnızca global değişkenler için söz konusudur. Bu nedenle aşağıdaki tanımlama geçerlidir. 
    Ancak aşağıdaki kodda tek bir x nesnesi vardır. "Tentative" sözcüğü "deneme niteliğinde" gibi bir anlama gelmektedir:

    #include <stdio.h>

    int x;
    int x;		/* geçerli, özel bir durum, tentative definition */

    int main(void)
    {
        x = 10;
        printf("%d\n", x);

        return 0;
    }

    int x;		/* geçerli, tentative definiton */

    Tentative tanımlama olması için global değişkene ilkdeğer verilmemiş olması gerekmektedir. Aynı isimli bir global değişkene bir kez ilkdeğer verilebilir. 
    Ancak birden fazla kez ilkdeğer verilemez. Örneğin:

    int a = 10;		/* geçerli, tentative değil */
    int a;			/* geçerli, tentative, aslında burada bir a yaratılmıyor */

    Ancak örneğin:

    int a = 10;
    int a = 20;			/* geçersiz! tentative değil */

    Bu kural ileride yeniden başka bir konunun içerisinde ele alınacaktır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Daha önce de belirtidliği gibi içerisine henüz değer atanmamış yerel değişkenin içeisinde bellekte daha önceden kalmış olan "çöp bir değer (garbage value)"
    bulunur. Ancak içerisine henüz değer atanmamış global bir değişkende her zaman 0 değeri olması garanti edilmiştir. 
    Ayrıca C'de içerisinde çöp değerlerin olduğu yerel değişkenlerin kullanılması "tanımsız davranışa (undefined behavior)" yol açmaktadır
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int x;

int main(void)
{
    int a;

    printf("%d\n", a);		/* geçerli, içerisine henüz değer atanmamış yerel değişkenler içerisinde çöp değer vardır (tanımsız davranış) */
    printf("%d\n", x);		/* içerisine henüz değer atanmamış global nesneler içerisinde her zaman 0 olur */
s
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Genel olarak global değişkenlerin yalnızca gerektiği durumlarda kullanılması gerekir. Yerel değişkenlerle yapabileceğimiz şeyler için global değişken 
    tanımlamak kötü bir tekniktir. Örneğin programımızda yalnızca main fonksiyonu olsun. Bu durumda global bir değişken tanımlamaya hiç gerek yoktur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyonların parametre değişkenleri faaliyet alanı bakımından ana bloğun başında tanımlanmış olan yerel değişkenler gibidir. Örneğin:

    void foo(int a, int b)
    {
        /* ... */
    }

    Buradaki a ve b parametre değişkenleri faaliyet alanı bakımından aşağıdaki fonksiyonla eşdeğerdir:

    void foo()
    {
        int a;
        int b;

        /* ... */
    }

    Görüldüğü gibi fonksiyonların parametre değişkenleri "blok faaliyet alanı (block scope)" uymaktadır. Yani yalnızca o fonksiyonda kulalnılabilirler. 
    Dolayısıyla farklı iki fonksiyonun parametre değişkenleri aynı isimde olabilir. Örneğin:

    void foo(int a)
    {
        /* ... */
    }

    void bar(int a)		/* geçerli, a yalnızca bu fonksiyonda kullanılabilir. 
    {
        /* ... */
    }

    Mademki fonksiyonun parametre değişkenleri faaliyet alanı bakımından ana bloğun başında bildirilen değişkenler gibidir, o halde parametre değişkeni ile
    aynı isimli fonksiyonun ana bloğunda bir değişken bildirilemez. Örneğin:

    void foo(int a)
    {
        int a;		/* geçersiz! parametre değişkeni olan a da aynı faaliyet alanına sahip */

        /* ... */
    }

    Fakat örneğin:

    void foo(int a)
    {
        {
            int a;		/* geçerli! iç içe yerel bloklarda aynı isimli değişkenler tanımlanabilir */

            /* ... */
        }
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de fonksiyon faaliyet alanına sahip tek değişken "goto etiketleridir". Goto deyimi deyimlerin ele alındığı geşecek bölümlerde görülecektir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Daha önceden de belirtildiği gibi C Programlama Dili "Prosedürel Programlama Modeline (Procedural Programming Paradigm)" uygun tasarlanmıştır. 
    Prosedürel programlamada "fonksiyonlar" birer yapı taşıdır. Programlar fonksiyonların birbirlerini çağırması biçiminde organize edilirler. Halbuki 
    Nesne Yönelimli Programlama Modelinin yapı taşı "sınıf" denilen kavramdır. C++ Programala Dili zaten C Programa Dilinin "Nesne Yönelimli Programalama Modelinin"
    uygulanabilmesi için genişletilmiş bir biçimidir. 

    Pekiyi prosedürel teknikte neden program fonksiyonların birbirlerini çağırması biçiminde organize edilmektedir? Yani neden bürün program main fonksiyonunda
    yazılıp bitirilmemektedir? Programın fonksiyonlar biçiminde organize edilmesinin birkaç açık sebei vardır:

    1) Mühendislikte karmaşık bir problem genellikle parçalarına ayrılarak çözülmektedir. İşte fonksiyonlar karmaşık problemin parçalarını oluşturmak
    için kulalnılmaktadır. Karmaşık işlemin parçaları fonksiyonlara yaptırılır. Sonra bu fonksiyonların çağrılmasıyla karmaşık işlem gerçekleştirilir. 
    Örneğin bir otomobil aslında çok fazla parçadan oluşmaktadır. Bu parçalar birbirleriyle monte edilmiştir. Sonuçta otomobil çalışır hale gelmiştir. 
    Aynı yöntem yazılımda da izlenmektedir. 

    2) Fonksiyonlar "yeniden kullanılabilirliği (reusability)" mümkün hale getirmektedir. Yani işin bir kısmını yapan kodları fonksiyon olarak yazarsak 
    başka projelerde de aynı fonksiyonları kullanabiliriz. Fonksiyonların oluşturduğu topluluğa "kütüphane (library)" denilmektedir. Örneğin standart C 
    fonksiyonları kütüphane biçiminde oluşturulmuştur. Biz onları farklı projelerde kullanabilmekteyiz. 

    3) Fonksiyonlar tekrarı engellemek amacıyla kullanılmaktadır. Bir iş kodun çeşitli yerlerinde yineleniyorsa onu fonksiyon olarak yazarsak toplamda 
    bu kodlardan bir tane projemizde bulundurmuş oluruz. Fonksiyonlar olmasaydı aynı kodu tekrar tekrar yazmak zorunda kalırdık. Bu durumda kod tekrarı 
    toplamda kodun fazla yer kaplamasına yol açardı. O kısımda yapılacak değişikler programın pek çok yerinde yapılmak zorunda kalırdı. Bu durum kodun aynı zamanda
    daha karmaşık gözükmesine yol açardı.

    4) Fonksiyonlar okunabilirliği de artırmaktadır. Fonksiyonların isimleri olduğu için kodu inceleyen kişiler onu daha kolay anlamlandırırlar. 
    Bu isimler aslında o kodun ne yaptığı hakkında da bilgi verir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                19. Ders - 02.08.2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir programlama dilindeki "çalıştırma birimlerine" "deyim (statement)" denilmektedir. Yani program aslında deyim denilen kod parçalarının peşi sıra 
    çalıştırılmasıyla çalışmaktadır. Deyimler C'de 5 gruba ayrılmaktadır:

    1) Basit Deyimler (Simple Statements): Bunlar bir ifadenin sonuna ';' atomu konularak elde edilen deyimlerdir. Yani ifade; biçiminde bir görünüme sahiplerdir. 
    Örneğin:

    a = b + c;
    foo();

    Bunlar birer basit deyimdit. İfade (expression) kavramının ';' atomunu içermediğine ifadenin sonuna ';' getirildiğinde onun bir deyim olduğuna 
    dikkat ediniz. 

    2) Bileşik Deyimler (Compound Statements): Bir blok içerisine sıfır tane ya da daha fazla deyim yerleştirilirse bloğun kendisi de bir deyim olur. 
    Ona "bileşik deyim" denilmektedir. Örneğin

    {
        ifade1;
        ifade2;
        ifade3;
    }

    Burada bu bloğun tamamı dışarıdan bakıldığında tek bir deyimdir. 

    3) Kontrol Deyimleri (Control Statements): Programlama dillerinde programın akışı üzerinde etkili olan, if gibi, while gibi, for gibi deyimlere
    "kontrol deyimleri" denilmektedir. Kontrol deyimleri dışarıdan bakıldığında tek bir deyim olarak ele alınırlar.

    4) Bildirim Deyimleri (Declarartion Statements): Bildirim yapmakta kullandığımız sentaks biçimi de aslında bir deyim belirtir. Bunlara bildirim deyimleri 
    denilmektedir. Örneğin:

    int a, b, c;

    5) Boş Deyimler (Null Statements): Solunda ifade olmadan kullanılan noktalı virgüller de bir deyim belirtir. Bunlara boş deyim denilmektedir. Örneğin:

    x = 10;;

    Burada iki deyim vardır. Birincisi x = 10; deyimidir. Bu bir basit deyimdir. İkincisi bundan sonraki noktalı virgüldür. Boş deyimler için bir şey yapılmıyor 
    olsa da bunlar yine bir deyim statüsündedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıda da belirtildiği gibi program deyimlerin çalıştırılmasıyla çalıştırılmaktadır. Her deyim çalıştığında bir şeyler yapılır. Şimdi bu deyimler 
    çalıştırıldığında ne olacağı üzerinde duralım:

    - Bir basit deyimin çalıştırılması demek o basit deyimdeki ifadenin çalıştırılması demektir. 

    - Bir bileşik deyimin çalıştırılması bileşik deyimi oluşturan deyimlerin sırasıyla çalıştırılması anlamına gelmektedir. Örneğin:

    {
        ifade1;
        ifade2;
        {
            ifade3;
            ifade4;
        }
    }
    ifade5;

    Burada dışarıdan bakıldığında iki deyim vardır: Bileşik deyim ve basit deyim. Bir bileşik deyimin çalıştırılması onu oluşturan deyimlerin sırasıyla
    çalıştırılması anlamına geldiğine göre burada sırasıyla aslında ifade1, ifade2, ifade3, ifade4, ifade5 çalıştırılacaktır.

    - Kontrol deyimleri çalıştırıldığında nelerin olacağı zaten sonraki başlıklarda ele alınacaktır. 

    - Bir bildirim deyimi çalıştırıldığında bildirilen değişkenler için bellekte yerler ayrılmaktadır. Örneğin:

    int a, b, c;

    Burada a, b ve c nesneleri için yerler ayrılacaktır. 

    - Boş deyimin çalıştırılması sırasında bir şey yapılmamaktadır. Yani boş deyimler bir yan etkiye yol açmamaktadır. 

    Bir fonksiyon çağrıldığında fonksiyonun belirttiği ana blok yani bileşik deyim çalıştırılır. Bu durumda bir C programının çalışması demek aslında 
    main fonksiyonun çağrılması demektir. Örneğin:

    int add(int a, int b)
    {
        return a + b;
    }

    int main(void)
    {
        int a;

        a = add(10, 20);
        printf("%d\n", a);

        return 0;
    }

    Burada main fonksiyonu çağrıldığında onun ana bloğunun belirttiği bileşik deyim çalıştırılır. Bu bileşik deyim içerisinde bir bildirim deyimi, 2 tane basit deyim ve bir
    tane kontrol deyimi vardır. a = add(10, 20) basit deyimi çalıştırılırken de add fonksiyonun ana beloğunun belrttiği bileşik deyim çalıştırılmış olur. 
    Yani görüldüğü gibi aslında program deyimlerin çalıştırılmasıyla çalıştırılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Şimdi tek tek kontrol deyimlerini ele alacapız. En yaygın kullanılan kontrol deyimi if deyimidir. if deyiminin genel biçimi şöyledir:

    if (<ifade>)
        <deyim>
  [ else
        <deyim> ]

    if anahtar sözcüğünden sonra parantezler içerisinde bir ifadenin bulunması gerekir. if deyiminin "doğru" ve "yanlış" kısımları vardır. Doğru ve yanlış kısımlarında
    tek bir deyim bulunmak zorundadır. Programcı burada birden fazla deyim bulundurmak istiyorsa onu bileşik deyim olarak ifade etmelidir. if deyimiin yanlış kısmı
    olmak zorunda değildir. if deyiminin tamamı dışarıdan bakıldığında tek bir deyim olarak ele alınmaktadır. 

    if deyimi şöyle çalıştırılmaktadır: Önce if parantezi içerisindeki ifadenin sayısal değeri hesaplanır. Bu değer sıfır dışı bir değerse deyimin yalnızca
    "doğru" kısmındaki deyim çalıştırılır. Bu ifadenin değeri 0 ise deyimin yalnızca "yanlış" kısmındaki deyim çelıştırılır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    printf("Bir deger giriniz:");
    scanf("%d", &a);

    if (a > 0)
        printf("pozitif\n");
    else
        printf("negatif ya da sifir\n");


    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki programda ikinci derece denklemin kökleri yazdırılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

void disp_roots(double a, double b, double c)
{
    double delta;
    double x1, x2;

    delta = b * b - 4 * a * c;
    if (delta >= 0) {
        x1 = (-b + sqrt(delta)) / (2 * a);
        x2 = (-b - sqrt(delta)) / (2 * a);

        printf("x1 = %f, x2 = %f\n", x1, x2);
    }
    else
        printf("Gercek kok yok!..\n");
}

int main(void)
{
    double a, b, c;

    printf("a:");
    scanf("%lf", &a);

    printf("b:");
    scanf("%lf", &b);

    printf("c:");
    scanf("%lf", &c);

    disp_roots(a, b, c);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İç içe (nested) if deyimi söz konusu olabilir. Örneğin aşağıda üç sayının en büyüğünü bulan bir if deyimi kullanılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b, c;

    printf("a:");
    scanf("%d", &a);

    printf("b:");
    scanf("%d", &b);

    printf("c:");
    scanf("%d", &c);

    if (a > b)
        if (a > c)
            printf("%d\n", a);
        else
            printf("%d\n", c);
    else
        if (b > c)
            printf("%d\n", b);
        else
            printf("%d\n", c);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    if deyiminin "yanlış" kısmı yani else kısmı olmak zoruda değildir. Eğer derleyici if deyiminin "doğru" kısmından sonra else anahtar sözcüğünü 
    göremezse bunun "else kısmı olmayan bir if" olduğuna karar verir ve if deyiminin bittiğini düşünür. Örneğin:

    if (ifade1) ifade2; ifade3;

    Burada if eyiminin doğru kısmı ifade2 ile birmiştir ve else anahtar sözcüğü gelmemiştir. Bu udurmda artık ifade3 if içerisinde değildir. Bu kod parçasına
    dışarıdan bakıldığında iki deyim vardır: if deyimi ve bir basit deyim.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    printf("Bir deger giriniz:");
    scanf("%d", &a);

    if (a > 0)
        printf("pozitif\n");
    printf("son\n");		/* if deyiminin dışında */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bazen yeni programcılar if deyiminin doğru kısmını yanlışlıkla boş deyimle kapatırlar. Budurumda kod geçerli olduğu halde istenileni yapmaz hale
    gelir. Örneğin:

    if (ifade1);			/* dikkat! yanlışlıkla yerleştirilmiş boş deyim */
        ifade2

    Burada artık if deyiminin "doğru" kısmında boş deyim vardır. Boş deyimden sonra else anahtar sözcüğü gemediği için if deyimi bitmiştir. 
    Dolayısıyla ifade2; if deyimi dışındadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    printf("Bir deger giriniz:");
    scanf("%d", &a);

    if (a > 0);					/* dikkat! yanlışlıkla yerleştirilmiş boş deyim */
        printf("pozitif\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki kod parçasında programcı if deyiminin "doğru" kısmına birden fazla deyim yerleştirmiştir. Bunları bloklayarak tek deyim biçiminde 
    ifade etmesi gerekirdi:

    if (ifade1)
        ifade2;
        ifade3;
    else
        ifade4;

    Derleyici bakış açısıyla kodu incelediğimizde derleyici if parantezinden sonra blok açılmadığını gördüğünde yalnızca ifade2; deyiminin if deyiminin doğru
    kısmını oluşturduğunu düşünmektedir. ifade2; deyiminden sonra else gelmediği için derleyiciye göre if deyimi sonlanmıştır. Derleyici daha sonra 
    else anahtar sözcüğünü gördüğünde durumu "sanki if olmadan yalnız başına else anahtar sözcüğü kullanılmış gibi" ele almaktadır. Bu durumda verilen mesaj
    "error: else without if" gibi bir şey olabilir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir koşul doğru iken diğerlerinin doğru olma olasılığı yoksa bu koşullara "ayrık koullar" denir. Yani ayrık koşullarda koşulların yalnızca bir 
    tanesi doğru olabilmektedir. Örneğin:

    a > 0
    a < 0
    a == 0

    Bu koşullar ayrıktır. Örneğin:

    a == 1
    a == 2
    a == 3

    Bu koşullar da ayrıktır. Ancak örneğin:

    a > 0
    a > 10

    Bu koşullar ayrık değildir. 

    Ayrık koşulların ayrı if deyimleri ile ele alınması kötü bir tekniktir. Örneğin:

    if (a == 1)
        printf("bir\n");
    if (a == 2)
        printf("iki\n");
    if (a == 3)
        printf("uc\n");

    Burada a == 1 ise gereksiz bir biçimde diğer iki koşul da -doğrulanmayacağı halde*- gereksiz bir biçimde yapılmaktadır. a == 2 ise de a == 3 koşulu 
    gereksiz biçimde yapılacaktır. İşte ayrık koşullar "else if" ile ele alınmalıdır. Örneğin:

    if (a == 1)
        printf("bir\n");
    else
        if (a == 2)
            printf("iki\n");
        else 
            if (a == 3)
                printf("üc\n");

    Burada dışarıdan bakıldığında tek bir if deyimi vardır. Her if diğerinin else kısmı içeisindedir. Pek çok programcı böyle else-if merdivenlerini
    aşağıdaki gibi alt alta yazmaktadır:

    if (a == 1)
        printf("bir\n");
    else if (a == 2)
        printf("iki\n");
    else if (a == 3)
        printf("uc\n");
    else if (a == 4)
        printf("dort\n");
    else
        printf("hicbiri\n");

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    printf("Bir deger giriniz:");
    scanf("%d", &a);

    if (a == 1)
        printf("bir\n");
    else if (a == 2)
        printf("iki\n");
    else if (a == 3)
        printf("uc\n");
    else if (a == 4)
        printf("dort");
    else
        printf("bes\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte sayının işareti yazdırılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    printf("Bir deger giriniz:");
    scanf("%d", &a);

    if (a > 0)
        printf("pozitif\n");
    else if (a < 0)
        printf("negatif\n");
    else
        printf("sifir\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C, C++, Java ve C# gibi dillerde "dangling else" denilen bir durum vardır. Eğer iki if için tek bir else varsa bu else içteki if deyimine 
    ilişkin kabul edilmektedir. Örneğin:

    if (ifade1) if (ifade2) ifade3; else ifade4;

    Buradaki else içteki if deyiminin else kısmıdır. Bunu daha güzel şöyle yazabiliriz:

    if (ifade1)
        if (ifade2)
            ifade3;
        else
            ifade4;

    Bazen deneyimli programcılar bile bu "dangling else" durumunda hata yapabilmektedir. Örneğin aşağıdaki gibi bir kodla karşılaşmış olalım:

    if (ifade1)
        if (ifade2)
            ifade3;
    else
        ifade4;

    Burada muhtemelen programcı ifade4; deyiminin dıştaki if deyimin else kısmında olmasını istemiştir. Çünkü hizalaması bunu düşündürmektedir. 
    Ancak derleyici hizalamaya bakmamaktadır. Dolayısıylla derleyici buradaki else kısmının içteki if deyiminin else kısmı olduğuna karar verir. 
    O halde programcı bir "bug" yapmıştır. Bu tür "dangling else" durumlarında eğer gerçekten else kısmın dıştaki if deyimine ilişkin olması isteniyorsa
    bilinçli bloklama yapılmalıdır. Örneğin:

    if (ifade1) {
        if (ifade2)
            ifade3;
    }
    else
        ifade4;

    Burada artık else kısmı dıştaki if deyimine ilişkindir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                                20.Ders 04/08/2022
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    if deyiminin yalnızca yanlış kısmı bulunamaz. Bu işlem koşulun tersi oluşturularak dolaylı biçimde sağlanabilir. Örneğin:

    if (a > 0)
    else {				/* geçersiz */
        /* ... */
    }
        
    Burada aslında a > 0 değilse bir işlem yapılmak istenmiştir:

    if (a <= 0) {
        /* ... */
    }

    Tabii mademki if deyiminin yalnızca else kısmı bulunamaz. O halde doğru kısmına bir boş deyim yerleştirilerek de aynı durum sağlanabilir:

    if (a > 0)
        ;
    else {				/* geçerli */
        /* ... */
    }
    
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir program parçesının yinelemeli olarak çalıştırılmasını sağlayan kontrol deyimlerine "döngü (loop)" denilmektedir. C'de döngüler iki kısma yarılmaktadır:

    1) while Döngüleri (while Loops)
    2) for Döngüleri (for loops)

    while döngüleri de kendi aralarınd "kontrolün başta yapıldığı while döngüleri" ve "kontrolün sonra yapıldığı while döngüleri" olmak üzere ikiye ayrılmaktadır.

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Kontrolün başta yapıldığı while döngülerinin genel biçimi şöyledir:

    while (<ifade>)
        <deyim>

    while anahtar sözcüğünden sonra parantez içerisinde bir ifadenin bulunması gerekir. while döngüsü bir deyim içerir. Tabii bu deyim, basit, bileşik 
    ya da herhangi bir deyim olabilir. Yani döngünün içerisine birden fazla deyim yerleştirilecekse bloklama yapılmalıdır. 

    while döngüsü şöyle çalışmaktadır: Derleyici while parantezinin içerisindeki ifadenin sayısal değerini hesaplar. Bu değer sıfır dışı bir değerese (yani doğru ise)
    döngü deyimi çalıştırılıp başa dönülür. Döngü while parantezi içerisindeki ifadenin değeri 0 olduğunda sonlanır. 

    Aşağıdaki örnekte 0'dan 10'a kadar (10 dahil değil) sayılar ekrana (stdout dosyasına)yazdırılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    i = 0;
    while (i < 10) {
        printf("%d\n", i);
        ++i;
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte 10'dan başlanarak 0'a kadar (0 dahil değil) sayılar ekrana yazdırılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    i = 10;
    while (i) {
        printf("%d\n", i);
        --i;
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte klavyeden (stdin dosyasından) 'q' karakteri girilene kadar döngü devam etmektedir. Burada atama operatörüne öncelik vermek için 
    parantez kullanıldığına dikkat ediniz. Ayrıca getchar fonksiyonun ve stdin dosyasından okuma yapan diğer fonksiyonların tamponlu (buffered) çalıştırklarını
    anımsayınız. Eğer tampon doluysa getchar yeni bir giriş istememektedir. Ancak tampon boşsa yeni bir giriş istemektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int ch;

    while ((ch = getchar()) != 'q')
        putchar(ch);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Virgül operatörünün önce sol tarafının sonra sağ tarafının yapıldığını ve virgül operatörünün sağ tarafındaki ifadenin değerini ürettiğini anımsayınız.
    O halde yukarıdaki döngü eşdeğer olarak aşağıdaki gibi de olabilirdi.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int ch;

    while (ch = getchar(), ch != 'q')
        putchar(ch);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte 1'den n'e kadar sayıların toplamı hesplanmaktadır. (Tabii aslında bu toplam tek bir ifade ile de hesaplanabilirdi).
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i, n, total;

    printf("Bir sayi giriniz:");
    scanf("%d", &n);

    i = 1;
    total = 0;
    while (i <= n) {
        total += i;
        ++i;
    }
    printf("%d\n", total);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında scanf fonksiyonun da bir geri dönüş değeri vardır. scanf fonksiyonu başarılı bir biçimde yerleştirilen parça sayısı ile geri dönmektedir. 
    scanf stdin tamponunun başındaki boşluk karakterlerini (leading space) atar. Sonra format karakterlerine uygun olmayan ilk karakter gördüğünde 
    onu tampona geri bırakıp işlemini sonlandırır. Örneğin:

    result = scanf("%d", &val);

    Burada biz bir sayı yerine "ali" bir yazı girmiş olalım. Bu durumda scanf a karakterini tampondan aldığında bunun %d format karakterine uygun olmadığını tespit eder. 
    Bu a karakterini tampona geri bırakıp 0 değeri ile geri döner. Örneğin:

    result = scanf("%d%d", &a, &b);

    Burada klavyeden şunları girmiş olalım:

    100 ali

    scanf burada yalnızca a için yerleştirme yapabilecektir. Tamponda ali kalacaktır ve b için yerleştirme yapmayacaktır. Bu durumda scanf 1 değeri ile geri dönecektir. 
    Bu nedenle aşağıdaki örnekte eğer biz klavyeden bir syaı girmezsek sonsuz döngü oluşacaktır:
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int val;
    
    while (scanf("%d", &val), val != 0)
        printf("%d\n", val * val);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki örnekte biz eğer klavyeden geçersiz bir karakter girildiğinde de döngüyü sonlandırmak istiyorsak scanf fonksiyonun geri dönüş değerine de
    bakmalıyız.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int val;
    
    while (scanf("%d", &val) && val != 0)
        printf("%d\n", val * val);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    while parantezi içerisindeki ifadede önek ya da sonek ++ ya da -- operatörü kullanılabilir. Aşağıdaki örnekte önek ++ operatörü kullanılmıştır. 
    burada artırım önce yapılıp artırılmış değer karşılaştırmaya sokulacaktır. Dolayısıyla ilk yazılacak değer 1, son yazılacak değer 9 olacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    i = 0;
    while (++i < 10)
        printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Eğer wehile parantezi içerisindeki ++ ya da -- operatörüğ sonek durumundaysa artırım ya da eksiltim öncelik sırasına göre yapılmakla birlikte sonraki
    işleme artırılmamış ya da eksiltilmemiş değer sokulacaktır. 

    Aşağıdaki örnekte ilk yazdırılacak değer 1'dir. Son yazdırılacak değer ise 10 olacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    i = 0;
    while (i++ < 10)
        printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    n bir nesne belirtmek üzere biz n defa yinelenen bir döngüyü while ile şöyle oluşturabiliriz:

    while (n-- > 0) {
        /* ... */
    }

    Bu bir kalıp olarak kullanılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int n;

    n = 3;
    while (n-- > 0)
        printf("ok\n");		/* üç kere yinelenecek */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında while parantezinin içerisinde yalnızca sonek ++ ya da -- operatörü varsa artırmak ya da eksiltme yapılır. Ancak kontrole nesnenin artırılmamış ya da 
    eksiltilmemiş değeri sokulur. Dolayısıyla n pozitif olmak üzere aşağıdaki işlevsel olarak döngüler eşdeğerdir:

    while (n-- > 0) {
        /* ... */
    }

    while (n--) {
        /* ... */
    }

    while (n-- != 0) {
        /* ... */
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Kontrolün sonda yapıldığı while döngüleri (do-while döngüleri) seyrek kullanılmaktadır. Genel biçimleri şöyledir:

    do 
        <deyim>
    while (<ifade>);

    while parantezi sonundaki ';' boş deyim belirtmez. Kullanılması zorunlu olan sentaksın bir parçasını oluşturmaktadır. Döngünün do anahtar sözcüğü ile
    başlatıldığına dikkat ediniz. Yine döngü içerisinde tek bir deyim vardır. Bu deyim basit, bileşik ya da herhangi bir deyim olabilir. Örneğin:

    do 
        ifade1;
    while (ifade2);

    Örneğin:

    do {
        ifade1;
        ifade2;
        ifade3;
    } while (ifade4);

    do-while döngüsünde kontrol noktasının sonda olduğuna dikkat ediniz. Dolayısıyla döngü en az bir kez yinelenmektedir. Burada do anahtar sözcüğü olmasaydı
    döngü kontrolün başta yapıldığı while döngüsü olarak ele alınırdı. Örneğin:
    
    {
        ifade1;
        ifade2;
        ifade3;
    } while (ifade4);

    Derleyiciye göre buarada iki deyim vardır: Bileşik deyim ve ondan bağımsız olarak kontrolün başta yapıldığı while döngüsü. Dolaysıyla buradaki ';'
    boş deyim anlamına gelmektedir. 

    Aşağıdaki örnekte ekrana ilk çıkacak değer 0 son çıkacak değer 9'dur.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    i = 0;
    do {
        printf("%d\n", i);
        ++i;
    } while (i < 10);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Kontrolünm sonda yapıldığı while döngülerine yukarıda da belirttiğimiz gibi aslında oldukça seyrek gereksinim duyulmaktadır. Aşağıdaki örnekte
    kullanıcıdan 'e' ya da 'h' karakteri ile bir seçim yapması istenmiştir. Eğer kullanıcı e' ya da 'h' karakterinden birini girmemişse aynı soru yinelenmiş 
    ve kullanıcı bu karakterlerdne birini girmeye zorlanmıştır. Buradaki döngünün kontrolün sonda yapıldığı while döngüsü olması çok daha anlamlıdır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void clear_stdin(void)
{
    while (getchar() != '\n')
        ;
}

int main(void)
{
    int ch;

    do {
        printf("(e)vet/(h)ayir?");
        ch = getchar();
        if (ch != '\n')
            clear_stdin();
    } while (ch != 'e' && ch != 'h');

    if (ch == 'e')
        printf("evet\n");
    else
        printf("hayir\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Programcılar yanlışlıkla kontrolün başta yapıldığı while döngülerini boş deyim ile kapatabilmektedir. Örneğin:

    while (n-- > 0);
        printf("%d\n", n);

    Burada while parantezinin sonuna yerleştirilen ';' boş deyim belirtir. Dolayısıyla artık aşağıdaki printf while döngüsünün içerisinde değildir. 
    Eüer programcı gerçekten döngüyü boş deyim kapatmak istiyorsa (örneğin bir gecikme sağlamak istemiş olabilir) bu durumda ';' sanki bir deyim gibi 
    hizalanmalıdır. Çünkü kodu gören kişi bunun yanlışlıkla yapılmadığını anlayacaktır:

    while (n-- > 0)
        ;

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int n;

    n = 5;
    while (n-- > 0);			/* dikkat! yanlışlıkla yerleştirilmiş boş deyim */
        printf("%d\n", n);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bazen sonsuz döngülerin oluşturulması gerekebilir. Bunun için while parantezi içerisine sıfırın dışında herhangi bir sayı yerleştirilebilir. 
    Tabii genellikle programcılar 1 sayısını tercih ederler. Örneğin:

    while (1) {				/* sonsuz döngü (infinite loop) */
        /* ... */
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    21.Ders 16/08/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    En çok kullanılan döngüler for döngüleridir. for döngülerinin genel biçimi şöyledir:

    for ([ifade1]; [ifade2]; [ifade3])
        <deyim>

    for anahtar sözcüğündne sonra parantezler içerisinde iki tane ';' bulunmak zorundadır. Bu iki ';' for döngüsünü üç kısma ayırır. for döngüsünün 
    bu kısımlarında "ifade (expression)" tanımna uyan herhangi ifadeler bulunabilir. for döngüsünün içeirsindeki deyim yine herhangi bir deyim olabilir. 

    for döngüleri en fazla aşağıdaki gibi karşımıza çıkar:

    for (ilkdeğer; koşul; artırım) {
        /* ... */
    }

    Örneğin:

    for (i = 0; i < 10; ++i) {
        /* ... */
    }

    for döngüsü şöyle çalışmaktadır: Önce döngüye girişte for döngüsünün birinci kısmındaki ifade bir kez çalıştırılır. Artık bu ifade bir daha çalıştırılmaz. 
    İkinci kısımdak ifade ilk girişte ve her yinelemede çalıştırılmaktadır. Döngü bu ikinci kısımdaki ifade sıfır dışı bir değerde olduğu sürece yinelenmektedir. 
    Döngünün üçüncü kısmı döngü deyimi çalıştırıldıktan sonra başa dönerken çalıştırılmaktadırç for döngüsünün çalışması tamamen aşağıdaki ile eşdeğerdir:

    ifade1;
    while (ifade2) {
        <deyim>
        ifade3;
    }
    
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 0; i < 10; ++i)
        printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Örneğin biz artırımı ikişer ikişer de yapabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 0; i < 10; i += 2)
        printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki döngüde belli bir değerden eksiltim uygulanarak sıfıra kadar yinelenme sağlanmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 10; i != 0; --i)
        printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsünün üçüncü kısmında ++ ya da -- operatörü kullanıldığında bunun önek mi yoksa sonek mi olduğunun hiçbir önemi yoktur. Örneğin:

    for (i = 0; i < 10; ++i) {
        /* ... */
    }

    ile

    for (i = 0; i < 10; i++) {
        /* ... */
    }

    eşdeğerdir.

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsünün üç kısmında da ifade tanımına uyan herhangi ifadeler yerleştirilebilir. Önemli olan bunun programcının amacına uygunluğudur. 
    Örneğin aşağıdaki gibi bir for döngüsü tamamen geçerlidir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    i = 0;
    for (printf("ifade1\n"); i < 3; printf("ifade3\n")) {
        printf("deyim\n");
        ++i;
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte 0'dan 6.28'e kadar sayıların sinüz değerleri 0.1 artırımla yazdırılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

int main(void)
{
    double x, y;

    for (x = 0; x < 6.28; x += 0.1) {
        y = sin(x);
        printf("%f\t%f\n", x, y);
    }

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte 1'den klavyeden girilen sayıya kadar sayıların toplamı hesaplanmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int n, total, i;

    printf("Bir sayi giriniz:");
    scanf("%d", &n);

    total = 0;
    for (i = 1; i <= n; ++i)
        total += i;

    printf("%d\n", total);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte isprime fonksiyonu bir sayının asal olup olmadığını tespit etmektedir. Eğer sayı asalsa fonksiyon 1 değeri ile, asal değilse 0
    değeri ile geri dönmektedir. Bir döngü içerisinde return deyimini kullanırsak fonksiyon sonlanır dolayısıyla döngü de sonlanmış olur. 

    Aşağıdaki örnekte isprime fonksiyonundan faydalanılarak 2'den 1000'e kadar asal sayılar yan yana yazdrılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int isprime(int val)
{
    int i;

    for (i = 2; i < val; ++i)
        if (val % i == 0)
            return 0;

    return 1;
}

int main(void)
{
    int i;

    for (i = 2; i < 1000; ++i)
        if (isprime(i))
            printf("%d ", i);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Öklit teoremine göre aslında sayı asal değilse mutlaka sayının kareköküne kadar bir çarpanı vardır. Yani sayının kareköküne kadar kontrol yapmak yeterlidir. 
    Ayrıca çift sayılın kontrol edilmesine de gerek yoktur. Ancak 2 için özel bir durum vardır. 2 çift olmasına karşın asal bir sayıdır. 

    O halde yukarıdaki isprime fonksiyonunu daha etkin çalışacak biçimde aşağıdaki gibi düzeltebiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

int isprime(int val)
{
    int i;
    double val_sqrt;

    if (val % 2 == 0)
        return val == 2;

    val_sqrt = sqrt(val);
    for (i = 3; i <= val_sqrt; i += 2)
        if (val % i == 0)
            return 0;

    return 1;
}

int main(void)
{
    int i;

    for (i = 2; i < 1000; ++i)
        if (isprime(i))
            printf("%d ", i);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsünün birinci kısmındaki ifade hiç yazılmayabilir. Örneğin döngünün birinci kısmındaki ifade yukarıya alınırsa dğeişhen hiçbir şey olmaz:

    for (ifade1; ifade2; ifade3)
        <deyim>

    ile

    ifade1;
    for(; ifade2; ifade3)
        <deyim>

    eşdeğerdir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    i = 0;
    for (; i < 10; ++i)
        printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsünün üçüncü kısmı da yazılmayabilir. Örneğin:

    for (ifade1; ifade2; ifade3)
        <deyim>

    ile

    ifade1;
    for (; ifade2; ) {
        <deyim>
        ifade3;
    }

    eşdeğerdir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Birinci ve üçüncü kısmı olmayan for döngüleri tamamen while döngüleriyle eşdeğerdir. Örneğin:

    while (ifade) {
        /* ... */
    }

    ile

    for (; ifade; ) {
        /* .... */
    }

    eşdeğerdir. 

    Görüldüğü gibi for döngüsü while döngüsü gibi, while döngüsü de for döngüsü gibi kullanılabilmektedir:

    ifade1;
    while (ifade2) {
        <deyim>
        ifade3;
    }

    ile 


    for (ifade1; ifade2; ifade3)
        <deyim>

    eşdeğerdir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    for döngülerinin ikinci ksımındaki ifade de hiç yazılmayabilir. Bu durumda koşulun sürekli bir biçimde sağlandığı kabul edilmektedir. Örneğin:

    for (ifade1;; ifade2) {
        /* .... */
    }

    Burada döngü sürekli yinelenir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 0;; ++i)			/* sonsuz döngü */
        printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında for döngüsünün hiçbir kısmı olmayabilir. Ancak her zaman iki tane ';' parantez içerisinde bulunmak zorundadır. Böyle for döngüleri "sonsuz döngü"
    oluşturmak için kullanılabilmektedir. Örneğin:

    for (;;) {
        /* ... */
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    double val;

    for (;;) {				/* sonsuz döngü */
        printf("Bir deger giriniz:");
        scanf("%lf", &val);
        printf("%f\n", val * val);
    }
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsünün kısımlarında virgül operatörü kullanılarak ifadeler genişletilebilir. Örneğin biz for döngüsünün birinci kısmında birden fazla dğeişkene 
    virgül operatöründen faydalanarak değer atayabiliriz. Benzer biçimde üçüncü kısımda da virgül operatörü ile birden fazla işlem yapabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i, k;

    for (i = 0, k = 100; i + k > 50; ++i, k -= 2)
        printf("%d %d\n", i, k);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsü yanlışlıkla boş deyim ile kapatılabilmektedir. Bu durumda boş deyim döngü deyimi gibi ele alınır. Dolayısıyla kodun anlamı tamamen 
    değişir. Örneğin:

    for (i = 0; i < 10; ++i);
        printf("%d\n", i);

    Burada döngü yanlışlıkla boş deyim ile kapatılmıştır. Bu durumda printf artık döngünün dışında kalmıştır. Tabii bazen döngü gerçekten boş deyim ile 
    kapatılmak istenebilir. Bu durumda ';' bir tab içeden yazılarak okunabilirlik artırılabilir. Örneğin:

    for (i = 0; i < 1000000; ++i)
        ;
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 0; i < 10; ++i);			/* dikkat! döngü yanlışlıkla boş deyim ile kapatılmış! */
        printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte ilk getchar() klavyeden okunanları tampona yerleştirir ve ilk karakterin sıra numarasıyla geri döner. Sonraki getchar çağrıları
    tampondaki sıradaki karakterleri alır. Tamponun sonunda ENTER tulu nedeniyle '\n' karakteri bulunacaktır. O halde aşağıdaki kodda klavyedne girilen karakterlerin 
    sayısı hesaplanmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 0; getchar() != '\n'; ++i)
        ;

    printf("%d\n", i);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İç içe (nested) döngüler söz konusu olabilir. Döngü deyimleri de dışarıdan bakıldığında tek bir deyim durumundadır. Eğer bir döngünün içerisinde 
    başka bir döngü varsa blok açmaya hiç gerek yoktur. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i, k;

    for (i = 0; i < 10; ++i)
        for (k = 0; k < 10; ++k)
            printf("(%d, %d)\n", i, k);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte klavyeden okunan n satır sayısı olmak üzere şu kalıp bastırılmaktadır:

    *
    **
    ***
    ****
    ...
    ****.... ****
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int n;
    int i, k;

    printf("Bir sayi giriniz:");
    scanf("%d", &n);

    for (i = 1; i <= n; ++i) {
        for (k = 0; k < i; ++k)
            putchar('*');
        putchar('\n');
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'ye C99 ile birlikte C++'ta da zaten olan "for döngüsünün birinci kısmında bildirim yapabilme" olanağı eklendi. Bu kurala göre biz döngü 
    değişkenini doğrudan for döngüsünün birinci kısmında bildirebiliriz. Örneğin:

    for (int i = 0; i < 10; ++i) {
        /* ... */
    }
        
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 10; ++i)
        printf("%d ", i);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsünün birinci kısmında bildirilen değişkene ilkdeğer vermek gerekir. Standartlarda burada bildirilen değişkenlere ilkdeğer vermemek 
    geçerli kabul edilse de toplamda anlamsızdır. Örneğin:

    for (int i; i < 10; ++i)  {		/* geçerli ama anlamsız */
        /* ... */
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsünün birinci kısmında bildirilen değişkenler yalnızca o for döngüsünde kullanılabilir. Çünkü orada gizli bir bloğun olduğu kabul edilmektedir. 
    Yani örneğin:

    for (bildirim; ifade2; ifade3)
        <deyim>

    döngüsünü eşdeğeri şöyledir:

    {
        bildirim;
        for (; ifade2; ifade3)
            <deyim>
    }

    Böylece örneğin:

    for (int i = 0; i < 10; ++i) {
        /* ... */
    }
    printf("%d\n", i);		/* geçersiz! i burada faaliyet göstermiyor */

    Aşağıdaki örnekte her iki for döngüsündeki i aslında o for döngülerinde kullanılan farklı yerel i'lerdir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 10; ++i)
        printf("%d ", i);
    printf("\n");

    for (int i = 0; i < 10; ++i)
        printf("%d ", i);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki gibi bir durum da mümkündür:

    int i;

    for (int i = 0; i < 10; ++i) {
        /* burada i'yi kullanırsak for döngüsünün birinci kısmında bildirilen i olur */
    }

    Aşağıdaki durum da geçerli olsa da bu tür koslardan kaçınınız:

    for (int i = 0; i < 10; ++i)
        for (int i = 0; i < 10; ++i)  {		/* geçerli */
            /* Burada i kullanılırsa iç for döngüsündeki i anlaşılır */
        }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 10; ++i)
        for (int i = 0; i < 10; ++i)			/* geçerli ama kötü teknik */
            putchar('.');

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    for döngüsünün birinci kısmında birden fazla değişkenin bildirimi yapılabilir. Bu durumda bu değişkenlerin aynı türdne olması gerekir. Farklı türlerden 
    değişkenlerin birinci kısımda bildirilme olanağı yoktur. Örneğin:

    for (int i = 0, k = 100; i + k > 50; ++i, k -= 2) {		/* geçerli 
        /* ... */
    }

    Ancak örneğin:

    for (int i = 0, double k = 0; ;) {		/* geçersiz! böyle bir sentaks yok! */
        /* .... */
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    for (int i = 0, k = 100; i + k > 50; ++i, k -= 2)
        printf("%d %d\n", i, k);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    break deyimi döngü deyimlerinin içerisinde ya da switch deyiminin içerisinde kullanılabilir. Genel biçimi şöyledir:

    break;

    Programın akışı break deyimini gördüğünde içinde bulunulan döngü deyimi sonlandırılır. Programın akışı döngü deyiminden sonraki deyim ile devam eder. 
    Yani break döngüyü bitirmektedir. Tabii döngü break genellikle bir koşul altında kullanılır. Örneğin:

    for (;;) {
        /* .... */
        if (koşul)
            break;
        /* ... */
    }

    Sonsuz döngülerden çıkmak için break tek seçenektir. Ancak breajk deyimi sonsuz olmayan döngülerde de kullanılabilir. 

    Bazen döngülerden çıkış koşulları çok çeşitli olabilmektedir. Bu tür durumlarda programcılar döngüyü sonsuz döngü yapıp içeriden break ile çıkmayı 
    tercih edebilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    double val;

    for (;;) {
        printf("Bir sayi giriniz:");
        scanf("%lf", &val);
        if (val == 0)
            break;
        printf("%f\n", val * val);
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                22.Ders 18/08/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İç içe döngülerde break deyimi yalnızca kendi döngüsünü sonlandırmaktadır. Yani break hangi döngünün içerisinde kullanılmışsa yalnızca onu 
    kırmaktadır. 

    Aşağıdaki örnekte ENTER tuşuna basıldığında iç döngü sonraki yinelemeyle devam eder. q tuşuyna basıldığında önce iç döngüdeki break ile iç döngüden
    çıkılır, sonra dış döngüdeki break ile dış döngüden de çıkılır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void clear_stdin(void)
{
    while (getchar() != '\n')
        ;
}

int main(void)
{
    int ch;

    for (int i = 0; i < 10; ++i) {
        for (int k = 0; k < 10; ++k) {
            printf("(%d, %d)\n", i, k);
            printf("Press ENTER to continue or q to exit:");
            ch = getchar();
            if (ch != '\n')
                clear_stdin();
            if (ch == 'q')
                break;
        }
        if (ch == 'q')
            break;
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte bir prompt çıkartılmıştır. Bu prompt eşliğinde tek karakterli komutlar istenmektedir. q tuşuna basıldığında komut yorumlayıcıdan
    çıkılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void clear_stdin(void)
{
    while (getchar() != '\n')
        ;
}

int main(void)
{
    int ch;

    for (;;) {
        printf("CSD>");

        ch = getchar();
        if (ch != '\n')
            clear_stdin();

        if (ch == 'q')
            break;
        if (ch == 'r')
            printf("remove command executes...\n");
        else if (ch == 'c')
            printf("copy command executes...\n");
        else if (ch == 'd')
            printf("dir command executes...\n");
        else if (ch == 'm')
            printf("move command executes...\n");
        else
            printf("invalid command: %c\n", ch);
        
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki örnekteki programı baştaki SPACE ve TAB karakterlerini atacak biçimde geliştirebiliriz. Aynı zamanda baştak SPACE ve TAB atıldıktan sonra ENTER 
    tuşuna basılırsa yeni bir prompt'a geçecek biçimde programı düzenleyebiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int get_command(void)
{
    int ch;

    while ((ch = getchar()) == ' ' || ch == '\t')
        ;

    if (ch != '\n')
        while (getchar() != '\n')
            ;

    return ch;
}

int main(void)
{
    int ch;

    for (;;) {
        printf("CSD>");

        ch = get_command();

        if (ch != '\n') {
            if (ch == 'q')
                break;
            if (ch == 'r')
                printf("remove command executes...\n");
            else if (ch == 'c')
                printf("copy command executes...\n");
            else if (ch == 'd')
                printf("dir command executes...\n");
            else if (ch == 'm')
                printf("move command executes...\n");
            else
                printf("invalid command: %c\n", ch);
        }
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    if deyiminin doğru kısmında break ya da return gibi deyimler varsa if deyimine else kısmının konulmasının bir anlamı olmaz. Örneğin:

    if (ifade1)
        break;
    else
        ifade2;

    ile aşağıdaki eşdeğerdir:

    if (ifade1)
        break;
    ifade2;

    Aşağıdaki if deyimine bakınız:

    if (ifade1 && ifade2)
        ifade3;

    Bu işlemin eşdeğeri şöyledir:

    if (ifade1)
        if (ifade2)
            ifade3;

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    continue deyimi yalnızca döngü içerisinde kullanılaabilen bir deyimdir. Genel biçimi şöyledir:

    continue;
    
    Programın akışı continue deyimini gördüğünde döngünün içerisindeki deyim sonlandırılıp yeni bir yinelemeye geçilmektedir. Yani break deyimi döngü
    deyiminin kendisini sonlandırırken, continue deyimi döngü içerisindeki deyimin sonlandırılmasına yol açar. continue seyrek kullanılan bir deyimdir. 
    continue deyimi for döngüsü içerisinde kullanılırsa yeni bir yineleme oluşacağı için for döngüsünün üçüncü kısmı başa dönüşte yapılacaktır. 

    Aşağıdaki örnekte i çift iken akış continue deyimini görür. Böylece döngüde yeni bir yinelemeye geçilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 10; ++i) {
        if (i % 2 == 0)
            continue;
        printf("%d\n", i);
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    continue deyimi özelllikle döngüler içerisindeki geniş if bloklarını elimine etmek için kullanılmaktadır. Örneğin:

    for (;;) {
        ch = get_command();
        if (ch != '\n') {
            /* ... */
        }
    }

    Bu işlemin eşdeğeri şöyle oluşturulabilir:

    for (;;) {
        ch = get_command();
        if (ch == '\n') 
            continue;
        /* ... */
    }

    Tabii bazen bir döngü içerisinde pek çok yerde akışın başa sarılması istenebilir. Bu tür durumlarda continue tasarımı oldukça sade göstermektedir. 
    continue deyimi de iç içe döngülerde yalnızca iç döngüyü başa sarar.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int get_command(void)
{
    int ch;

    while ((ch = getchar()) == ' ' || ch == '\t')
        ;

    if (ch != '\n')
        while (getchar() != '\n')
            ;

    return ch;
}

int main(void)
{
    int ch;

    for (;;) {
        printf("CSD>");

        ch = get_command();
        if (ch == '\n')
            continue;
        
        if (ch == 'q')
            break;
        if (ch == 'r')
            printf("remove command executes...\n");
        else if (ch == 'c')
            printf("copy command executes...\n");
        else if (ch == 'd')
            printf("dir command executes...\n");
        else if (ch == 'm')
            printf("move command executes...\n");
        else
            printf("invalid command: %c\n", ch);
        
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
     Yalnızca sabitlerden ve operatörlerden oluşan ifadelere "sabit ifadeleri (constant expression)" denilmektedir. Örneğin:

     3
     2 + 5
     2 + 5 * 3
     1 + 2 + 3 + 4

     birer sabit ifadesidir. Sabit ifadelerinin derleme aşamasında derleyici tarafından sayısal değerleri hesaplanabilmektedir. Pek çok derleyici 
     sabit ifadelerini derleme işlemi sırasında hesaplar böylece bu işlemlerin gereksiz bir biçimde programın çalışma zamanı sırasında yapılmasını engeller. 
     Bu optimizasyon temasına "constant folding" denilmektedir. C'de bazı durumlarda sabit ifadelerinin kullanılması zorunludur. Örneğin:

     - Global değişkenlere verilen ilkdeğerlerin sabit ifadesi olması zorunludur. 
     - Global dizilerde (C99 öncesi tüm dizilerde) uzunluk sabit ifadeleriyle belirtilmek zorundadır.
     - case ifadeleri sabit ifadesi olmak zorundadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    switch deyimi bir ifadenin çeş,tl, sayısal değerleri için farklı birtakım işlemlerin yapılması için düşünülmüş bir deyimdir. switch deyimi olmasaydı
    aslında gereksinim duyulan şeyler if deyimleriyle de yapılabilirdi. Ancak switch deyimi okunabilirliği artırmaktadır ve bazı durumlarda derleyicinin daha etkin 
    kod üretmesini sağlamaktadır. switch deyiminin tipik genel biçimi şöyledir:

    switch (<ifade>) {
        case <s.i>:
            /* ... */
            [break;]
        case <s.i>:
            /* ... */
            [break;]
        case <s.i>:
            /* ... */
            [break;]
        /* ... */
        [default:
            /* .... */
        ]
    }

    switch anahttar sözcüğünden sonra parantez içerisinde bir ifade bulunmak zorundadır. switch deyimi tipik olarak case bölümlerinden oluşur. 
    case anahtar sözcüğünden sonra sabit ifadesi bulunması gerekir. case bölümleri tipik olarak break deyimleriyle sonlandırılmaktadır. Ancak bu zorunlu
    değildir. switch deyiminin isteğe bağlı bir default bölümü olabilir. 

    switch deyimi şöyle çalışmaktadır: Önce switch parantezi içerisindeki ifadenin sayısal değeri hesaplanır. Sonra bu değere tam eşit olan case bölümü
    araştırılır. Eğer bu değere eşit olan bir case bölümü varsa akış o bölüme aktarılır. O bölümdeki deyimler çalıştırılır. break deyimi döngülerde olduğu gibi 
    switch deyiminin de sonlandırılmasına yol açmaktadır. Eğer switch parantezi içerisindeki ifadenin değeri ile eşit olan bir case bölümü yoksa ancak 
    default bölüm varsa akış default bölüme aktarılmaktadır. default bölüm olmak zorunda değildir. Eğer switch parantezi içerisindeki ifadenin sayısal değerine 
    eşit olan bir case bölümü yoksa ve default bölüm de yoksa akış switch deyimiminin dışındaki ilk deyimle devam eder. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int val;

    printf("Bir deger giriniz:");
    scanf("%d", &val);

    switch (val) {
        case 1:
            printf("bir\n");
            break;
        case 2:
            printf("iki\n");
            break;
        case 3:
            printf("üc\n");
            break;
        case 4:
            printf("dort\n");
            break;
        case 5:
            printf("bes\n");
            break;
        default:
            printf("hicbiri\n");
            break;
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aynı switch deyiminde aynı değerde birden fazla case bölümü olamaz. case anahtar sözcüğünün yanındaki ifadenin sabit ifadesi olması zorunludur. 
    Bu sayede derleyici case ifadelerinin yinelenmediğini derleme aşamasında tespit edebilmektedir. 

    case bölümlerinin tamsayı türlerine ilişkin sabit ifadesi olması zorunludur. Yani case anahtar sözcüğünün yanında float, double, long double gibi 
    noktalı sayılar bulunamaz. Benzer biçimde switch parantezi içerisindeki ifade de tamsayı türlerine ilişkin olmak zorundadır. 

    switch deyiminde case bölümlerinin sıralı (sorted) olması ya da default bölümün sonda olması bir zorunluluk değildir. Ancak case bölümlerinin sıralı olması 
    ve default bölümün sonda olması okunabilirliği artırabilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int val;

    printf("Bir deger giriniz:");
    scanf("%d", &val);

    switch (val) {
    case 5:
        printf("bes\n");
        break;
    case 1:
        printf("bir\n");
        break;
    default:
        printf("hicbiri\n");
        break;
    case 2:
        printf("iki\n");
        break;

    case 4:
        printf("dort\n");
        break;
    case 3:
        printf("üc\n");
        break;
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                    23. Ders 23/08/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de switch deyiminde "aşağıda doğru düşme (fall through)" denilen bir özellik vardır. Akış bir case bölümüne devredildikten sonra o case bölümünün 
    sonunda break yok ise aşağıda doğru akmaya devam eder. İlk break görüldüğünde switch'ten çıkılır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int val;

    printf("Bir deger giriniz:");
    scanf("%d", &val);

    switch (val) {
        case 1:
            printf("bir\n");			// fallthrough
        case 2:
            printf("iki\n");			
            break;
        case 3:
            printf("üc\n");				// fallthrough
        case 4:
            printf("dort\n");
            break;
        case 5:
            printf("bes\n");
            break;
        default:
            printf("hicbiri\n");
            break;

    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Frklı case değerleri için aynı şeylerin yapılması isteniyorsa bunun en pratik yöntemi aşağıdaki gibidir:

    case 1:
    case 2:
        ...
        break;

    Bunun daha pratik bir yolu yoktur. Burada switch ifadesi 1 ise fallthrouh nedeniyle zaten 2 ile aynı kod çalıştırılacaktır. 

    Aşağıdaki örnekte komut satırı uygulaması swithc deyimi ile yapılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int get_command(void)
{
    int ch;

    while ((ch = getchar()) == ' ' || ch == '\t')
        ;

    if (ch != '\n')
        while (getchar() != '\n')
            ;

    return ch;
}

int main(void)
{
    int ch;

    for (;;) {
        printf("CSD>");

        ch = get_command();
        if (ch == '\n')
            continue;
        if (ch == 'q')
            break;

        switch (ch) {
            case 'e':
            case 'r':
                printf("remove command executes...\n");
                break;
            case 'c':
                printf("copy command executes...\n");
                break;
            case 'd':
                printf("dir command executes...\n");
                break;
            case 'm':
                printf("move command executes...\n");
                break;
            default:
                printf("invalid command: %c\n", ch);
                break;
        }
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    switch deyiminde hiçbir henüz case ya da default bölümlerinden önce kodlar yerleştirilebilir. Bildirimler yapılabilir. 
    Burada yapılan bildirimler geçerlidir. Ancak buraya yerleştirilen kodlar çalıştırılmazlar. Dolayısıyla buradaki 
    tanımlamada nesneye ilkdeğer veriliyor olsa bile bu ilkdeğerler işleme sokulmamaktadır. Örneğin:
    
    switch (expr) {
        int i = 4;
        f(i);
        //...
        default:
            printf("%d\n", i);
    }

    Burada i değişkeninin bildirimi geçerlidir. Ancak akış hiçbir zaman o noktaya gelemeyeceği için verilen ilkdeğer 
    nesneye yerleştirilemeyecektir. f fonksiyonu da hiçbir zaman çağrılmayacaktır. Bu tür durumlarda bu işlemlerin switch 
    yukarısına alınması gerekir. Örneğin:

    int i = 4;
    f(i);
    switch (expr) {}
        // ...
        default:
            printf("%d\n", i);
    }

    Ayrıca case ya da default bölümlerinde bildirim yapılamamaktadır. Örneğin:

    switch (expr) {
        case 1:
            int a;		// geçersiz!
        //...
    }

    Yalnızca bir case ya da default bölümde değişkenin kullanılmasını istiyorsanız. Orada ayrı bir blok oluşturmalısınız.
    Örneğin:

    switch (expr) {
        case 1: 
        {
            int a;		// geçerli
            //...
        }
        //...
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    switch deyimlerinde case bölümlerinin çok uzatılması okunabilirliği bozmaktadır. Bu nedenle case bölümlerinde uzun işlemler yapılacaksa 
    o işlemleri yapan fonksiyonlar tanımlanmalı ve case bölümlerinde bu fonksiyonlar çağrılmalıdır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int get_command(void)
{
    int ch;

    while ((ch = getchar()) == ' ' || ch == '\t')
        ;

    if (ch != '\n')
        while (getchar() != '\n')
            ;

    return ch;
}

void erase_command(void)
{
    printf("erase command executes...\n");
}

void copy_command(void)
{
    printf("copy command executes...\n");
}

void dir_command(void)
{
    printf("dircommand executes...\n");
}

void move_command(void)
{
    printf("move command executes...\n");
}

int main(void)
{
    int ch;

    for (;;) {
        printf("CSD>");

        ch = get_command();
        if (ch == '\n')
            continue;
        if (ch == 'q')
            break;

        switch (ch) {
            case 'e':
            case 'r':
                erase_command();
                break;
            case 'c':
                copy_command();
                break;
            case 'd':
                dir_command();
                break;
            case 'm':
                move_command();
                break;
            default:
                printf("invalid command: %c\n", ch);
                break;
        }
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de case bölümlerinin hemen switch bloğüunun içerisinde olması zorunlu değildir. Bir case bölümü başka bir case bölümünün içerisinde bir yerlerde 
    olabilir. C# ve Java gibi dillerde böyle bir özellik yoktur. Örneğin:

    switch (ifade) {
        case 1:
            ...
            ...
            if (falanca) {
                ...
                case 2:
                    .....
                    .....
                    break;
            }
        break;
        ...
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b;

    printf("Bir deger giriniz:");
    scanf("%d", &a);

    switch (a) {
        case 1:
            printf("bir\n");
            printf("Bir deger daha giriniz:");
            scanf("%d", &b);
            if (b > 0) {
                case 2:
                    printf("islemler devam ediyor..\n");
                    break;
            }
            break;
        case 3:
            printf("uc\n");
            break;
        default:
            printf("default\n");
            break;
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında switch içerisind tek bir deyim varsa switch deyimi de bloklanmak zorunda değildir. Örneğin:

    switch (ifade) 
        case 1:
            printf("bir\n"); printf("iki\n");

    Burada yalnızca ilk printf printf çağrısı switch deyiminin içerisindedir. İkinci printf switch içerisinde değildir. Standartlarda case bir deyim olarak
    şöyle ifade edilmiştir:

    case <sabit ifadesi>: deyim

    switch (ifade) 
        case 1: {
                printf("bir\n"); printf("iki\n");
            }

    Burada switch içerisinde yine tek bir deyim vardır. O deyim de bileşik deyimdir. Dolayısıyla iki printf çağrısı da switch içerisinddir. Örneğin:

    switch (a) 
        case 1: 
            printf("bir\n");
            break;				/* geçersiz! */

    Burada switch içerisinde yalnızca printf çağrısı vardır. break deyimi switch içerisinde değildir. switch ve döngü içerisinde olmayan break deyimleri 
    geçersizdir.
        
    C standartlarında aslında switch deyiminin genel biçimi şöyle verilmiştir:

    switch (<ifade>)
        <deyim>

    Yani bu genel biçime göre aslında switch deyimi case deyimini içermeyebilir. Ancak case içermeyen switch deyimleri geçerli olsa da anlamlı değildir. Örneğin:

    switch (ifade) {	/* geçerli ama anlamlı değil */
        ifade1;
        ifade2;
    }

    İşte switch deyiminde eğer bloklama yapılmazsa onun içeriinde tek deyimin olduğu kabul edilmektedir. Örneğin:

    switch (ifade)
        ifade1; ifade2;

    Burada switch içerisinde case bölümü ya da default bölümü olmadığına göre switch deyimi anlmasızdır. Ancak gerçerlidir. Burada ifade2 switch deyimi 
    dışındadır. 

    Bir döngü içerisinde bir switch deyimi olsun. Bu switch deyimi içerisinde break kullandığımızda biz switch deyimini sonlandırmış oluruz. Döngü deyimini 
    sonlandırmış olmayız. Ancak continue deyimi switch için anlamlı olmadığına göre döngü içerisindeki switch deyiminde continue kullanıldığında
    switch deyimi de sonlanarak sonraki yinelemeye geçilir. Örneğin:

    for (;;) {
        switch (ifade) {
            case 1:
                ...
                if (falanca)
                    continue;			/* bu continue döngü başına dönüşü sağlar 
            ...
        }
        ....
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    goto deyimi programın akışını koşulsuz biçimde belli bir noktaya aktarmak için kullanılan bir kontrol deyimidir. Genel biçimi şöyledir:

    goto <label>;
    .....
    <label:>
        ....

    goto anahtar sözcüğünün yanınada isimlendirme kuralına uygun bir isim bulunur. Bu isme "etiket (label)" denilmektedir. 
    Daha sonra bu etiket ':' atomuyla fonksiyonda bir yerde bulundurulmak zorudandır. Etiket akışın aktarılacağı yeri belirtir. 
    Örneğin:

    if (ifade)
        goto EXIT;
    ...
    EXIT:
        ....

    Etiketler genelikle programcılar tarafından büyük harflerle isimlendirilmektedir. 

    goto deyimi döngü oluşturmak için kullanılmamalıdır. Çünkü goto deyimleri programın okunabilirliğini, anlaşılabilirliğini 
    bozabilmektedir. Aşağıdaki örnekte goto deyiminin çalışmasına ilişkin bir örnek veriyoruz. Ancak goro deyimi aşağıdaki 
    örnekte olduğu gibi döngü oluşturmak için kullanılmamalıdır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    i = 0;
REPEAT:
    printf("%d\n", i);
    ++i;
    if (i < 10)
        goto REPEAT;

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    goto deyimi ile başka bir fonksiyona atlanamaz. Aynı fonksiyon içerisinde başka bir yere atlanabilir. Etiketler 
    yalnızca goto işleminde etki gösterir. Yoksa programın akışı sırasında etiketle karşılaşılmasının bir etkisi yoktur. 
    Kaldı ki bir etiketin bulunyor olması bir goto bulundurulmasını zornlu kılmamaktadır. Tabii goto'suz etiketlerin de 
    bir anlamı yoktur. Bir etikete birden fazla yerden goto yapılabilir. Örneğin:

    if (falanca)
        goto EXIT;
    /* ... */
    if (filanca)
        goto EXIT;
    /* ... */
    EXIT:
        /* .... */

    goto etiketleri "fonksiyon faaliyet alanına (function scope)" ilişkindir. Yani bir fonksiyon içerisinde aynı isimli tek 
    bir goto etiketi olabilir. goto etiketlerinin blok faaliyet alanına ilişkin olmadığına dikkat ediniz. Örneğin:

    int main(void)
    {
        /* ... */
        {
            /* ... */
        EXIT:
            printf("exit\n");
        }
        /* ... */

        {
            /*  ... */
        EXIT:						/* geçersiz! */
            printf("exit\n");
        }
        return 0;
    }

    Standratlara göre goto etiketinden sonra bir deyim bulunmak zorundadır. Çünkü aslında bir deyim için goto yapılmaktadır. 
    Örneğin:

    void foo(void)
    {
        /* ... */

        if (falanca)
            goto EXIT;

        /* ... */
    EXIT:					/* geçersiz! etiketten sonra bir deyim olması gerekir */
    }

    Bu tür durumlarda boş deyimden faydalanılabilir. Örneğin:

    void foo(void)
    {
        /* ... */

        if (falanca)
            goto EXIT;

        /* ... */

    EXIT:					/* geçerli, goto etiketinden sonra bir deyim var */	
        ;		
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    goto deyimi üç durumda anlamlı ve güzel bir biçimde kullanılabilir:

    1) İç içe döngülerden ya da döngü içerisindeki switch deyiminden tek hamlede çıkmak için
    2) Ters sırada kaynak boşaltımı yapmak için
    3) Bazı özel algoritmalarda çözümü kolaylaştırmak için

    Aşağıdaki örnekte iç bir döngüden tek hamlede goto ile çıkılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int ch;

    for (int i = 0; i < 10; ++i) {
        for (int k = 0; k < 10; ++k) {
            printf("(%d,%d)\n", i, k);
            printf("press q to exit:");
            ch = getchar();
            if (ch == 'q')
                goto EXIT;
            if (ch != '\n')
                while (getchar() != '\n')
                    ;
        }
    }

EXIT:
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte bir döngü içerisindeki switch deyiminden tek hamlede goto ile çıkılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int get_command(void)
{
    int ch;

    while ((ch = getchar()) == ' ' || ch == '\t')
        ;

    if (ch != '\n')
        while (getchar() != '\n')
            ;

    return ch;
}

int main(void)
{
    int ch;

    for (;;) {
        printf("CSD>");

        ch = get_command();
        if (ch == '\n')
            continue;

        switch (ch) {
            case 'e':
            case 'r':
                printf("remove command executes...\n");
                break;
            case 'c':
                printf("copy command executes...\n");
                break;
            case 'd':
                printf("dir command executes...\n");
                break;
            case 'm':
                printf("move command executes...\n");
                break;
            case 'q':
                goto EXIT;
            default:
                printf("invalid command: %c\n", ch);
                break;
        }
    }
EXIT:
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Kaynak tahsisatlarında ters sırada boşaltım yapmak için goto deyminden faydalanılmaktadır. Biz kursumuzun bu noktasında henüz bu tür kavramları görmedik. 
    Ancak yine kavramsal bir örnek verebiliriz. alloc_resource isimli bir fonksiyon bir kaynağı yahsis ediyor olsun. Eğer tahsisat başarısızsa 0 değerine 
    geri dönüyor olsun. Biz de bu fonksiyonla bir dizi kaynağı tahsis etmek isteyelim. Tahsisatı geri bırakan free_Source isimli bir fonksiyonun olduğunu düşünelim.
    Örneğin:

    int foo(void)
    {
        int r1, r2, r3, r4, r5;

        r1 = alloc_resource();
        if (!r1) {
            return 0;
        }
        r2 = alloc_resource();
        if (!r2) {
            free_resource(r1);
            return 0;
        }
        r3 = alloc_resource();
        if (!r3) {
            free_resource(r1);
            free_resource(r2);
            return 0;
        }
        r4 = alloc_resource();
        if (!r4) {
            free_resource(r1);
            free_resource(r2);
            free_resource(r3);
            return 0;
        }
        r5 = alloc_resource();
        if (!r5) {
            free_resource(r1);
            free_resource(r2);
            free_resource(r3);
            free_resource(r4);

            return 0;
        }

        /* işlemler yapılıyor  */

        free_resource(r1);
        free_resource(r2);
        free_resource(r3);
        free_resource(r4);
        free_resource(r5);

        return 1;		/* başarılı */

    }

    Burada kod tekrarı oldukça kötü bir yazım oluşturmaktadır. İşte bu tür durumlarda goto ile ters sırada boşaltım uygulayabiliriz:

    int foo(void)
    {
        int r1, r2, r3, r4, r5;

        r1 = alloc_resource();
        if (!r1) 
            goro EXIT1;

        r2 = alloc_resource();
        if (!r2) 
            goto EXIT2;

        r3 = alloc_resource();
        if (!r3) 
            goto EXIT3;

        r4 = alloc_resource();
        if (!r4)
            goto EXIT4;

        r5 = alloc_resource();
        if (!r5) 
            goto EXIT5;

        /* işlemler yapılıyor  */

        return 1;		/* başarılı */

EXIT5:
        free_resource(r4);
EXIT4:
        free_resource(r3);
EXIT3:
        free_resource(r2);
EXIT2:
        free_resource(r1);	
EXIT1:
        return 0;		/* başarısız */

    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İç bir bloğa goto ile atlanırken o blokta tanımlanan değişkenler çöp değer almış olabilirler. Bu tür durumlara dikkat 
    ediniz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int val;

    printf("Bir sayi giriniz: ");
    scanf("%d", &val);
    if (val == 0)
        goto INSIDE;

    {
        int a;

        a = 10;
    INSIDE:
        printf("%d\n", a);
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                            24. Ders 23/08/2022 - Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir atama işlemi sırasında atanan değerin türüne "kaynak tür", atanan değere "kaynak değer" ve atamanın yapıldığı 
    nesnenin türüne ise "hedef tür"	denilmektedir. Atama işlemi sırasında kaynak türle hedef tür farklı türler olabilir. 
    Örneğin kaynak tür double iken hedef tür int olabilir. Farklı türlerin birbirlerine atanması sırasında kaynak türdeki 
    değer hedef türe dönüştürülür sonra atama gerçekleştirilir. Örneğin:

    int = double

    gibi bir atama söz konusu olsun. Bu durumda önce double türü int türüne dönüştürülür sonra atama gerçekleştirilir. Buna 
    "otomatik tür dönüştürmesi (implicit type conversion)" denilmektedir. Yani bir türden bir türe atamanın olması o türden 
    o türe otomatik dönüştürmenin olması anlamına gelir. C'de nümerik türler arasında otomatik dönüştürme vardır. Ancak bu 
    dönüştürmeler sırasında bilgi kaybı söz konusu olabilmektedir. Bu durumda programcının nasıl bir kayıp ile karşı karşıya 
    kalacağını kestirmesi gerekir. Swift gibi Rust gibi bazı dillerde hiçbir zaman farklı türler birbirilerine atanamamaktadır. 
    Farklı türlerin birbirlerine atanabilirliğine programlama dillerinde "katı tür sistemi (strong type)" ve "gevşek tür sistemi
    (weakly type)" denilmektedir. C bu bakımdan gevşek tür tür sistemine sahip bir programlama dilidir. 
    
    Farklı türlerin birbirlerine atanmasında şu kurallar izlenmektedir (buradaki maddeleri else-if biçiminde değerlendirmelisiniz):

    1) Kaynak türdeki değer hedef türün sınırları içerisinde kalıyorsa bilgi kaybı söz konusu olmaz. Örneğin long long int 
    bir değeri int türüne atamak isteyelim: Eğer long long int içerisindeki değer int türünün sınırları içerisinde kalıyorsa 
    bilgi kaybı söz konusu olmaz. Örneğin her int değer long türünün sınırları içerisinde kalmaktadır. Bu durumda C'de int türünden
    long türüne yapılan atamalarda bir bilgi kaybı söz konusu olmaz.

    2) Büyük tamsayı türünden küçük sayı türüne yapılan atamalarda hedef tür işaretsiz bir tamsayı türü ise kaynak türdeki değerin 
    yüksek anlamlı (most significant) byte'ları kaybedilir, düşük anlamlı (least significant) byte'ları atanır. Ancak hedef tür 
    işaretli bir tamsayı türü ise bilgi kaybının nasıl olacağı derleyicileri yazanların isteğine bırakılmıştır (implementetion 
    dependent).  Örneğin:

    #include <stdio.h>

    int main(void)
    {
        int a = 0x12345678;
        unsigned short b;

        b = a;

        printf("%x\n", b);		/* 5678 */

        return 0;
    }

    Bu tür durumlarda sayılar 10'luk sistemdeyse sanki atama işleminde rastgele bir değer elde ediliyormuş duygusuna kapılınabilir. 
    Aslında yüksek byte'ler atılıp düşük anlamlı byte'lar atanmaktadır. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        int a = 56732683;			/* 0x0361AC0B */
        unsigned short b;

        b = a;

        printf("%u\n", b);		/* 0xAC0B = 44043 */

        return 0;
    }

    Büyük tamsayı türünden küçük tamsayı türüne atama yapılırken hedef tür olan küçük tamsayı türü işaretli bir tür ise bu durumda 
    bilgi kaybının nasıl olacağı derleyicileri yazanların istedğine bırkaılmışsa da derleyicilerin hemen hepsi yine saının yüksek anlamlı byte'larını 
    atmaktadır. Tabii düşük anlamlı byte atandığı zaman sayının işareti de değişebilmektedir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        int a = 56732683;		/* 0361AC0B */
        short b;

        b = a;

        printf("%d\n", b);		/* 0xAC0B = -21493 */

        return 0;
    }

    3) Kaynak tür işaretli bir tamsayı türü, hedef tür de kaynak türün işaretsiz biçimi ise bu durumda sayının bit kalıbı değişmez. 
    Yani hedef türdeki değer aynı byte değerleriyle kaynak türe atanır. Başka bir deyişle kaynak türdeki değerin bit karşılığının 
    tamamı hedef türde depolanır. Tabii işaret bitinin anlamı değişecektir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        unsigned int a = 0xAB8254C2;
        int b;

        b = a;				

        printf("%u\n", b);		/* 0xFFFFFFFF = 4294967295 */

        return 0;
    }

    İşaretsiz bir tamsayı türü aynı türün işaretli biçimine atanırsa zaten eğer kaynak değer hedef türün sınırları içerisinde 
    kalıyorsa 1. Madde uygulanır. Kalmıyorsa bilgi kaybının nasıl olacağı derleyicileri yazanların isteğine bırakılmıştır. Fakat 
    derleyicilerin hemen hepsi yine sayının bit kalıbını değiştirmeden hedef türe atamaktadır.

    4) Küçük işaretli tamsayı türünden büyük işaretsiz tamsayı türüne atama yapılırken eğer küçük işaretli tamsayı türü pozitifse 
    zaten bilgi kaybı söz konusu olmaz. Ancak negatifse bu durumda dönüştürme iki aşamada yapılmaktadır: Önce küçük işaretli 
    türdeki değer büyük türün işaretli biçimine dönüştürülür, sonra büyük türün işaretli biçiminden büyük türün işaretsiz biçimine 
    dönüştürme yapılır. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        signed char a = -1;
        unsigned int b;

        b = a;		/* önce signed char, signed int türüne sonra da unsigned int türüne dönüştürülür */

        printf("%u\n", b);		/* 4294967295 */

        return 0;
    }

    5) Gerçek sayı türlerinden tamsayı türlerine yapılan atamalarda eğer gerçek sayı türü içerisindeki değer tamsayı türü 
    ile ifade edilebiliyorsa" zaten 1. Madde uygulanır, bilgi kaybı söz konusu olmaz. Ancak kaynak türdeki gerçek sayı değeri noktalı 
    bir sayı ise (yani noktadan sonraki kısım 0 değilse) bu durumda sayının noktadan sonraki kısmı tamamen atılır, tam kısmı atanır 
    (truncation toward zero). Eğer sayının noktadan sonraki kısmı atıldıktan sonra tam kısmı hala hedef türün sınırları içerisine 
    sığmıyorsa bu durum "tanımsız davranışa (undefined behavior)" yol açmaktadır. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        double a = -12.99;
        int b;

        b = a;					/* -12 */
        printf("%d\n", b);

        return 0;
    }

    Örneğin -2.99 biçimindeki double değer işaretsiz bir tamsayı türüne atanırsa bu da tanımsız davranış oluşturmaktadır. Yani C standartlarına
    göre önce -2 değerinin elde edilip bu -2 değerinin işaretsiz tamsayı türüne dönüştürülmesi garanti edilmemiştir. 

    6) Tamsayı türlerinen gerçek sayı türlerine atama yapılırken eğer atama yapılan değer hedef gerçek sayı türü ile tam olarak ifade 
    edilebiliyorsa bilgi kaybı oluşmaz (1. Madde uygulanır), eğer tamsayı türünden değer hedef gerçek sayı türüyle tam olarak ifade edielmiyorsa
    ancak basamaksal bir kayıp söz konusu değilse (yani mantissel bir kayıp söz konusu ise) bu durumda orijinale en yakın ondan küçük olan 
    ya da orijinal değer en yakın ondan büyük olan değer elde edilir. Fakat kayıp basamaksalsa bu durumda "tanımsız davranış (undefined behavior)"
    
    #include <stdio.h>

    int main(void)
    {
        unsigned int a = 1234567890;
        float b;

        b = a;
        printf("%f\n", b);	/* 1234567936.000000 */

        return 0;
    }

    Aslında bugün kullandığımız sistemlerde long long türü bile float türüne atandığında basamaksal bir kayıp oluşmamaktadır. Fakat S 
    standartlarında "derleyicilere özgü daha büyük tamsayı türlerinin olabileceği" belirtilmiştir. derleyicilere özgü çok büyük tamsayı 
    türleri ancak basamaksal kayıplar oluşturabilir.

    7)Küçük gerçek sayı türünden büyük gerçek sayı türüne yapılan atamalarda bilgi kaybı söz konusu olmaz. (Yani 1. Madde uygulanır.) 
    Ancak büyük gerçek sayı türünden küçük gerçek sayı türüne yapılan atamalarda bilgi kaybı söz konusu olabilir. Eğer kayıp basamaksal değilse, 
    yani mantis kaybı söz konusu ise kaynak değere	en yakın ondan küçük ya da kaynak değere en yakın ondan büyük sayı elde edilir. 
    Basamaksal kayıp söz konusu olursa bu durum "tanımsız davranışa" yol açacaktır. Örneğin:

    double a = 1e200;
    float b;

    b = a;		/* undefined behavior */

    Örneğin:

    #include <stdio.h>

    int main(void)
    {
        double a = 1.234567890123;
        float b;

        b = a;
        printf("%.10f\n", b);	/* 1.2345678806 */

        return 0;
    }

    8) Herhangi bir türden _Bool türüne atama yapıldığında eğer atanan değer 0 ise 0 değeri atanır, sıfır dışı bir değer 
    ise 1 değeri atanır. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        int a = -123;
        _Bool b;

        b = a;
        printf("%d\n", b);	/* 1 */

        return 0;
    }

    Adres türleri söz konusu olduğunda NULL adres 0 olarak, NULL olmayan adres 1 olarak atanmaktadır. Adres türleri 
    ileride ayrı bir bölümde ele alınacaktır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İki operandlı bir operatör işleme sokulduğunda eğer operandlar aynı türden ise işlemin sonucu da bu türdne elde edilir. Ancak operand'lar farklı 
    türlerden ise önce operand'lar aynı türe dönüştürülür. Sonra işlem yapılır. İşlemin sonucu da dönüştürmenin yapıldığı ortak tür türünden olur. 
    C'de yalnızca değişkenlerin ve sabitlerin değil aslında her ifadenin bir türü vardır. 

    İşlem öncesi tür dönüştürmesi derleyici tarafından otomatik bir biçimde yapılmaktadır. Dönüştürmenin özet ancak üstünkörü kuralı 
    "küçük türün büyük türe dönüştürülmesi sonucun büyük tür türünden elde edilmesidir." Örneğin int ile long işleme sokulursa int önce long'a 
    dnüştürülür sonuç long türünden çıkar. Burada dönüştürme geçici nesne yoluyla yapılmaktadır. ÖÇrneğin a int türünden, b long türünden olsun. c'nid de
    long türünden olduğunu düşünelim:

    c = a + b;

    Burada dönüştürme geçici nesne yoluyla yapılmaktadır. Yani önce derleyici long türünden geçici bir nesneyi kendisi yaratır. Sonra a'yı bu nesneye 
    nesneye atar. Sonra iki long değeri toplar. Sonra geçici nesneyi yok eder ve sonucu c'ye atar. 

    temp = a;
    c = temp + b;
    temp yok ediliyor

    Uygulamada derleyiciler bu tür dönüştürmesini CPU yazmaçları içerisinde çok hızlı bir biçimde yaparlar. İşlem öncesi otomatik tür dönüştürmesinin
    ayrıntalı şöyledir:

    1) Tamsayı türü ile gerçek sayı türü işleme sokulduğunda dönüştürme her zaman gerçek sayı türüne doğru yapılır. Örneğin long long ile float işleme 
    sokulacak olsa long long türü float türüne dönüştürülür sonuç float türünden çıkar.

    2) Küçük tamsayı türü ile büyük tamsayı türü işleme sokulduğunda küçük tamayı türü büyük tamsayı türüne dönüştürülür. Örneğin int ile long işleme sokulduğunda 
    int türü long türüne dönüştürülür sonuç long türünden çıkar. Ya da örneğin int türü ile unsigned long türü işleme sokulursa şint türü unsigned long
    türüne dönüştürülür, sonuç unsigned long türünden çıkar. Ancak küçük işaretsiz tamsayı türü ile büyük işaretli tamsayı türü işleme sokulurken eğer 
    küçük tamsayı türü ile büyük tamsayı türü aynı uzunluktaysa dönüştürme büyük türün işaretsiz biçimine doğru yapılır. Örneğin int ile long türlerinin aynı 
    uzunlukta olduğunu varsayalım. Biz unsigned int ile long türünü işleme sokarsak unsigned int türü ve long türü unsigned long türüne dönüştürülür sonuç
    unsigned long türünden çıkar.

    3) Aynı tamsayı türünün işaretli ve işaretsiz biçimleri işleme sokulursa işaretli tamsayı türü işaretsize dönüştürülür sonuç işaretsiz türden çıkar.
    Örneğin int ile unsigned int işleme sokulursa int türü unsigned int türüne dönüştürülür sonuç unsigned int türünden çıkar. Örneğin:

    #include <stdio.h>

    int main(void)
    {     
        int a = -1;
        unsigned int b = 1;
        unsigned result;

        result = a * b;
        printf("%u\n", result);

        return 0;
    }

    4) İki gerçek sayı türü kendi aralarında işleme sokulursa küçük gerçek sayı türü büyük gerçek sayı türüne dönüştürülür, sonuç büyük gerçek sayı türünden çıkar.
    Örneğin float ile double işleme sokulursa sonuç double türünden çıkar.

    5) C'de tamsayı işlemleri en az int duyarlılığında yapılmaktadır. int türünden küçük olan türler kendi aralarında işleme sokulursa önce her iki tür de 
    int türüne dönüştürülür sonuç int türünden çıkar. Bu işleme "int türüne yükselme (integer (integral) promotion)" denilmektedir. Örneğin short ile short 
    işleme sokulursa sonuç short türünden çıkmaz. Önce her iki operand da bağımsız olarak int türüne dönüştürülür sonuç int türünden çıkar. Benzer biçimde 
    örneğin short türü ile char türü işleme sokulursa önce her iki operand da bağımsız olarak int türüne dönüştürülür, sonuç int türünden çıkar. int türüne 
    yükseltme kuralının şöyle bir ayrıntısı vardır: Eğer ilgili sistemde short türü ile int türü aynı uzunluktaysa bu durumda operandlardan biri unsigned 
    short ise diğeri int ya da int türünden küçük ise dönüştürme int türüne değil unsigned int türüne yapılmaktadır. Örneğin short türü ile int türünün aynı 
    olduğu DOS sisteminde çalışıyor olalım. Burada biz short ile unsigned short türünü	işleme soksak sonuç unsigned int türünden çıkar. Benzer biçimde 
    unsigned short ile int türünü işleme soksak sonuç yine unsigned int türünden çıkar (burada int türüne yükseltme kuralının değil küçük tamsayı türünün
    büyük tamsayı türüne yükseltme kuralının uygulandığına dikkat ediniz.)

    6) Bölme işleminde her iki operand da tamsayı türlerine ilişkinse sonuç tamsayı türüne ilişkin çıkar. Bölüm noktalı olsa bile noktadan sonraki kısım atılmaktadır. 
    Örneğin:

    a = 10 / 4;

    Burada 10 ve 4 int türdendir. Bu durumda a'ya 2 değeri atanır. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        int a = 10;
        int b = 4;
        double c;

        c = a / b;
        printf("%f\n", c);		/* 2.000000 */

        return 0;
    }

    Burada sonucun double çıkmasını istiyorsak operandlardan en az birinin double yapmamız gerekir. Örneğin:

    a = 10.0 / 4;

    Burada artık sonuç double türünden çıkacaktır. 	Aşağıdaki örnekte sayı tersten  basamaklarına ayrılmaktadır.

    #include <stdio.h>

    int main(void)
    {
        int a = 12345;
        int digit;

        while (a) {
            digit = a % 10;
            printf("%d\n", digit);
            a /= 10;
        }

        return 0;
    }

    7) int türüne yükseltme kuralı tek operandlı operatörlerde de yürütülmektedir. Örneğin a short türden ise -a ifadesinde işaret eksi operatörü ugulanmadan 
    önce int türünden küçük olan short türü önce int türüne yükseltilir. Sonuç int türünden elde edilir. Yani -a ifadesi int türden olur. Benzer biçimde !a 
    gibi bir işlemde de eğer a int türünden küçük olsa bile sonuç int türden elde edilir. 

    8) Daha önceden de bahsedildiği gibi C'de aşağıdaki operatörler, operandları hangi türden olursa olsun her zaman int türden 0 ya da 1 değeri üretmektedir:

    !
    <, >, <=, >=
    ==, != 
    &&
    ||

    Örneğin:

    a == b işleminde a double türden b long türden olsa bile her zaman bu işlemin sonucu int türden elde edilir. Ancak C standartlarına göre buradaki operatörler 
    önce işlem öncrsi tür dönüştürmelerine sokulur. Karşılaştırma dönüştürülmüş türe göre yapılır. Ancak sonuç her zaman int türden elde edilir. Yani örneğin 
    a == b işleminde a double türden b long türden ise, önce long tür double türüne dönüştürülür. Karşılaştırma iki double türü ile yapılır. Ancak elde edilen ürün her zaman 
    int türden olur. Benzer biçimde !a gibi bir işlemde sonuç her zaman int türden elde edilir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Tür dönüştürme işlemi programcı tarafından "açıkça (explicit)" da yapılabilmektedir. Bunun için tür dönüştürme
    operatörü (type cast operator) kullanılmaktadır. Tür dönüştürme operatörünün kullanımı şöyledir:

    (<tür>) ifade

    Dönüştürülecek türün parantezler içerisine yazıldığında dikkat ediniz. Örneğin:

    b = (double)a;

    Burada a ifadesi açıkça double türüne dönüştürülmüş sonra atama yapılmıştır. Tür dönüştürme operatörü tek operand'lı
    önek (unary prefix) bir operetördür. Diğer tek operand'lı operatörlerde olduğu gibi tür dönüştürme operatörü de öncelik
    tablosunun ikinci düzeyinde sağdan-sola grupta bulunmaktadır:
    
    ()					Soldan-Sağa
    + - ++ -- ! (tür)	Sağdan-Sola
    * / %				Soldan-Sağa
    + -					Soldan-Sağa
    <  > <= >=			Soldan-Sağa
    != ==				Soldan-Sağa
    &&					Soldan-Sağa
    ||					Soldan-Sağa
    =, +=, /=, *=,...	Sağdan-Sola
    ,					Soldan-Sağa

    Örneğin:
    
    result = (double)a / b;

    Bu ifadede de üç operatör vardır. En yüksek öncelikli operatör tür dönüştürme operatörüdür. O halde işlemler şu 
    sırada yapılacaktır:

    İ1: (double)a
    İ2: İ1 / b
    İ3: result = İ2

    Burada görüldüğü gibi a ifadesi önce double türüne dönüştürülüp sonra b'ye bölünmüştür. Eğer a / b ifadesinin sonucunu
    double türüne dönüştüreceksiniz ifadeyi parantez içerşsşne almalısınız. Örneğin:,

    result = (double)(a / b);

    Burada önce a / b işlemi yapılacak bunun sonucu double türüne dönüştürülecektir. Örneğin:

    result = (double)(long)a;

    Tür dönüştürme operatörünün sağdan sola öncelikli grupta bulunduğuna dikkat ediniz. Dolayısıyla burada işlemler
    şu sırada yapılacaktır:

    İ1: (long)a
    İ2: (double)İ1
    İ3: result = İ2

    Yani a ifadesi önce long türüne sonra double türüne dönüştürülmektedir. 

    Pekiyi açıkça (explicit) dönüştürmeye ne gerek vardır? Aslında bazı durumlarda mecburen açıkça dönüştürme kullanılmak 
    zorundadır. Özellikle göstericiler söz konusu olduğunda bazı dönüştürmelerin açıkça yapılması zorunludur. Aşağıdaki 
    ifadeye dikkat ediniz:

    int a = 10, b = 4;
    double result;

    result = a / b;

    Burada a / b işleminin sonucu 2 çıkacak ve noktadan sonraki kısım kırpılacaktır. Pekiyi biz sonucun 2.5 çıkmasını 
    nasıl sağlarız? İşte bu tür durumlarda mecburen açıkça dönüştürme uygulamak gerekir. Örneğin:

    result = (double)a / b;

    Elimizde double türden bir d değişkeni olsun. Biz de bunun tam kısmını işleme sokmak isteyelim. Böylesi bir durumda da
    mecburen açıkça tür dönüştürmesi uygulamak gerekir. Örneğin:

    result = (int)d * 2;

    Açıkça dönüştürmeler de yine "geçici nesne yoluyla" yapılmaktadır. Örneğin:

    result = (double)a / b;

    Burada a değişkeni double türüne kalıcı olarak dönüştürlmemektedir. Bu işlem için dönüştürme uygulanmaktadır. Yani 
    bu işlemin eşdeğeri şöyledir:

    double result = a;
    result = temp / b;
    <temp yok ediliyor>

    Tabii derleyiciler bu biçimdeki geçici nesneleri genellikle CPU yazmaçlarında oluşturmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{     
    int a = 10, b = 4;
    double result;

    result = a / b;     
    printf("%f\n", result);     /* 2.000000 */

    result = (double)a / b;
    printf("%f\n", result);     /* 2.500000 */


    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
                                                25. Ders 01/09/2022 - Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir işlemde operandlar aynı türdense ya da farklı türlendense ancak işlem öncesi otomatik tür dönüştürmesi uygulanmışsa elde edilen değer söz konusu 
    ortak türün sınırları içerisine girmiyorsa bu duruma "taşma (overflow)" denilmektedir. C'de işaretli tamsayı türleri üzerinde taşma oluşursa bu durum 
    "tanımsız davranışa"" yol açmaktadır. Ancak işaretsiz tamsayı türleri üzerinde taşma olursa her zaman taşan yüksek anlamlı byte'lar atılmaktadır. Örneğin:

    int a, b, c;
    ...
    c = a + b;

    Burada a ve b int türden olsun. a + b işleminin sonucu da int türden olacaktır. Ancak a + b işleminin sonucu int türün sınırları içerisinde kalmıyorsa 
    bu durum tanımsız davranışa yol açar. Örneğin:

    unsigned int a, b, c;
    ...
    c = a + b;

    Burada a + b işleminin sonucu unsigned int türden olacaktır. Ancak sonuç unsigned int türünün sınırları dışında ise bu durum tanımsız davranış değildir. 
    Yüksek anlamlı byte'lar her zaman atılır. Tabii taşma bazı tek operandlı operatörlerde de ortaya çıkabilir. Örneğin:

    int a, b;

    a = -2147483648;
    b = -a;				/* tanımsız davranış */


    Burada -a işleminin sonucu da int türdendir. Ancak bu işlemin sonucunda elde edilen değer int türünün sınırları içerisinde değildir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İşaretsiz bir tamsayı türüne işaret eksi operatörü uygulanırsa tür int türünden küçükse önce int türüne yükseltme kuralı uygulanır. Sonra eğer sayı işaretsiz 
    ise önce onun negatifi elde edilir. Elde edilen negatif değer işaretsiz türe dönüştürülür. Örneğin:

    usnigned a = 1, b;

    b = -a;

    Burada -a işleminin sonucu unsigned int türden olacaktır. -a işleminde önce -1 elde edilir. Ancak sonuç unsigned int türünden olacağı için unsigned int türüne
    dönüştürülür. Böylece işlemden en büyük unsigned int değer elde edilir. Başka bir deyişle burada 1 değerine ikiye tümleme işlemi uygulanır ve elde edilen değer 
    unsigned int biçiminde ele alınır.  Ya da örneğin -a gibi bir değer -1 * a olarak düşünülebilir. Bu durumda a unsigned int türden ise -1 de unsigned 
    int türüne dönüştürülür. Buradan en büyük pozitif değer elde edilir. Bu değer a ile çarpılıp yüksek anlamlı byte'lar atılırsa aslında önceki ifade 
    edilen durumla aynı durum oluşmuş olur. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned a = 1, b;

    b = -a;
    printf("%u\n", b);			/* 4294967295 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de geleneksel olarak bir fonksiyonun parametresi int türünden küçük bir türdense parametre o türden değil int türünden ifade edilir. Aynı durum geri 
    dönüş değeri için de uygulanmaktadır. Bu bir zorunluluk değildir. Ancak bir gelenektir. Örneğin foo fonksiyonun parametresinin short bir değer aldığını varsayalım. 
    Programcı parametreyi short yapmaz int yapar. Benzer biçimde putchar fonksiyonu bir karakteri ekrana (stdout dosyasına) yazdırmaktadır. Parametre char 
    yerine int yapılmıştır. Aynı durum geri dönüş değerleri için de benzer biçimde uygulanmaktadır. Bu nedenle C programlarında genel olarak fonksiyonların 
    parametrelerinde ve geri dönüş değerlerinde char gibi, short gibi türler geleneksel olarak kullanılmaz. Onların yerine int türü kullanılır. 

    Pekiyi neden fonksiyonların parametrelerinde ve geri dönüş değerlerinde int türünden küçük türler programcılar tarafından tercih edilmemektedir? 
    Yani bu geleneğin anlamı nedir? İşte bunun iki nedeni vardır:

    1) C'de zaten tamsayı işlemleri her zaman "int türüne yükseltme kuralı" gereği en az int duyarlılığında yapılmaktadır. Bu durumda bir değişkenin int türünden
    küçük olmasının çoğu kez bir anlamı yoktur. Aynı zamanda parametre aktarımı ve geri dönüş değerinin oluşturulması da zaten işlemciler tarafından en az 
    int duyarlılıkta yapılmaktadır. Yani parametrelerin ve geri dönüş değerlerinin int türden olması daha doğal bir gösterim sunmaktadır. 

    2) Eskiden fonksiyon prototiplerinin olmadığı zamanlarda zaten "default argument conversion" kuralı gereğince int türünden küçük olan türler int türüne 
    yükseltilerek fonksiyona aktarılıyordu. Dolayısıyla bu gelenek zaten eski zamanlardan beri bu gerekçeyle uygulanıyordu.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C standartlarına göre bir kodun etkisi aynı kalacak biçimde derleyiciler kodu daha hızlı çalışacak ya da daha az yer kaplayacak biçimde optimize edebilir. 
    Burada önemli olan programcının varsaydığı ya da oluşturmak istediği her şeyin optimize edilmil kodda oluşturulmuş olmasıdır. Örneğin:

    x = a + b + c + 1;
    y = a + b + c + 2;
    z = a + b + c + 3;

    Biz bu kodu böyle yazmış olsak da örneğin derleyici daha az işlem yapılacak şekilde kodu şöyle düzenleyebilir:

    temp = a + b + c;
    x = temp + 1;
    y = temp + 2;
    z = temp + 3;

    Biz derleyicinin kodu böyle düzenlediğini bilmek zorunda değiliz. Ne de olsa bizim niyetlediğimiz her şeyi derleyicinin optimize ettiği kod da yapmaktadır. Örneğin:

    for (int i = 0; i < 10; ++i)  {
        printf("%d\n", i);
        x = 100;
    }
    printf("%d\n", x);

    Burada x'in döngü içerisinde durmasının programı yavaşlatmak dışında hiçbir anlamı yoktur. Derleyici kodu şöyle optimize edebilir:

    for (int i = 0; i < 10; ++i)  {
        printf("%d\n", i);
    }
    x = 100;
    printf("%d\n", x);

    Örneğin:

    for (int i = 0; i < 1000000; ++i)
        ifade;

    Derleyici bu kodu isterse aşağıdaki gibi düzenleyebilir ve biz bunu programın çalışması sırasında anlayamayız:

    for (int i = 0;  < 1000000; i += 5) {
        ifade;
        ifade;
        ifade;
        ifade;
    }

    Örneğin:

    int foo(void)
    {
        return 100;
    }
    ...
    x = foo();

    Burada derleyici aslında bu fonksiyonu hiç çağırmadan aşağıdaki gibi de kod üretebilir:

    x = 100;

    Ancak fonksiyon şöyle olsaydı:

    int foo(void)
    {
        printf("foo\n");

        return 100;
    }
    ...
    x = foo();

    Artık bu optimizasyonu yukarıdaki gibi yapamayacaktı. Tabii derleyici kütüpahedeki ya da başka modüldeki fonksiyonlar
    için bu biçimde bir optimizasyon yapamamaktadır.

    C derleyicilerinde optimizasyonlar genellikle derleyicilerin komut satırı seçenekleriyle azaltılıp yükseltilebilmektedir. 
    Döngü açımı (loop unrolling), otomatik inline açımı gibi optimizasyonlar "argresif" optimizasyonlardır. Pek çok 
    derleyicide bu optimizasyonların yapılabilmesi için özel komut satırı seçeneklerinin girilmesi gerekmektedir. Kod 
    optimizasyonlarının derleme zamanı üzerinde ciddi yavaşlatıcı etkileri olabilmektedir. Kod optimizasyonu programın
    kaynak kod düzeyinde debug edilmesini engelleyebilmektedir. Bu nedenle kaynak kod düzeyinde debug yapılabilmesi için
    optimizasyonların büyük ölçüde kapatılması gerekmektedir. Örneğin Microsoft C derleyicilerinde projenin "debug" 
    versiyonunda optimizasyonlar kapatılırken, ""relese" versiyonunda optimizasyonlar açılmaktadır. gcc ve clang derleyicilerinde
    optimizasyonlar kategorik olarak -O0, -O1, -O2 ve -O3 gibi seçeneklerle ayarlanabilmektedir. Microsoft derleyicilerinde 
    de benzer biçimde /Od, /O1, /O2 ve /Ox seçenekleri bulunmaktadır. 

    Optimizasyon default olarak hıza dayalı biçimde yapılmaktadır. Optimize edilmiş kod optimize edilmemiş koddan daha 
    büyük hale gelebilmektedir. (Örneğin döngü açımında aslında kod büyümektedir.) Fakat bazen derleyicilerin hız yerine
    kodu küçültecek optimizasyon yapmasını da isteyebiliriz. Özellikle küçük kapasiteli bilgisayar sistemlerinde ve 
    mikrodenetleyicilerde bu tür istekler söz konusu olabilmektedir. gcc ve clang derleyicilerinde -Os seöeneği, 
    Microsoft derleyicilerinde /Ox seçeneği size optimizasyonları için kullanılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de ismi isxxx başlayan ve ismine "karakter test fonksiyonları" denilen bir grup standart C fonksiyonu vardır. Bu fonksiyonların parametreleri int türden 
    ve geri dönüş değerleri de int türdendir. Her ne kadar bu fonksiyonların parametreleri int türdense de aslında bu fonksiyonlar char bir değeri parametre 
    olarak alırlar. Bu fonksiyonlar parametreleriyle aldıkları karakteri test ederler. Eğer test olumlu ise sıfır dışı herhangi bir değere olumsuz ise sıfır değerine
    geri dönerler. Bunların listesi şöyledir:

    isupper			Büyük harf bir karakter mi?
    islower			Küçük harf bir karakter mi?
    isalpha			Alfabetik karakter mi?
    isalnum			Alfabetik ya da nümerik bir karakter mi?
    isdigit			Sayısal bir karakter mi?
    isxdigit		HEx digit bir karakter mi?
    isspace			Boşluk karakterlerinden biri mi?
    ispunct			Noktalama karakterlerinden biri mi?
    isascii			İlk 128 karakterden biri mi?
    iscntrl			Kontrol karakterlerinden biri mi? (ASCII tablosunun ilk 32 karakteri kontrol karakterleridir)

    Bu fonksiyonlar kullanılırken <ctype.h> dosyası include edilmelidir. Karakter test fonksiyonları yalnızca ACII tablosundaki karakterler için çalışmaktadır. 
    Biz bu fonksiyonlarla öğrneğin Türkçe karakterleri test edemeyiz. 

    Karakter test fonksiyonlarının parametreleri unsigned char türüyle temsil edilebilmelidir. Yani örneğin biz bu fonksiyonlara [0, 255] aralığının dışında 
    herhangi bir değer girersek bu fonksiyonlar tanımsız davranışa yol açarlar. Bu fonksiyonlar UTF-8 gibi multibyte karakterler için kullanılamazlar. 
    Ancak bir byte'lık encodinglerde lokal spsifik davranış gösterebilirler. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    int ch;

    printf("Bir karakter giriniz:");
    ch = getchar();

    if (isupper(ch))
        printf("upper case\n");
    else if (islower(ch))
        printf("lower case\n");
    else if (isdigit(ch))
        printf("digit\n");
    else if (isspace(ch))
        printf("white space char\n");
    else
        printf("another character\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İki önemli karakter fonksiyonu da toupper ve tolower fonksiyonlarıdır. toupper fonksiyonu küçük bir karakteri büyük harfe, tolower fonksiyonu da
    büyük harf bir karakteri küçük harfe dönüştürür. toupper eğer parametresi küçük bir karakter değilse aynı karakterle geri dönmektedir. 
    Benzer biçimde tolower fonksiyonu da eğer parametresi büyük bir harf bir karakter değilse aynı değerle geri döner. Fonksiyonların parametrik yapıları şöyledir:

    int toupper(int ch);
    int tolower(int ch);

    Her ne kadar bu fonksiyonların parametreleri ve geri dönüş değerleri int türdense de aslında char türden bir bilgiyi kabul etmektedir. 
    Bu fonksiyonların geri döndürdüğü int değerlerin yüksek anlamlı byte'ları her zaman 0'dır. Düşük anlamlı byte'larında dönüştürülmüş karakterin numarası vardır. 
    Dolayısıyla biz bu fonksiyonların geri dönüş değerlerini char türünden bir nesneye atayabiliriz. 

    Bu fonksiyonların parametreleri de unsigned char sınırları içerisinde [0, 255] arasında değerler girilebilir.   

    Bu fonksiyonları kullanırken de <ctype.h> dosyasının include edilmesi gerekir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    int ch;

    printf("Bir karakter giriniz:");
    ch = getchar();

    ch = toupper(ch);
    putchar(ch);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte "case insensitive" karakter karşılaştırması örneği verilmiştir. Biz bir karakter toupper ya da tolower fonksiyonuna sokup bunun sonucunu
    karşılaştırırsak "case insensitive" karşılaştırma yapmış oluruz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <ctype.h>

int main(void)
{
    int ch;

    printf("(e)vet/(h)ayir?\n");
    ch = getchar();
    ch = tolower(ch);

    if (ch == 'e')
        printf("evet\n");
    else if (ch == 'h')
        printf("hayir\n");
    else
        printf("e ya da h girilmedi!\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    toupper fonksiyonunu basit bir biçimde aşağıdaki yazabiliriz. Tabii bu yazımda küçük harf ve büyük harflerin karakter tablosunda peşi sıra gittiği 
    varsayılmaktadır. ASCII tablosunda bunlar gerçekten peşi sıra girmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int mytoupper(int ch)
{
    if (ch >= 'a' && ch <= 'z')
        return ch - 'a' + 'A';

    return ch;
}

int main(void)
{
    char ch;

    ch = getchar();
    ch = mytoupper(ch);

    putchar(ch);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                        26. Ders 06/09/2022 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de derleme işlemi kaynak dosyada yukarıdan aşağıya doğru yapılmaktadır. Bir fonksiyonun çağrıldığını gören derleyici fonksiyonun çağrılma noktasına kadar 
    fonksiyonun geri dönüş değerinin türü hakkında bir bilgiyi edinmesi gerekir. Çünkü çağrılma noktasında doğru kodu üretebilmek için derleyicinin 
    çağrılan fonksiyonun en azından geri dönüş değerinin türünü biliyor olması gerekmektedir. C90'da fonksiyonun çağrılma noktasına kadar fonksiyonun 
    geri dönüş değerinin türü hakkında derleyici bir bilgi edinememişse fonksiyonun int türden geri dönüş değerine sahip olduğunu ancak herhangi bir 
    parametrik yapıya sahip olabileceğini varsayarak kod üretmektedir. Bu durumda eğer derleyici fonksiyonu çağrılma noktasının aşağısında 
    int geri dönüş değerinin dışında bir geri dönüş değeri türüyle tanımlandığını görürse bu durum geçersizdir ve error oluşturmaktadır. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        foo();

        return 0;
    }

    int foo(void)
    {
        return 100;
    }

    Bu kod C90'a göre geçerlidir. Çünkü yukarıdan aşağıya derleyici fonksiyonun çağrılma noktasına kadar fonksiyonun geri dönüş değerinin türünü anlayamadıysa
    fonksiyonun int geri dönüş değerine sahip olduğu varsayımıyla kod üretir. Daha sonra fonksiyonun gerçekten de int geri dönüş değerine sahip olarak 
    tanımlandığını görürse kendi varsaydığı durumla gerçekleşen durum aynı olduğu için bu durumda herhangi bir sorun ortaya çıkmaz. Ancak eğer fonksiyonun 
    daha aşağıda int geri dönüş değeri dışında herhangi bir geri dönüş değerine sahip olarak tanmlandığını görürse bu durumda yanlış ürettiği için derleme işlemi 
    başarısız olacaktır. Derleyici bu tür durumlarda geri dönüp ürettiği kodu düzeltmemektedir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        foo();

        return 0;
    }

    double foo(void)				/* error! */
    {
        return 3.14
    }

    C90'da çağrılma noktasına kadar fonksiyonun geri dönüş değerinin türüne ilişkin derleyici bilgi edinememişse onun int geri dönüş değerine sahip 
    ancak herhangi bir parametrik yapıya ilişkin olabileceği varsayımıyla kod üretmektedir. Yani C90'da da fonksiyon daha aşağıda int geri dönüş değerine sahip
    ancak farklı bir parametrik yapıyla tanımlanmışsa bu durum yine geçerlidir. Örneğin:
    
    #include <stdio.h>

    int main(void)
    {
        foo();

        return 0;
    }

    int foo(int a, int b)			/* C90'da geçerli */
    {
        return a + b;
    }

    Ancak C++'ta ve C99 ve ötesinde yukarıda açıklanan kural değiştirilmiştir. C99 ve ötesinde artık bir fonksiyon çağrılmışsa çağrılma noktasına kadar
    mutlaka derleyicinin fonksiyonun en azından geri dönüş değerinin türü hakkında bir bilgiyi edinmiş olması gerekmektedir. Dolayısıyla aşağıdaki 
    örnek C90'da geçerli olduğu halde C99 ve ötesinde (ve C++'ta) geçersizdir:

    int main(void)
    {
        foo();						/* C99 ve ötesinde geçersiz! Ancak C90'da geçerli */

        return 0;
    }

    int foo(void)			
    {
        return 100
    }

    İşte çağrılma noktasına kadar çağrılan fonksiyonun en azından geri dönüş değerinin türü hakkında derleyicinin bir bilgi edinebilmesinin iki 
    yoldu vardır:

    1) Çağrılan fonksiyonu çağıran fonksiyonun daha yukarısında tanımlamak
    2) Çağrılan fonksiyonun "prototip" denilen bir bildirimini çağrılma noktasından yukarıda bir yere yerleştirmek. Örneğin:

    double foo(void)
    {
        return 3.14;
    }

    int main(void)
    {
        double result;

        result = foo();
        printf("%f\n", result);

        return 0;
    }

    Burada çağrılan fonksiyon çağıran fonksiyonun yukarısında tanımlandığı için herhangi bir sorun yoktur. 
----------------------------------------------------------------------------------------------------------------------*/
    
/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon protoipleri bir "tanımalama (definition)" işlemi değildir. Yani prototip bildirimini gördüğünde derleyici bellekte bir yer ayırmaz. 
    Yalnızca o fonksiyonun geri dönüş değeri ve parametrik yapısı hakkında bilgi edinir. Prototip bildiriminin genel biçimi şöyledir:

    <fonksiyonun geri dönüş değerinin türü> <fonksiyonun_ismi>([parametre bildirimi]);

    Örneğin:

    double foo(void);			/* Fonksiyon prototip bildirimi */

    int main(void)
    {
        double result;

        result = foo();
        printf("%f\n", result);

        return 0;
    }

    double foo(void)
    {
        return 3.14;
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Prototip bildirimi oluşturmanın en pratik yolu fonksiyon tanımlamasının ilk satırını alıp sonuna ';' atomunu yerleştirmektedir. Örneğin:

    double div(double a, double b)
    {
        return a / b;
    }

    Burada bu fonksiyonun ilk satırı alınıp sonuna ';' konulursa zaten prototip haline gelir. 

    double div(double a, double b);
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

double div(double a, double b);

int main(void)
{
    double result;

    result = div(3, 2);
    printf("%f\n", result);

    return 0;
}

double div(double a, double b)
{
    return a / b;
}

/*----------------------------------------------------------------------------------------------------------------------
    Prototipteki parametre değişkeni isimleriyle tanımlamadaki parametre değişkenlerinin isimlerinin uyuşması bir zorunluluk değildir. Örneğin:

    double div(double x, double y);
    
    /* .... */

    double div(double a, double b)			/* geçerli */
    {
        return a / b;
    }

    Prototipte yalnızca parametre değişkenlerinin türleri belirtilebilir, isimleri belirtilmeyebilir. Örneğin:

    double div(double, double);		/* geçerli */

    Prototipte belirtilen geri dönüş değeri türü ve parametre türlerinin eğer tanımlama yapılmışsa tanımlamadkiyle uyuşması zorunludur. Aksi takdirde 
    kod geçersizdir. Örneğin:

    double div(double a, double b);
    
    /* .... */

    double div(float a, float b)			/* geçersiz! */
    {
        return a / b;
    }

    Bir fonksiyon ikinci kez tanımlanamaz ancak bir fonksiyonun prototipi birden fazla kez bildirilirse bir sorun oluşturmaz. Tabii bu durumda tüm prototip
    bildirimlerindeki geri dönüş değeri türü ve parametre türlerinin aynı lması gerekir. Örneğin:


    double div(double x, double y);
    double div(double x, double y);		/* geçerli, aynı olmak koşuluyla bir prototip bildirimi birden fazla kez yazılabilir */

    /* .... */

    double div(double a, double b)			/* geçerli */
    {
        return a / b;
    }

    C90'da prototip bildiriminde her ne kadar anlamsız olsa da geri dönüş değerinin türü yazılmayabiliyordu. Bu durumda geri dönüş değerinin türü için
    int yazılmış olduğu varsayılıyordu. Ancak bu kural C99 ile birlikte kaldırılmıştır. Artık prototipte geri dönüş değerinin türü yazılmak zorundadır. Örneğin:

    foo(void);		/* Bu prototip C90'da geçerli ancak C99 ve sonrasında geçersiz! */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'nin tüm versiyonlarında prototipte parametre parantezinin içinin boş bırakılmasıyla void yazılamsı farklı anlamlara gelmektedir. Parametre 
    parantezinin içi boş bırakılırsa bu durum "derleyicinin çağrılma sırasında parametreleri sayıca kontrol etmeyeceği" anlamına gelmektedir. Oysa parametre 
    parantezinin içine void yazılması fonksiyonun parametreye sahip olmadağı anlamına gelir. Örneğin:

    #include <stdio.h>

    void foo();             /* Bu prototip parametrelerin herhangi bir biçimde olabileceği anlamına gelmektedir */

    int main(void)
    {
    
        foo(10, 20);        /* geçerli, parametreler sayıca kontrol edilmiyor */

    
        return 0;
    }

    void foo(double a, double b)
    {
        printf("foo\n");
    }

    Ancak örneğin:

    #include <stdio.h>

    void foo(void);             /* bu prototip fonksiyonun parametreye sahip olmadığı anlamına gelmektedir */

    int main(void)
    {
    
        foo(10, 20);        /* geçersiz! Fonksiyon parametreye sahip değil */

        return 0;
    }

    void foo(void)
    {
        printf("foo\n");
    }

    Tabii eğer parametre parantezinin içinin boş bırakıldığı bir prototipten sonra artık derleyici parametre parantezinin içinin boş bırakılmaıdğı bir prototoip
    ile ya da fonksiyonun tanımlamasıyla karşılaşırsa bu durumda parametre kontrolü artık yapılır. Örneğin:

    #include <stdio.h>

    void foo();                 /* bu prototip parametre kontrolünün yapılmayacağı anlamına geliyor */

    void foo(int a, int b)		/* artın bu tanımlamayla fonksiyonun parametreleri çağrım sırasında derleyici tarafından kontrol edilecektir */
    {
        printf("foo\n");
    }

    int main(void)
    {
    
        foo(10, 20, 30);        /* geçersiz! */

        return 0;
    }

    Aşağıdaki iki prototip birlikte bulunabilir:

    void foo();                 /* bu prototip parametre kontrolünün yapılmayacağı anlamına geliyor */
    void foo(int a, int b);		/* artık derleyici parametre kontrolü yapacaktır */


    Tabii parametre parantezinin içi boş bırakıldığında fonksiyon yine uygun olmayan sayıda argümanla çağrılırsa bu durum "tanımsız davranışa" yol açar. 
    Örneğin:

    #include <stdio.h>

    void foo();                 /* bu prototip parametre kontrolünün yapılmayacağı anlamına geliyor */

    int main(void)
    {
    
        foo(10, 20, 30);        /* geçerli! derleme başarıyla sonuçlanır, ancak tanımsız davranış oluşur */

        return 0;
    }

    void foo(int a, int b)
    {
        printf("foo\n");
    }

    Pekiyi prototipte parametre parantezinin içinin boş bırakılabilmesi gibi bir kuralın anlamı nedir? İşte eskiden (80'lerin ilk yarısına kadar) prototip 
    diye bir kavram C'de yoktu. Yalnızvca "fonksiyon bildirimi (function declarations)" denilen bir kavram vardı. Fonksiyon bildiriminde de parametre 
    parantezinin içi boş bırakılyordu. Derleyici de o zamanlar fonksiyon bildirimini gördüğünde parametre kontrolü yapmıyordu. Daha sonra C'ye fonksiyon 
    prototipleri eklenince eskiye doğru uyumu korumak için prototiplerde parametre parantezinin içinin boş bırakılabilmesi geçerli kabul edildi. 
    Yani prototiplerde parametre parantezinin içinin boş bırakılabilmesi tamamen eskiye doğru uyumu korumak için düşünülmüştür. Tabii programcı prototip 
    yazarken parametre türlerini belirtmelidir. 

    C++ C'nin geçmişe doğru uyumu koruma gibi bir çabasına ortak olmamıştır. Dolayısıyla örneğin C++'ta prototiplerde parametre parantezinin içinin 
    boş bırakılmasıyla void yazılması arasında hiçbir farklılık yoktur. Her iki durum da "fonksiyonun parametreye sahip olmadığı" anlamına gelmektedir. Örneğin:

    void foo();
    void foo(void);		// C++'ta ikisi arasında hiçbir farklılık yok
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında C'de farklılık olduğunu belirtmiştik. Ancak fonksiyon tanımlaması söz konusu 
    olduğunda parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında hiçbir farkılık yoktur. Her iki durum da "fonksiyonun" parametreye
    sahip olmadığı anlamına gelir. Örneğin:

    #include <stdio.h>

    void foo()      /* tanımlamada parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında farklılık yok */
    {
        printf("foo\n");
    }

    int main(void)
    {
    
        foo(10);        /* geçersiz! foo'nun parametresi yok! */

        return 0;
    }

    Biz kursumuzda parametreye sahip olmayan fonksiyonların tanımlamasında açıkça parametre parantezinin içerisine void
    anahtar sözcüğünü yerleştireceğiz. C'deki gelenek daha çok bu biçimdedir. Ancak C++'ta programcılar genellikle 
    prototipte ya da tanımlamada parametre parantezinin içerisinde void anahtar sözcüğünü yerleştirmezler. Biz de C++ 
    kurslarında parametre parantezlerinin içerisine void anahtar sözcüğünü yerleştirmiyoruz.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun prototipinin yazılmış olması onu çağırmayı zorunlu hale getirmez. Yazni biz onlarca fonksiyonun prototipini yazıp onları hiç çağırmayabiliriz. 
    Bu durum tamamen geçerlidir. Fonksityon prototipleri bir tanımlama olmadığı bellekte yer kaplamazlar. Yalnızca derleyici tarafından kod derlenirken bunlardan
    faydalanılmaktadır. Tabii çok sayıda fonksşyon için prototip yazıldığında derleyici bunların hepsini gözden geçireceği için mikro mertebede de olsa derleme süresi uzayabilir. 
    Ancak program çalışırken bu protiplerin bellk üzerinde ya da performans üzerinde hiçbir etkisi yoktur.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon prototipleri global düzeyde ya da yerel düzeyde bildirilebilir. Eğer prototip global alana yerleştirilirse bu durumda yerleştirildiği 
    yerden dosyanın sonunaa kadar her yerde etikili olur. Eğer prototip bir yerel bloğa yerleştirilmişse yerleştirildiği yerde o bloğun sonuna 
    kadarki bölgede etkili olur. Hemen her zaman programcılar prototipleri global düzeysde programın tepesinde ya da bir başlık dosyasının içerisinde 
    bildirirler. Örneğin

    #include <stdio.h>

    void foo(void);

    int main(void)
    {
        void bar(void);

        foo();        /* geçerli, prototip bildirimi görülmüş durumda */
        bar();		  /* geçerli, prototip bildirimi görülmüş durumda */

        return 0;
    }

    void tar()
    {
        foo();		/* geçerli prototip bildirimi görülmüş durumda */
        bar();		/* geçersiz! prototip bildirimi derleyici tarafından görülmüyor */
    }

    void foo(void)      /* tanımlamada parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında farklılık yok */
    {
        printf("foo\n");
    }

    void bar(void)
    {
        printf("bar\n");
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Derleyici standart C fonksiyonlarının farkında değildir. Yani derleyici bizim foo fonksiyonumuz için ne yapıyorsa printf, sqrt gibi standart C 
    fonksiyonları için de aynı şeyi yapar. Başka bir deyişle örneğin derleyici printf fonksiyonunu gördüğünde ona hiçbir özel muamele yapmamaktadır. 
    O halde standart C fonksiyonları için de prototip gerekmektedir. Eğer standart C fonksiyonları için prototip yazılmzsa C90'da onların geri dönüş değerleri 
    int kabul edilmektedir. C99 ve ötesinde zaten bu durum geçerli değildir. Örneğin aşağıdaki kodda sqrt fonksiyonun prototipi olmadığı için 
    C90'da onun geri dönüş değeri int kabul edilecek ve "tanımsız davranış" oluşacaktır. C99 ve ötesinde aşağıdaki kod geçerli değildir:

    #include <stdio.h>

    int main(void)
    {
        double result;

        result = sqrt(10);			/* C90'da tanımsız davranış! C99 ve ötesinde geçersiz' */
        printf("%f\n", result);

        return 0;
    }

    Standart C fonksiyonları için prototipleri biz kendimiz yazabiliriz. Ama bu tavsiye edilen bir durum değildir. Örneğin:

    #include <stdio.h>

    double sqrt(double);

    int main(void)
    {
        double result;

        result = sqrt(10);			/* geçerli, prototip yazılmış ve doğru */
        printf("%f\n", result);

        return 0;
    }

    İşte C'de standart C fonksiyonarının prototipleri gruplara ayrılarak çeşitli başlık dosyalarının içerisine yazılmıştır. Örneğin tüm matematiksel 
    fonksiyonların prototipleri <math.h> dosyasının içerisine, tüm dosya, ekran ve klavye fonksiyonlarının prototipleri <stdio.h> dosyasının içerisine, 
    tüm karakter test fonksiyonlarının prototipleri <ctype.h> dosyasının içerisine yerleştirilmiştir. Böylece programcı standart C fonksiyonlarının 
    protiplerini elle yazmak yerine onların zaten yazılı olduğu başlık dosyasını include eder. Burada önemli bir nokta bu başlık dosyalarında bu fonksiyonların
    tanımlamalarının yani kodlarının olmadığı yalnızca prototiplerinin olduğudur. 

    Tabii bir bir başlık dosyasını include ettiğimizde aslında bir grup standart C fonksiyonunun prototipini kaynak koda eklemiş oluruz. Ancak yukarıda da 
    belirttiğimiz gibi bir fonksiyonun prototipinin yazılmış olması onu çağırmayı zorunlu hale getirmemektedir. 

    O halde programcı tipik olarak hangi standart C fonksiyonunu çağıracaksa onun prototipinin hangi başlık dosyasında olduğunu öğrenmeli ve o dosyayı 
    include etmelidir. 

    Tabii aslında başlık dosyalarının içerisinde prototiplerden başka birtakım bildirimler de vardır. Konular ilerledikçe biz bu başlık dosyalarının ieçrisinde 
    başka nelerin olduğunu da göreceğiz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                            27. Ders 08/09/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Başlık dosyalarında standart C fonksiyonlarının prototiplerinin bulunduğu belirtmiştirk. Pekiyi bunların kendileri nerededir? İşte standart C fonksiyonları
    derlenmiş bir biçimde "kütüphane (library)" denilen özel dosyaların içerisinde bulunmaktadır. Kütüphaneler linker tarafından link işlemi sırasında
    bakılmaktadır. Linker standart C fonksiyonlarının bulunduğu kütüphane dosyalarına otomatik olarak bakar böylece onları oradan alır. Kütüphaneler 
    içerisinde fonksiyonlar derlenmiş bir biçimde bulunmaktadır. Dolayısıyla onların yeniden derlenmesine gerek olmaz. 

    Tabii kütüphaneler programcılar tarafından da oluşturulabilmektedir. Programcılar da kütüphane dosyaları oluşturup link aşamasında linker'a oluşturdukları
    kütüphaneye de bakmasını söyleyebilmektedirler. Yukarıda da belirttiğimiz gibi linker zaten (genellikle) otomatik olarak standart C fonksiyonlarının 
    bulunduğu kütüphanelere de bakmaktadır.

    Kütüphane dosyaları "statik kütüpahenler" ve "dinamik kütüphaneler" olmak üzere ikiye ayrılmaktadır. Statik kütüphane dosyalarının Windows'ta uazantıları
    ".lib" biçiminde UNIX/Linux ve macOS sistemlerinde ise ".a" (archive sözcüğünden kısaltma) biçimindedir. Dinamik kütüphanelerin ise Windows'ta uzantıları ".dll"
    (dynamic link library'den kısaltma) biçimindedir. 

    Tabii kütüphaneler programcılar tarafından da oluşturulabilmektedir. Programcılar da kütüphane dosyaları oluşturup link aşamasında linker'a oluşturdukları
    kütüphaneye de bakmasını söyleyebilmektedirler. Yukarıda da belirttiğimiz gibi linker zaten (genellikle) otomatik olarak standart C fonk/Linux ve macOS sistemlerinde
    ise ".so" (shared object sözcüklerinden kısaltma) biçimindedir. Bugün Windows, UNIX/Linux ve macOS sistemlerinde ağırlıklı biçimde dinamik kütüphaneler 
    kullanılmaktadır. 

    Statik kütüphane dosyaları "object modüllerden", object modüller ise fonksiyonlardan oluşmaktadır. Yani bir statik kütüphane aslında doğrudan fonksiyonları tutmaz. 
    Object modülleri tutar. Derlenmiş fonksiyonlar object modüllerin içerisindedir. O halde biz bir grup fonksiyonu statik kütüphane dosyasının içerisine yerleştirmek istersek
    ona onu derleriz. Object modül haline getiririz. Object modülü statik kütüphane dosyasına ekleriz. 

    Linker programı kaynak kodda olmayan fonksiyonları belirtilen kütüphane dosyalarında aramaktadır. Eğer fonksiyonu linker bir statik kütüphane dosyasında 
    bulursa onun içinde bulunduğu object modülün tamamını oradan alarak çalıştırılabilen dosyaya enjekte eder. Böylece artık çalıştırılabilen dosya 
    gerçekten kütüphanedeki fonksiyonların makine kodlarını içeriyor durumda olur. Dolayısıyla artık bu program çalıştırılırken statik kütüphane dosyalarının 
    bulundurulmasına gerek kalmamaktadır. Burada iki önemli nokta vardır:

    1) Linker fonksiyonu statik kütüphane dosyasında bulursa onun içinde bulunduğu object modülün hepsini çalıştırılabilen koda enjekte eder. 
    Örneğin biz yalnızca foo fonksiyonunu çağırmış olsak bile foo fonksiyonun içinde bulunduğu statik kütüphanede 100 tane fonksiyon varsa bu 
    100 fonksiyonun hepsi çalıştırılabilen dosyaya yazılacaktır.

    2) Çalıştırılabilen dosya kütüphaneden çekilen object modülleri de içerir. Böylece bu programın çaçıştırılması sırasında artık statik kütüphanelere 
    gereksinim duyulmayacaktır. 

    Statik kütüphanelerin en önemli dezavantajı her çalıştırılabilen dosyanın kullanılan kütüphane fonksiyonlarının kodlarını barındırmasıdır. Bu da çalıştırılabilen
    dosyaların diskte fazlaca yer kaplaması anlamına gelir. Örneğibn pek çok C programı printf fonksiyonunu kullanmaktadırç. O zaman o programlarının hepsinin
    içerisinde printf fonksiyonun makine kodları bulunur.

    Dinamik kütüpaheneler içerisinden bir fonksiyon çağrıldığında linker fonksiyonun kodunu dinamik kütüphaneden alarak çalıştırılabilen dosyaya yazmaz. 
    Bunun yerine linker çalıştırılabilen dosyaya işletim sistemi için "ilgili programın hangi danamik kütüphaneleri onların içerisindeki hangi fonksiyonları
    kullandığı bilgisini" yazar. Böylece çalıştırılabilen dosyalar bu fonksiyonların kodlarını içermezler. İşletim sisteminin yükleyicisi (loader) çalıştırılabilen 
    dosyayı belleğe yüklerken o dosyanın kullanmış olduğu dinamik kütüphaneleri de belleğe yüklemektedir. Böylece program çalışırken akış belleğe yüklenmiş olan
    dinamik kütüphane içerisine geçerek oradaki kodları çalıştırır. Bu sistemde dinamik kütüphanenin bir bölümü değil hepsi belleğe yüklenmektedir. (Yani
    örneğin biz dinamik kütüphaneden tek bir fonksiyon çağırmış olsak bile onun tamamı belleğe yüklenir.) Dinamik kütüphaneler programın birer parçası kabul edilmektedir. 
    Dolayısıyla program başka bir makineye konuşlandırılırken yalnızca çalıştırılabilen dosya değil o dosyanın kullandığı dinamik kütüphane 
    dosyaları da o sisteme taşınmak zorundadır. Dinamik kütüphane kullanan programlar biraz daha geç yüklenme eğilimindedir. Ancak işletim sistemleri 
    farklı programlar aynı dinamik kütüphaneyi kullanıyorsa mümkün olduğu kadar o dinamik kütüphaneyi tekrar tekrar belleğe yüklemezler. Bu kınun bazı detayları vardır.

    Bugün Microsoft C derleyicileri ve gcc derleyicileri ve bunların linker programları default durumda standart C fonksiyonlarını dinamik kütüphanelerden almaktadır.
    Tüm standart C fonksiyonları genellikle tek bir dinamik kütüphanede toplanmıştır. Ancak bu durum değişebilmektedir. Tabii bu sistemler aynı standart C fonksiyonlarını
    statik kütüphanelere de yerleştirmişlerdir. Programcı isterse default durumu değiştirerek standarty C fonksiyonlaırnın statik kütüphanelerden alınmasını sağlayabilir. 

    Microsoft derleycilerinde standart C fonksiyonlarının statik kütüphanelerden alınmasını sağlamak için komut satırında /MT ya da /MTd komut satırı argümanları 
    kullanılır. IDE'debn bu ayar Proje seçenlerinden C-C++/Code Generation/Runtime Library combobox'ından ayarlanmaktadır. gcc sisteminde -static linker seçeneği
    derleme link işlemine eklenmelidir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Biz kendimizin tanımalamadağı yani kaynak dosyasımızda olmayan bir fonksiyonu çağırmış olalım. Örneğin:

    #include <stdio.h>

    void xxxxx(void);

    int main()
    {
        xxxxx();

        return 0;
    }

    Burada hata hangi aşamada ortaya çıkacaktır? İşte kaynak kodda olmayan bir fonksiyon çağrıldığında eğer derleyici fonksiyonu kaynak kodda bulamazsa 
    derlemeyi başarıyla sonuçlandırır. Ancak objecet modüle linker için bir not yazar. Bu notta adeta şöyle demektedir: "Sevgili linker ben xxxxx isimli 
    bir fonksiyonun çağrıldığını gördüm. Ancak onu kaynak kodda bulamadım. Onu sen diğer object modüllerde ve kütüphane dosyalarında ara ve bulmaya çalış.
    Bulmazasan yapacak bir şey yok". İşte linker bu notu okuyarak fonksiyonu arar ve onu bulursa sorun çıkmaz. Ancak bulamazsa link aşamasında error 
    oluşur. O halde olmayan bir fonksiyon çağrıldığında hata derleme aşamasında değil link aşamasında linker'ın bu fonksiyonu bulamaması biçiminde 
    ortaya çıkmaktadır. 

    Aslında standart C fonksiyonlarının standart C fonksiyonu olduğunu ne derleyici ne linker bilmektedir. Örneğin derleyici printf fonksiyonunu 
    gördüğünde onu kaynak kodda bulamadığı için object modüle linker için benzer notu yazar. Linker de printf fonksiyonunu tanımamaktadır. Ancak onu kütüphanelerde 
    ararken bulur. Halbuki xxxxx fonksiyonu bulamayacktır. O halde standart C fonksiyonlarının standart C fonksiyonları olduğu yalnızca programcılar tarafından 
    bilinmektedir. Tabii biz derleyicileri install ederken bu standart C fonksiyonları statik ya da dinamik kütüphane dosyalarına yerleştirilmiş durumda olur.
    En azından bu garanti edilmiştir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aslında C derleyicisi kendi içerisinde iki modülden oluşmaktadır: Önişlemci (Preprocessor) ve Derleme (Compile) Modülleri:

    .c ----> Önişlemci Modülü -----> Derleme Modülü -----> Object Dosya

    Kaynak kod önişlemci modülü tarafından alınır. Önişlemci kaynak kod üzerinde çeşitli düzenlemeleri yapar ve kodu derleme modülüne verir. Derleme işleminin
    bütün faaliyetleri derleme modülü tarafından yapılmaktadır. Yani C derleyicisi dediğimiz şey aslında bu derleme modülüdür. Ancak önişlemci de derleyicinin 
    bir parçasıdır. 

    C'de  # ile başlayan satırlar önişlemciye ilişkindir. Yani önişlemci #'li satırlarla uğraşmaktadır. 

    #'den sonra ismine "önişlemci komutu" denilen bir anahtar sözcük gelir. Önişlemci komutu önişlemciye ne yapması gerektiğini belirtmektedir. Pek çok 
    önişlemci komutu vardır. Ancak bunların arasında "include" ve "define" önişlemci komutları en çok kullanılanlardır. Biz de kursumuzun bu bölümünde 
    bu iki komutu inceleyeceğiz. Diğer önişlemci komutlarını kursumuzun son bölümlerinde ele alacağız. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                            28. Ders 13/09/2022 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    En çok kullanılan önişlemci komutlarından biri #define komutudur. Bu komutun genel biçimi şöyledir:

    #define STR1	STR2

    Burada #define komutundan sonra boşluk karakterleri atılıp ilk boşlukuz yazı mümesi elde edilir. Buna STR1 diyelim. Sonra yeniden boşluk karakterleri atılıp
    satır sonuna kadar tüm karakterler elde edilir. Buna da STR2 diyelim. Önişlemci kaynak kodda STR1 gördüğü yerlere STR2 yazısını yerleştirmektedir. Örneğin:

    #define MAX_VAL		(10 + 20)

    Burada STR1 "MAX_VAL" yazısını STR2 ise "(10 + 20)" yazısını temsil eder. İşte önişlemci kaynak kodda "MAX_VAL" gördüğü yere "(10 + 20)" yerleştirecektir.  
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define MAX_VAL		(10 + 20)

int main(void)
{
    int a;

    a = MAX_VAL * 2;		/* derleme modülü burada a = (10 + 20) * 2 yazısını görecek */

    printf("%d\n", a);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Önişlemcinin derleme yapmadığına yalnızca kaynak kod üzerinde yazısal düzenlemeler yaptığına dikkat ediniz. Önişlemciden geçirilmiş kod derleme modülüne 
    geldiğinde artık # ile başlayan satırlar koddan silinmiş olacaktır. Yani kaynak kod önişlemciden geçtikten sonra artık #'li satırlardan arındırılmış 
    durumda olur. 

    #define komutunun hesap yapmadığına yalnızca yer değiştirme yaptığına dikkat ediniz. Bu nedenle aşağıdaki kodda ekranda 50 yazısını göreceksiniz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define MAX_VAL		10 + 20

int main(void)
{
    int a;

    a = MAX_VAL * 2;		/* a = 10 + 20 * 2 */

    printf("%d\n", a);		/* 50 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir program içerisindeki birtakım sabitler sayı olarak değil #define komutu ile yazı biçiminde ifade edilirse kodu inceleyen kişi onu daha iyi anlamlandırır. 
    Bu nedenle C programcıları birtakım sayıları programda böyle yazısal biçimde ifade ederler. İşte #define komutu ile bir yazıya bir sayı karşılık getirilmesi 
    durumunda yazıya "sembolik sabit (symbolic constant)" denilmektedir. Örneğin:

    #define MAX_SIZE			100
    #define LINE_LENGTH			1024
    #define NITEMS				12
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Önişlemciler tipik olarak geçici bir dosya açarak #'li satırlar üzerindeki düzenlemeyi bu geçici dosyada yaparlar. Sonra derleme modülüne önişlemden 
    geçirilmiş bu geçici dosyayı verirler. Derleme işleminden sonra da bu geçici dosyayı silerler. Bu nedenle biz bu geçici dosyayı görmeyiz. Ancak derleyicilerin 
    çoğunda önişlemcinin yarattığı dosyayı görebilmemin yolları da vardır. Microsost'un C derleyicisinde /P seçeneği gcc ve clang derleyicilerinde -E 
    seçeneği bu amaçla kullanılabilir. Tabii Visual Studio IDE'sinde aynı işlem görsel olarak proje seçenekerlinde C-C++/Preprocessor/Preprocessor to File 
    seçeneği ile de yapılabilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Sembolik sabitlerin derleme modülü için bir sabit statüsünde olduğuna dikkat ediniz. C'de bazı durumlarda sabit ifadelerinin zorunlu olduğunu anımsayınız. 
    Örneğin:

    #include <stdio.h>

    #define CMD_DEL		1
    #define CMD_DIR		2
    #define CMD_COPY	3
    ...

    switch (a) {
        case CMD_DEL:
            break;
        case CMD_DIR:
            break;
        case CMD_COPY:
            break;
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Önişlemci genel olarak C'yi bilmemektedir. Dolısıyla önişlemci komutlarının bir faaliyet alanı yoktur. Örneğin #define komutu kaynak kodun herhangi 
    bir yerinde yazılabilir. Bir fonksiyonun içinde yazılması ile dışında yazılması arasında farklılık yoktur. Neerede yazılmışsa oradan kaynak kodun sonuna kadarki
    bölgede etki göstermektedir. 

    #define önişlemci komutları için en iyi yer programın tepesi ya da bir başlık dosyasının içidir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/
    #define komutunda komutun STR1 kısmı değişken ya da anahtar sözcük olabilir. Sabit, ayıraç ya da operatör olamaz. Örneğin aşağıdaki komutlar geçersizdir:

    #define +		-
    #define 100		200
    #define ;		+

    Ancak komutun STR2 kısmı herhangi bir yazı olabilir. Aşağıdaki komutlar geçerlidir:

    #define TERMINATOR		;
    #define ADD				+


    Aşağıdaki örnekte program önişlemciden geçtikten sonra C'ce anlamı duruma gelecektir. 
/*----------------------------------------------------------------------------------------------------------------------

#include <stdio.h>

#define tam			int
#define ana			main
#define bos			void
#define	eger		if
#define degilse		else
#define don			return
#define yazf		printf

tam ana(bos)
{
    tam i = 0;

    eger (i > 0)
        yazf("pozitif\n");
    degilse
        yazf("negatif ya da sifir\n");

    don 0;
}

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    #define komutunda genel olarak STR1 yazısına "makro (macro)" da denilmektedir. Örneğin:

    #define MAX		10

    Burada MAX için "sembolik sabit" de diyebiliriz "makro" da diyebiliriz. Makro daha genel bir isimdir.  
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Önişlemci #'li satırlar üzerinde değişiklik yapmaz. Ancak önişlemci açtığı bir maroyu yeniden önişleme sokmaktadır. Ta ki artık önişleme soktuğunda 
    değiştirilecek bir yazı kalmayana kadar. Örneğin:

    #define MAX		100
    #define MIN		(MAX - 50)

    ...

    x = MIN;

    Burada önişlemci MIN için önce aşağıdaki gibi bir açım yapar:

    (MAX - 50)

    Açtığı kodu yeniden önişleme sokar:

    (100 - 50)

    yazısını elde eder. Artık değiştirilecek bir şey kalmadığı için işlemi bitirir. Yukarıdaki #define komutlarını ters sırada yazsaydık da bir şey değişmeeyecekti:

    #define MIN		(MAX - 50)
    #define MAX		100
    
    ...

    x = MIN;

    Burada yine önce MIN için şıu açımı yapar:

    (MAX - 50)

    Sonra açtığı makroyu yeniden önişleme sokar:

    (100 - 50)
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Önişlemci iki tırnak içerisindeki yazılar üzerinde değişiklik yapmaz. Örneğin:

    #include <stdio.h>

    #define MAX		100

    int main(void)
    {
        printf("MAX");		/* MAX */

        return 0;
    }

    Burada MAX iki tırnak içerisind eolduğu için önişlemci bu yazı üzerinde değişiklik yapmamıştır. Dolayısıyla ekrana MAX yazısı çıkacaktır. Tabii 
    biz bir yazıyı iki tırnaklı bir ifadeyle yer değiştirebiliriz. Örneğin:

    #include <stdio.h>

    #define MSG		"Success\n"

    int main(void)
    {
        printf("MSG");		/* MSG */
        printf(MSG);		/* Success */

        return 0;
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'nin standart başlık dosyalarının içerisinde fonksiyon prototiplerinin yanı sıra #define ile oluşturulmuş çeşitli sembolik sabitler de bulunmaktadır. 
    Dolayısıyla biz bu başlık dosyalarını include ettiğimizde artık bu semboik sabitleri kullanabiliriz. Örneğin <stdio.h> içerisinde EOF isimli, BUFSIZ
    ve çeşitli başka isimlerle sembolikler define edilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    a = EOF;				/* EOF <stdio.h> içerisinde define edilmiş */
    printf("%d\n", a);

    a = BUFSIZ;				/* BUFSIZ <stdio.h> içerisinde define edilmiş */
    printf("%d\n", a);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    #define komutunda komutun STR2 kısmı hiç olmayabilir. Örneğin:

    #define TEST

    Bu durumda kaynak kodda STR1 görülen yer eboşluk atanır dolayısıyla STR1 yazıları silinmiş olur. Genel olarak STR2 kısmı olmayan #define komutları
    bazen kodu inceleyen kişiler için ipucu vermek amacıyla bazen de diğer önişlemci komutları için kullanılmaktadır. 

    Aşağıdaki örnekte programın çalışmasında herhangi bir bozukluk olmayacaktır. Çünkü IN yazıları kod derleme modülüne verildiğinde silinmiş olacaktır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define IN

void foo(IN int a)
{
    printf("%d\n", a);
}

int main(void)
{
    IN IN
        IN

        IN
    foo(10);

    IN
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Makrolar parametreli olabilmektedir. Bunlara "parametreli makolar" ya da "fonksiyon gibi makrolar (function-like macros)" denilmektedir. Bir makroda
    #define komutunun STR1 kısmında bir parantez açılırsa parantezin içerisindeki ',' ile ayrılmış isimlere "makro parametreleri" denilmektedir. Örneğin:

    #define SQUARE(a)	...

    Burada a makro parametreisidir. Örneğin:

    #define MAX(a, b)	...

    Burada a ve b makro parametreleridir. 

    Parametreli makrolar bir fonksiyon çağrısı gibi işleme sokulurlar. Zaten bu nedenle onlara C standartlarında "function-like macro" denilmektedir. 
    Bir makro çağrıldığında önişlemci parametreleri yerleştirerek makroyu açar. Örneğin:

    #define square(a)		a * a
    ...

    result = square(10);

    Burada 10 a parametresine karşı gelmektedir. O halde önişlemci kodu aşağıdkai gibi açacaktır:

    result = 10 * 10;

    Parametreleri makroların tamamen bir fonksiyon gibi kullanılması gerekir. Halbuki yukarıdaki square makrosu açıldığında tam bir fonksiyon etkisi yaratamaz. Örneğin:

    result = square(10 - 2);

    Eğer square bir fonksiyon olsaydı önce argümanın değeri hesaplanacaktı ve biz 8'in karesini elde edecektik. Ancak square gerçekte bir makrodur. Önişlemci kodu şöyle
    açacaktır:

    result = 10 - 2 * 10 - 2;

    Bu açılmış hal derleme modülüne geldiğinde çarpma işleminin önceliği olduğu için istenileni yapamayacaktır. O halde fonksiyon gibi makro yazabilmek için
    makro parametrelerinin paranzteze alınması gerekir:

    #define square(a)		(a) * (a)
    ...
    result = square(10 - 2);

    Artık önişlemci makroyu şöyle açacaktır:

    result = (10 - 2) * (10 - 2);

    Görüldüğü gibi şimdi makro fonksiyon gibi davranır hale gelmiştir. Ancak makro parametrelerinin pazarnteze alınması da yetmemektedir. Örneğin:

    result = !square(1 - 1);

    Eğer square bir fonksiyon olsaydı buradan 1 elde edilirdi. Ancak square yukarıdaki gibi makro olursa ! operatörünün önceliğinde dolayı farklı bir değer elde edilecektir. 

    #define square(a)		(a) * (a)
    ...
    result = !square(1 - 1);

    Bu durumda açım şöyle yapılacaktır:

    result = !(1 - 1) * (1 - 1)

    Burada 0 elde edilecektir. O halde parametreli makro yazılırken makro ayrıca en dıştan da paranteze alınmalıdır:

    #define square(a)		((a) * (a))
    ...
    result = !square(1 - 1)

    Artık açım şöyle yapılacaktır:

    result = !((1 - 1) * (1 - 1))

    Bu durumda foksiyon gibi makro yazabilmek için iki kuralı uygulamak gerekir:

    1) Komutun STR2 kısmında makro parametreleri paranteze alınmalıdır.
    2) Komutun STR2 kısmında makro en dıştan paranteze alınmalıdır. 

    Örneğin:

    #define square(a)		((a) * (a))

    Parametreli bir makro kodu izleyen tarafından tam bir fonksiyon taklidi yapabilmelidir. Parametreli makro bir fonksiyon değildir. Bir yer değiştirme 
    işlemini yapmakatradır. Ancak yer değiştirilen kod bir fonksiyon gibi etki göstermektedir. Önişlemci parametreli makro çağrıldığında argümanı makro
    parametreleriyle eşler ve açımı ona göre yapar. Örneğin:

    #define average(a, b, c)		(((a) + (b) + (c)) / 3.0)

    Biz bu makroyu şöyle işleme sokalım:

    result = average(1 + 2, 3 + 4, 5 + 6);

    Şimdi burada "1 + 2" a parametresi ile, "3 + 4" b parametresi ile ve "5 + 6" c parametresi ile eşleşecektir. Bu durumda önişlemci şöyle açım uygulaacaktır:

    result = (((1 + 2) + (3 + 4) + (5 + 6)) / 3.0);

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define square(a)		((a) * (a))

int main(void)
{
    int result;

    result = square(10 - 2);		/* result = ((10 - 2) * (10 - 2)); */
    printf("%d\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                            29. Ders 15/09/2022 Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi biz neden fonksiyon yazmıyoruz da fonksiyon etkisi yaratacak parametreli makro yazmaya çalışıyoruz? İşte fonksiyon çağırma işlemi bazı makine komutları 
    kullanılarak yapılmaktadır. Oysa makronun enjekte edilmesi fonksiyon çağırma işlemi anlamına gelmediği için fonksiyonun çağrılması sırasındaki 
    makine komutları elimine edilmş olur. Bir fonksiyon çağrıldığında çağrı koda eklenen makine komutlarının yarattığı dezavantaja İngilizce "function call overhead"
    denilmektedir. Örneğin aşağıdaki gibi bir fonksiyon olsun:

    int square(int a)
    {
        return a * a;
    }

    Biz bu fonksiyonu şöyle çağırmış olalım:

    result = square(val);

    32 bit Intel işlemcilerinde burada üretileck makine komutları şunlardır:

    square:
        push ebp
        mov	ebp, esp

        mov eax, [ebp + 8]
        imul eax, [ebp +8]

        pop ebp
        mov esp, ebp

    ...
    push val
    call square
    add esp, 4
    move result, eax
    
    Aslında burada yalnızca iki makine komutu bu çarpma işlemini yapmaktadır:

    mov eax, [ebp + 8]
    imul eax

    Diğer komutlar fonksiyon çağırma nedeniyle mecburen koda eklenen komutlardır. İşte bir iki satırlık fonksiyonların fonksiyon olarak değil de parametreli
    makro biçiminde yazılması fonksiyon çağırma sırasında gereken makine komutlarının elimine edilmesine yol açar. Yani makro fonksiyon çağrısına göre
    daha hızlı bir çalışmayı sağlar. 

    Uzun fonksiyonların makro olarak yazılması kötü bir tekniktir. Çünkü:

    1) Uzun bir fonksiyonda birkaç makine komutunun elimine edilmesinin pratik bir faydası olmayabilir. 
    2) Uzun makroların yazılması zordur ve okunabilirliği azaltmaktadır. 
    3) Uzun makrolar her çağrılan yere enjekte edileceği için kodu büyütürler. Elde edilen hıza kodda yaşanan büyüme kar-zarar ilişkisi dikkate alındığında 
    toplamda zarar oluşturmaktadır.
    

    Makroları çağırırken dikkat etmek gerekir. Makro argümanlarında ++ ve -- gibi operatörler "tanımsız davranış (undefined behavior)" oluşturabilirler. Örneğin:

    #define square(a)			((a) * (a))

    Bu makroyu şöyle çağırmış olalım:

    result = square(++val);

    Burada eğer square bir makro yerine fonksiyon olsaydı val önce artırılır, artırılmış değerin karesi alınırdı. Halbuki square bir makro olduüunda 
    önişlemci şöyle bir açım uygulayacaktır:

    result = ((++val) * (++val))

    Bu da "tanımsız davranış" oluşturacaktır.

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aslında küçük bazı standart C fonksiyonları derleyicileri yazanlar tarafından birer fonksiyon olarak değil de makro olarak yazılabilmektedir. Örneğin 
    <ctype.h> içerisindeki karakter test fonksiyonları pek çok derleyici tarafından makro olarak yazılmaktadır. Tabii programcı bunu bilmek zorunda değildir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir C programının yazıldığı editör dar ise biz uzun atomları nasıl yazabiliriz? Örneğin string'lerin tek bir satırda yazılması gerekir. Benzer biçimde 
    #define komutunun da tek bir satırda yazılması gerekir. Pekiyi ya editörümüzün genişliği yeterli değilse? Ya da tek satıra yazmak okunabilirliği bozuyorsa? 
    İşte C'de üst satır ile alt satırı sanki tek bir satırmış gibi derleyiciye göstermenin bir yolu vardır: Eğer bir satırda \ karakterinden hemen sonra LF (Line Feed)
    karakteri gelirse (yani \ karakterinden hemen sonra ENTER tuşuna basılırsa) önişlemci tarafından işin başında bu iki satır \ ve LF karakterleri silinerek sanki tek 
    satır haline dönüştürülür. Böylece biz istersek bir satırı kesip aşağıdan bu yöntemle devam edebiliriz. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        printf("Hello\
     World\n");

        return 0;
    }

    Ya da örneğin:

    #include <stdio.h>

    int main(void)
    {
        int number_of_students;

        number_of_\
    students = 10;

        printf("%d\n", number_of_students);

        return 0;
    }

    Bu sayede bir satırdan büyük olan makrolar daha okunabilir biçimde yazılabilirler. C standratlarına göre \ birleştirmesi yapıldıktan sonra derleyicilerin 
    en az 4095 karakterlik satırları desteklemesi gerekmektedir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

#define error_check(result)			\
{									\
    if (!result) {					\
        printf("Error!\n");			\
        exit(1);					\
    }								\
}									

int main(void)
{
    int test = 0;

    error_check(0);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Çok satırlı makro yazarken programcı makroyu blok içerisine alır. Ancak blok içerisine alma yine de makronun tam olarak fonksiyon taklidi yapmasına
    olanak sağlamaz. Örneğin:

    #include <stdio.h>
    #include <stdlib.h>

    #define error_check(result)			\
    {									\
        if (!result) {					\
            printf("Error!\n");			\
            exit(1);					\
        }								\
    }									

    int main(void)
    {
        int val = 10;
        int status = 0;

        if (val > 0) 
            error_check(status);				/* compile time error */
        else
            printf("Everything is ok\n");

        return 0;
    }

    Buradaki kodu önişlemci aşağıdaki gibi açacaktır:

    if (val > 0) 
        if (!status) {
            printf("Error!\n");
            exit(1);
        };
    else
        printf("Everything is ok\n");

    Burada error_check(status) ifadesinin sonundaki noktalı virgül başımıza bela açmaktadır. Çünkü önişlemci kodu açtığında artık açılan kodda bu ';' 
    boş deyim olarak elşe alınacak ve bloklama yapılmadığı için sentaks hatası oluşacaktır. Tabii biz dıştaki if deyimini bloklarsak sorun çözülür ancak
    makromuzun da tam bir fonksiyon taklisi yapamadığı açıktır. İşte bu tür durumlarda do-while deyimi imdadımıza yetişmektedir. Yukarıdaki makroyu şöyle ayzmış olalım:

    #define error_check(result)			\
    do  {								\
        if (!result) {					\
            printf("Error!\n");			\
            exit(1);					\
        }								\
    } while (0)

    Burada while parantezinin sonuna ';' yerleştirmediğimize dikkat ediniz. Bu while döngüsü aslında hiç dönmeyecektir. while döngüsünün sonundaki ';'
    bir boş deyim değildir. Olması gereken bir atomdur. O halde makroyu aşağıdaki gibi kullanan kişi gerçekten de koyduğu ';' ile sentaksı tamamlar. Böylece de 
    do-while tek deyim olarak ele alınır:

    if (val > 0)
        error_check(status);			/* Burada ';' artık boş deyim olmayacak, do-while deyimini tamamlayan ';' haline gelecek		
    else
        printf("Everything is ok\n");

    Önişmeci makroyu açtığında şu durum olşacaktır:

    if (val > 0)
        do {
            if (!status) {
                printf("Error!\n");
                exit(1);
            }
        } while (0);
    else
        printf("Everything is ok\n");

    Artık if deyiminin doğru kısmında tek bir deyim vardır. 

    Bu nedenle çok satırlı makroların bu biçimde do-while tekniği ile yazıldığını görürseniz şaşırmayınız. 

    Tabii çok satırlı makrolar if gibi deyimlerin içerisine yerleştirilemezler. Çok satırları makrolar geri dönüş değeri void olan fonksiyonlar gibi düşünülmeliedir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

#define error_check(result)			\
do  {								\
    if (!result) {					\
        printf("Error!\n");			\
        exit(1);					\
    }								\
} while (0)

int main(void)
{
    int val = 10;
    int status = 0;

    if (val > 0)
        error_check(status);
    else
        printf("Everything is ok\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii makrlar başka amaçlarla da kullanılabilir. Biz her ne kadar henüz dizileri görmesek de aşağıdaki örnekte tüm elemanları 1 olan bir diziyi 
    makrolar yardımıyla kolay bir biçimde oluşturabilmekteyiz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define FILL10(val)	val, val, val, val, val, val, val, val, val, val
#define FILL100(val) FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val)
#define FILL1000(val) FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val)

int main(void)
{
    int a[1000] = {FILL1000(1)};

    for (int i = 0; i < 1000; ++i)
        printf("%d ", a[i]);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte C'ye de "inline fonksiyonlar" eklenmiştir. Inline fonksiyonlar fonksiyon gibi makroların güvenli bir alternatifidir. Kursumuzda bu konu
    ileride ele alınacaktır. Bu nedenle fonksiyona benzer makroların #define ilke değil inline fonksiyonlar yoluyla yazılması C99 ve sonrasında daha uygun 
    olabilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    En çok kullanılan diğer bir önişlemci komutu da #include komutudur. #include komutunu açısal parantezler içerisinde ya da iki tırnak içerisinde 
    bir dosya ismi izler. Yani komutun genel biçimi şöyledir:

    #include <dosya_ismi>
    #include "dosya_ismi"

    Biz şimdiye kadar hep include işleminde açısal parantezleri kullandık. Önişlemci #include komutunu gördüğünde belirtilen dosyayı açar. Geçici dosya yoluyla
    dosyanın içerisiğini komutun yerleştirildiği yere yapıştırır. Böylece artık kod derleme modülüne geldiğinde derleme modülü #include komutunu değil o dosyanın içeriğini 
    görecektir. 

    include edilecek dosyanın uzantısı ".h" olmak zorunda dğeildir. Herhangi bir dosya da örneğin bir .c dosyası da include edilebilir. 

    Aşağıdaki örnekte "sample.c" dosyası "test.c" dosyasını include etmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

/* sample.c */

#include <stdio.h>
#include "test.c"

int main(void)
{
    foo();

    return 0;
}

/* test.c */

void foo(void)
{
    printf("foo\n");
}

/*----------------------------------------------------------------------------------------------------------------------
    #include komutu kaynak kodun herhangi bir yerine yerleştirilebilir. Tabii yerleştirme yerine göre yerleştirilen dosya içeriğinin anlamlı olması 
    gerekir. #include komutu da tek bir satıra yazılmak zorundadır. 

    Aşağıdaki örnekte #include komutu yerel bir blokta bulundurulmuştur. İçerik itibari ile bulundurulanyer geçerli bir kod oluşturur. 
----------------------------------------------------------------------------------------------------------------------*/

/* sample.c */

#include <stdio.h>

int main(void)
{
    int a =
#include "test.c"
        ;

    printf("%d\n", a);

    return 0;
}

/* test.c */

10

/*----------------------------------------------------------------------------------------------------------------------
    #include komutunda önişlemci include edilen dosyayı komutun bulunduğu yere yapıştırktan sonra yeniden önişlem işlemlerini açtığı üzerinde de yapar. 
    Böylece biz include dosyalarına önişlemci komutlarını yerleştirebiliriz. Örneğin include ettiğimiz dosyaların içerisinde #define önişlemci komutları da 
    olabilir. Bu durumda bu komutlar da etki gösterecektir. 
----------------------------------------------------------------------------------------------------------------------*/

/* sample.c */

#include <stdio.h>
#include "test.h"

int main(void)
{
    for (int i = 0; i < SIZE; ++i)
        printf("%d\n", i);

    printf("%d\n", square(10));

    return 0;
}

/* test.h */

#define SIZE	10
#define square(a)	((a) * (a))

/*----------------------------------------------------------------------------------------------------------------------
    include edilen dosyada başka include komutları da buılunabilir. Bu durumda yukarıda da belirtildiği gibi özyinelemeli bir biçimde include işlemi 
    uygulanır. 
----------------------------------------------------------------------------------------------------------------------*/

/* sample.c */

#include "project.h"

int main(void)
{
    printf("%f\n", sqrt(10));

    return 0;
}

/* project.h */

#include <stdio.h>
#include <math.h>

/*----------------------------------------------------------------------------------------------------------------------
    C'nin standart başlık dosyalarında "include koruması (include guard)" uygulanmıştır. Bu nedenle bu standart başlık dosyalarının doğrudan ya da dolaylı 
    olarak birden fazla kez include edilmiş olması bir soruna yol açmaz. include koruması ileride ele alınacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdio.h>		/* soruna yol açmaz, ama gereksiz */
#include <stdio.h>		/* soruna yol açmaz, ama gereksiz */

int main(void)
{
    printf("ok\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    include işleminde "döngüsel (cyclic) durumlar" error oluşturmaktadır. Örneğin biz "a.h" dosyasını include etmiş olalım. Bu dosya da "b.h" dosyasını include 
    etmiş olsun. "b.h" dosyası da "a.h" dosyasını include etmiş olsun. Bu durum döngüsellik oluşturmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/


/*----------------------------------------------------------------------------------------------------------------------
                                                            30. Ders 20/09/2022 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    include işleminin açısal parantezlerle yapılması ile iki tırnak içerisinde yapılması arasında farklılık vardır. Ancak bu farklılık C standartlarında 
    açık bir biçimde belirtilmemiş daha çok "derleyicileri yazanların isteğine (implementation defined)" bırakılmıştır. 
    
    Standartlarda bu konuda şunlar söylenmiştir: 
    
    - Eğer include işlemi açısal parantezlerle yapılmışsa önişlemci dosyayı kendisinin belirlediği bazı diziblerde arar. Bu dizinlerin nasıl belirleneceği
    derleyicileri yazanların isteğine bırakışmıştır. 

    - Eğer dosya ismi iki tırnak içerisinde belirtilmişse bu durumda önişlemci dosyayı kendisinin belirlediği bir biçimde ve dizinlerde arar. Ancak dosya
    bu aramada bulunamazsa sanki include işlemi açlsal parantezlerle yapılmış gibi bu kez dosya açısal parantezlerle belirtildiğinde aranan dizinlerde de aranır. 

    Her ne kadar standartlar belirlemeleri oldukça gevşek bırakmışsa da uygulamada pek çok derleyici şu biçimde işlem yapmaktadır:

    - Eğer dosya açısal parantezlerle include edilmişse dosya C'nin standart başlık dosyalarının yüklendiği dizinde aranmaktadır. Programcılar genellikle C'nin standart
    başlık dosyalarını bu biçimde include ederler. 

    - Eğer dosya iki tırnak içerisinde include edilmişse bu durumda yaygın derleyiciler dosyayı önce "o anda bulunulan dizinde (current working directory)" aramaktadır.
    Eğer dosya o anda bulunulan dizinde bulunamazsa bu kez dosya standart C başlık dosyalarının bulunduğu dizinde de aranmaktadır. 

    Bu durumda en yaygın kullanım programcının standart başlık dosyalarını açısal parantezlerle kendi başlık dosyalarını iki tırnak ile include etmesidir. Örneğin:

    #include <stdio.h>
    #include "project.h"

    İki tırnak ile include işlemi yapıldığında eğer Visual Studio gibi IDE'lerde çalışıyorsanız. Bu durumda "içinde bulunulan dizin (current working directory)"
    proje dizini olacaktır. Ancak komut satırından derleme işlemini yapıyorsanız içinde bulunulan dizinde promptta gördüğünüz dizin olacaktır. 

    C'nin standart başlık dosyalarının iki tırnak ile include edilmesinde bir sorun oluşmayacağına dikkat ediniz. Ancak kendi başlık dosyalarınızı açısal parantezlerle 
    include ederseniz muhtemelen önişlemci dosyayı bulamayacaktır. 

    C derleyicisi kurulurken (örneğin Visual Studio IDE'si kurulurken) başlık dosyalarının hangi dizine yerleştirileceği derleyiciden derleyiciye hatta aynı derleyicilerde 
    versyiondan versiyona değişebilmektedir. UNIX/Linux sistemleri geleneksel olarka standart başlık dosyalarını /usr/include dizinin içerisinde bulundurmaktadır. 
    Microsoft Visual Studio IDE'sinin kurulumu sırasında kurulum dizinini bize de sorabilmektedir. Ancak Microsoft versiyondan versiyona strateji değiştirebilmektedir. 

    Aslında açısal parantez ile include işlemi yapıldığında önişlemcinin arama dizinlerine ekler yapılabilmektedir. Microsft Visual Studio IDE'sinde
    Proje seçeneklerinde "C-C++/General/Additional Include Directories" sekmesinde dizinler aralarında ';' konularak girilebilmektedir. Bu durumda 
    önişlemci açısal parantazlerle include işlemi yapıldığında burada girilen dizinlere de bakmaktadır. Hatta projeden bağımsız olarak bu dizinlere kalıcı eklemeler de 
    yapılabilmektedir. Microsoft cl.exe kmut satırı derleyicisinde gcc ve clang derleyicilerinde -I seçeneği de bu amaçla kullanılabilmekltedir. Örneğin:

    gcc -o sample -I /home/kaan/Study/C  sample.c

    Birden fazla dizine bakılması için birden fazla kez -I seçeneği kullanmak gerekir. Örneğin:

    gcc -o sample -I /home/kaan/Study/C  -I /home/kaan/personal sample.c

    Ayrıca derleyiciler bu dizinleri belirlemek için bazı çevre değişkenlerinden de faydalanabilmektedir. Örneğin gcc derleyicilerinde CPATH çevre değişkenine
    derleyici önişlem aşamasında başvurmaktadır.

    #include komutunda dosya isimlerinin yol ifadesi içerip içermeyeceği de derleyicileri yazanların isteğine bırakılmıştır. Genel olarak dosya isimlerinde yol ifadesi 
    kullanmayınız. Derleyicilerin çoğu en azından göreli yol ifadelerine izin vermektedir. 

    Standart C başlık dosyalarının include edilme sırasının hiçbir önemi yoktur. Genellikle programcılar çok kullanılan başlık dosyalarını daha daha yukarıda 
    include etme eğilimindedirler. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir derleyicinin standartlara uygunluğu "standartlar auygun programı başarılı bir biçimde derlemesi ile" ölçülmektedir. Daha önceden de belirtildiği gibi 
    standartlara uygun olmayab hatalı kodların derleyiciler tarafından derlenip derlenmemesi derleyicilerin bir tercihidir. İşte C derleyicileri standartlarda
    olmayan ek birtakım özelliklere de sahip olabilmektedir. Bunlara "eklenti (extension)" denilmektedir. Örneğin bir C derleyicisi standartlarda olmayan ekstra 
    deyimlere, ekstra türlere, ekstra fonksiyonlara sahip olabilir. Burada önemli olan standartlara uygun programların başarılı bir biçimde derlenip 
    derlenmediğidir. Yani standartlar aslında asgariyi belirtmektedir. Tabii her derleyicinin kendine özgü eklentileri bulunabilmektedir. Bu durumda spesifik bir 
    derleyicinin eklentilerini kodumuzda kullanırsak bu kod başka derleyicilerde derlenmeyebilir. Bazı eklentiler oldukça yaygındır. Hatta pek çok programcı bunu
    standart bir özellik sanmaktadır. 

    Örneğin Linux çekirdeğinin kaynak kodlarında çok sayıda gcc eklentisi kullanılmıştır. Bu durumda biz Linux kaynak kodlarını örneğin Microsft derleyicilerinde 
    derleyemeyiz. Ancak gcc derleyicilerinde derleyebiliriz. Burada da görüldüğü gibi eklentilerin yoğun kullanılması kaynak kodun belli bir derleyiciye bağlı 
    olmasına yol açmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'nin üç operand'lı (ternary) tek bir operatörü vardır. Bu operatöre "koşul operatörü (conditional operator)" denilmektedir. Koşul operatörü ?: ile
    belirtilir ve gene kullanımı şöyledir:

    ifade1 ? ifade2 : ifade3

    Koşul operatörü if deyimini çağrıştıran ancak deyim olmayan bir operatördür. Her operatörde olduğu gibi koşul operatörü de bir değer üretir. 
    Koşul operatörü şöyle çalışır: Önce soru işaretinin solundaki ifade yapılır. Bu ifade sıfır dışı bir değerse (yani doğruysa) yalnızca soru işareti ve iki nokta üst üste
    arasındaki ifade yapılır. Eğer bu ifade sıfır ise (yani yanlış ise) bu durumda da yalnızca iki nokta üst üstenin sağındaki ifade yapılır. Koşul operatörünün
    çalışması if deyimine benziyor olsa da koşul bir değer üretmektedir. Programcı koşul operatörünün ürettiği değeri genellikle bir nesneye atar. 
    Koşul operatörü soru işaretinin solundaki ifade sıfır dışı bir değerdeyse soru işareti ve iki nokta üst üste arasındaki ifadenin değerini üretir, 
    soru işaretinin solundaki ifade sıfır ise iki nokta üst üstenin sağındaki ifadenin değerini üretir. 

    Örneğin:

    result = val % 2 == 0 ? 100 : 200;

    Burada val çift ise koşul operatöründen 100, tek ise 200 elde edilecektir. Bu durumda result değişkenine 100 ya da 200 atanacaktır. Yukarıdaki kodun işlevsel eşdeğeri 
    if deyimi ile de oluşturulabilir:

    if (val % 2 == 0)
        result = 100;
    else
        result = 200;

    Burada da görüldüğü gibi koşul operatörü ile yapılan her şey aslında if deyimiyle de yapılabilmektedir. Ancak koşul operatörü bazı durumlarda kompakt bir
    görünüm sunduğu için daha kısa yazımlara olanak sağlamaktadır. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int val, result;

    printf("Bir degere giriniz:");
    scanf("%d", &val);

    result = val % 2 == 0 ? 100 : 200;
    printf("%d\n", result);

    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
    Koşul operatöründe operatörün ürettiği değerin bir biçimde kullanılması gerekir. Eğer operatörün ürettiği değer kullanılmazsa her ne kadar kod geçerli olsa da
    kötü bir teknik uygulanmış olur. Örneğin:

    val % 2 == 0 ? ++x : ++y;		/* kötü teknik */

    Koşul operatörünün kullanılması gerekn üç durum vardır.

    1) Bir karşılaştırmanın sonucuna göre elde edilen değerin bir nesneye atanması gerektiği durumlar. Örneğin:

    result = val % 2 == 0 ? 100 : 200;

    Bu işlemin eşdeğer if karşılığı şöyledir:

    if (val % 2 == 0)
        result = 100;
    else
        result = 200;

    2) Fonksiyon çağırırken argüman ifadelerinde koşul operatörü kullanılabilir. Örneğin:

    foo(val % 2 == 0 ? 100 : 200);

    Bu işlemin eşdeğer if karşılığı şöyledir:

    if (val % 2 == 0)
        foo(100);
    else
        foo(200);

    3) return ifadelerinde de koşul operatörü kullanılabilir. Örneğin:

    return val % 2 == 0 ? 100 : 200;

    Bu ifadenin de eşdeğer if karşılığı şöyledir:

    if (val % 2 == 0)
        return 100;
    else 
        return 200;


    Aşağıdaki örnekte sayının çift ya da tek olduğu koşul operatörü sayesinde pratik bir biçimde ekrana yazdırılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int val, result;

    printf("Bir degere giriniz:");
    scanf("%d", &val);

    printf(val % 2 == 0 ? "cift\n" : "tek\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Örneğin 0'dan 100'e kadar sayıları beşer beşer aşağıdkai gibi yazdırmak isteylim:

    0 1 2 3 4
    5 6 7 8 9
    10 11 12 13 14
    ...

    Çözümlerden biri aşağıdaki gibi olabilir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 100; ++i) {
        printf("%d", i);
        putchar(i % 5 == 4 ? '\n' : ' ');
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında yukarıdaki kodu aşağıdaki gibi daha kompakt biçimde de yazabilirdik. Bu koddaki kritik bölüm şudur:

    printf("%d%c", i, i % 5 == 4 ? '\n' : ' ');

    Burada %d format karakteri i ile, %c format karakteri ise '\n' ya da ' ' ile eşleşmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    for (int i = 0; i < 100; ++i) 
        printf("%d%c", i, i % 5 == 4 ? '\n' : ' ');

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte max fonksiyonu iki parametresinin büyük olanına geri dönmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int max(int a, int b)
{
    return a > b ? a : b;
}

int main(void)
{
    int result;

    result = max(3, 7);			/* 7 */
    printf("%d\n", result);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Daha önceden de belirttiğimiz gibi birer satırlık fonksiyonların makro olarak yazıalması genel bir hız kazancı sağlamaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define MAX(a, b)		((a) > (b) ? (a) : (b))

int main(void)
{
    int result;

    result = MAX(3, 7);			/* result = ((3) > (7) ? (3) : (7)) */
    printf("%d\n", result);		/* 7 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Koşul operatörü öncelik tablosunda atama operatörünün hemen yukarısında sağdan sola grupta bulunmaktadır:

    ()					Soldan-Sağa
    + - ++ -- !	(tür)	Sağdan-Sola
    * / %				Soldan-Sağa
    + -					Soldan-Sağa
    <  > <= >=			Soldan-Sağa
    != ==				Soldan-Sağa
    &&					Soldan-Sağa
    ||					Soldan-Sağa
    ?:					Sağdan-Sola
    =, +=, /=, *=,...	Sağdan-Sola
    ,					Soldan-Sağa

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Koşul operatörü iç içe (nested) kullanılabilir. İç içe kullanımda parantez kullanmaya gerek yoktur. Örneğin üç sayının en büyüğünü bulmaya çalışalım:

    result = a > b ? a > c ? a : c : b > c ? b : c;

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b, c;
    int result;

    printf("a:");
    scanf("%d", &a);

    printf("b:");
    scanf("%d", &b);

    printf("c:");
    scanf("%d", &c);

    result = a > b ? a > c ? a : c : b > c ? b : c;
    printf("%d\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki gibi iç içe koşul operatöründe gerekmese bile okunabilirliği artırmak için parantez kullanılmalıdır. Örneğin:

    result = a > b ? (b > c ? b : c) : (b > c ? b : c);

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a, b, c;
    int result;

    printf("a:");
    scanf("%d", &a);

    printf("b:");
    scanf("%d", &b);

    printf("c:");
    scanf("%d", &c);

    result = a > b ? (a > c ? a : c) : (b > c ? b : c);
    printf("%d\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                31. Ders 22/09/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Koşul operatörünün öncelik tablosunda hemen atama operatörlerinin yukarısında olduğunu anımsayınız. Örneğin:

    x = a % 2 == 0 ? 100 + 200 : 300 + 400;

    Burada derleyiciye göre iki operatör vardır: Koşul operatörü ve atama operatörü. Diğer operatörler aslında koşul operatörünün operand'larını oluşturmaktadır. 
    Pekiyi neden yukarıdaki örnekte soru işaretinin solundaki her şey koşul operatörünün ilk operandını oluşturmamaktadır? İşte ayrıştırma (parsing) şöyle yapılmaktadır:
    Derleyici soru işaretinin solunda koşul operatöründen daha düşük öncelikli bir operatör görene kadar ilerler (örmeğimizde atama operatörüne kadar). 
    O kısım koşul operatörünün birinci operandını oluşturmaktadır. Soru işareti ile ':' arasındaki kısım koşul operatörünün ikinci operandını ve ':' den koşul operatöründen 
    daha düşük öncelikli operatöre kadar kısım koşul operatörünün üçüncü kısmını oluşturmaktadır. 

    Bazen bir operatörü koşul operatörünün operandı olmaktan çıkartmak isteyebiliriz. Bunun için parantezlerin kullanılması gerekir. Örneğin:

    x = (a % 2 == 0 ? 100 : 200) + 300;

    Burada a çift ise x'e 100 + 300, tek ise 200 + 300 atanacaktır. Çünkü artık '+' operatörü koşul operatörünün üçüncü operandı olmaktan çıkarrılmıştır.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;
    int result;

    printf("Bir deger giriniz:");
    scanf("%d", &a);

    result = (a % 2 == 0 ? 100 : 200) + 300;
    printf("%d\n", result);

    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
    Koşul operatörünün ürettiği değerin tür ikinci ve üçün operand'ların işlem öncesi otomatik tür dönüştürmesine sokulmasıyla
    elde edilen türdür. Örneğin sebolik olarak:

    koşul ? double : int

    Bu durumda koşul operatörü double türden değer üremektedir. Örneğin:

    koşul ? char : short

    Bu durumda koşul operatörü int türden değer üretecektir. Koşul operatörünün operand'ları aynı türden yapı ya da birlik 
    olabilir. Ya da aynı türden adresler de olabilir. Bu konudaki ayrıntılar ileride ele alınacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdki örnekte belli bir tarihin hangi gün olduğunu ekrana (stdout dosyasına) yazan disp_day isimli fonksiyon yazılmıştır. 
    Bu örneği anlayabilmek için şu noktalara dikkat ediniz:

    - Bir yılın artık olup olmadığı şöyle belirlenmektedir: 4'e tam bölünüp 100'e tam bölünmeyen ya da 400'e tam bölünen yıllar artıktır.
    - Önce 01/01/1900'den ilgili tarihe kadar geçen gün sayısı hesaplanmıştır. Sonra bu değerin 7'ye bölümünden elde edilen kalana bakılmıştır. 
    - 01/01/1900 güneü Pazar günüdür. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define isleap(year)	((year) % 4 == 0 && (year) % 100 != 0 || (year) % 400 == 0 ) 			

long total_days(int day, int month, int year)
{
    long tdays = 0;

    for (int i = 1900; i < year; ++i)
        tdays += isleap(i) ? 366 : 365;
        
    switch (month - 1) {
        case 12:			/* unreachable */
            tdays += 31;
        case 11:
            tdays += 30;
        case 10:
            tdays += 31;
        case 9:
            tdays += 30;
        case 8:
            tdays += 31;
        case 7:
            tdays += 31;
        case 6:
            tdays += 30;
        case 5:
            tdays += 31;
        case 4:
            tdays += 30;
        case 3:
            tdays += 31;
        case 2:
            tdays += isleap(year) ? 29 : 28;
        case 1:	
            tdays += 31;
    }

    tdays += day;

    return tdays;
}

void disp_day(int day, int month, int year)
{
    long tdays;

    tdays = total_days(day, month, year);
    
    switch (tdays % 7) {
        case 0:
            printf("Pazar\n");
            break;
        case 1:
            printf("Pazartesi\n");
            break;
        case 2:
            printf("Sali\n");
            break;
        case 3:
            printf("Carsamba\n");
            break;
        case 4:
            printf("Persembe\n");
            break;
        case 5:
            printf("Cuma\n");
            break;
        case 6:
            printf("Cumartesi\n");
            break;
    }
}

int main(void)
{
    disp_day(23, 4, 1920);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bilgisayar sistemlerinde ""ana belleğin (main memory)" (RAM olarak da bilinir) her bir byte'ına ilk byte 0 olmak üzere artan sırada bir sayı karşılık 
    getirilmiştir. Bu sayıya ilgili byte'ın "doğrusal adresi (linear address)" denilmektedir. Doğrusal adres terimi yerine "fiziksel adres (physical address)" terimi de 
    kullanılabilmektedir. Ancak doğrusal adres terimi "sayfalama (paging)" mekanizmasının bulundurğu işlemcilerde daha çok tercih edilmektedir. 

    Doğrusal adresler bilgisayarın çalışma prensibinde mikroişlemciler tarafından elektriksel düzeyde kullanılmaktadır. Çünkü mikroişlemciler RAM'de belli bir yere 
    onun doğrusal adresini bilerek erişirler. Her byte'ın ayrı bir doğrusal adresi vardır. Anımsanacağı gibi C Programlama Dilinde her byte 8 bit olmak zorunda değildir. 
    Dolayısıyla char türü de 8 bit olmak zorunda değildir. Ancak neredeyse sistemlerin hemen hepsinde bir byte 8 bitten oluşmaktadır. 

    Doğrusal adresler geleneksel olarak 16'lık sistemde belirtilirler. Ancak böyle bir zorunluluk yoktur. 

    Bir programdaki her nesne bellekte yer kaplayacağına göre onların birer doğrusal adresleri vardır. Bir byte'tan büyük nesnelerin doğrusal adresleri 
    onların en düşük adres değeriyle ifade edilmektedir. Örneğin int bir nesne bellekte aslında 4 byte oturmuş durumdadır. O halde bu int nesnenin 4 adresi olması gerekir. 
    Ancak biz bu int nesnenin doğrusal adresini ifade ederken onun en düşük adres değerini kullanırız. Örneğin int türden a nesnesi bellekte aşağıdaki gibi bulunuyro olsun:

    ...
    1FC14 --|
    1FC15	|
    1FC16	|	a
    1FC17---|
    ...

    Biz burada a'nın doğrsal adresini 1FC14 olarak ifade ederiz. Gerçekten de aslında işlemci de bir nesneyi RAM'den alırken ya da RAM'e yazarken 
    eğer nesne 1 byte'tan uzunsa onun en düşük adresini belirtir. Yani işlemce RAM'e şunu söylemektedir: "1FC14 doğrusal adresinden başlayan 4 byte'lık değeri bana ver".
    Tabii aslında işlemci de derleyici tarafından üretilmiş olan makine komutlarını çalıştırmaktadır. Eğer kod derlendikten sonra bellekte uygun yere yüklenirse 
    tüm program sorunsuz çalışabilmektedir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de adresler de ayrı bir tür belirtmektedir. Ancak yazılımsal adres iki bileşenli bir bilgidir. Yazılımsal adresin bileşenleri "tür bileşeni" ve "sayısal bileşendir."
    Sayısal bileşen bir doğrusal adres numarası belirtir. Tür bileşeni ise o doğrusal adres numarasından başlayan bilginin türünü belirtmektedir. 
    donanımsal olarak adres yalnızca bir sayıdan oluşmaktadır. Yazılımsal adresin sayısal bileşeni bir doğrusal adres numarası belirtir. Tür bileşeni ise 
    o doğrusal adresten başlayan nesnenin türüne ilişkindir. 

    Bundan sonra yalnızca "adres" denildiğinde "yazılımsal adres" belirtilecektir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        32. Ders 27/09/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Adres bilgileri C'de ayrı bir tür belirtmektyedir. Bir adres sabiti oluşturmanın genel biçimi şöyledir:

    (tür_bileşeni *) saysal_bileşen

    Örneğin:

    (int *) 0x1FC140

    Burada bu adres sabitinin sayısal bileşeni 0X1FC140 biçimindedir. Bu bir doğrusal adres belirtir. Bu adres sabitinin tür bileşeni int biçiminmdedir. 
    Adres sabitlerinin tür bileşenlerinin 16'lık sistemde belirtilmesi zorunluı değildir. Ancak yaygın bir gösterimdir. Aslında yukarıdaki adres sabiti 
    bir tür dönüştürme işlemidir. Bu işlemin (int *) kısmıdaki int adresin tür bileşenini belirtir. Buradaki * ise adres kavramı için kullanılmaktadır.

    C'de adres bilgileri adresin tür belişeni belirtilerek ifade edilir. Yani örneğin "adres" denmez, "int türden adres, double türden adres vs." denir. 
    İngilizce "adres" kavramı "pointer" sözcüğü ile ifade edilmektedir. "int türden adres" ise "pointer to int", "long türden adres" "pointer to long" 
    biçiminde belirtilir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aralarında fiziksel ya da mantıksal ilişki bulunan bir grup nesnenin oluşturğu topluluğa "veri yapısı (data structure)" denilmektedir. Veri yapısı 
    kavramı bir grup nesneyi çağrıştırmalıdır. Örneğin diziler, bağlı listeler, kuyruk sistemleri bir grup nesneden oluşan topluluklardır. Bunlar birer veri yapısıdır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Diziler (arrays) elemanları aynı türden olan ve bellekte ardışıl bir biçimde bulunan veri ytapılarıdır. Dizilerin elemanları aynı türdendir. Elemanlar arasında
    hiç boşluk yoktur. Buradaki "ardışıl (contiguous)" bir elemandan sonra hemen diğerinin geldiği yani arada hiç boşluk olmadığı anlamına gelmektedir. 

    Dizi tanımlamanın genel biçimi şöyledir:

    <tür> <dizi_ismi><[<uzunluk_ifadesi>]>;

    Örneğin:

    int a[10];
    double b[20];

    C90'da dizi tanımlamasında dizi uzunluklarının sabit ifadesi biçiminde belirtilmesi zorunludur. Ancak C99 ile birlikte yerel diziler için dizi uzunluklarının
    sabit ifadesi yerine değişken içeren ifadelerle de belirtilmesine olanak sağlanmıştır. Örneğin:

    {
        int n = 10;
        int a[n];		/* C90'da geçersiz, C99 ve ötesinde geçerli */
        ...
    }

    C++ her ne kadar C'yi kapsıyor olsa da C99 ile eklenen bu özelliği hiçbir zaman benimsememiştir. Microsoft C derleicileri de dil ayarı C99, C11, C17 yapılsa 
    bile halen bu özelliği desteklememktedir. 

    Bir diziyi dizi yapan iki özellik vardır:

    1) Dizinin tüm elemanları aynı türdendir. 
    2) Elemanlar arasında hiç boşluk yoktur. Yani elemanlar bbellekte ardışıl bir biçimde tutulur.

    Biz bir grup nesneyi tanımladığımızda bunların ardışıullığı konusunda C'de hiçbir garanti verilmemektedir. Örneğin:

    int x, y, z;

    Burada x, y ve z'nin bellekteki yerleşimleri herhangi bir biçimde olabilir. Ardışıllığı garanti edilmemektedir. Oysa örneğin:

    int a[3];

    Buradaki 3 int eleman kesinlikle ardışıl bir biçimde tutulur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir dizi bütünsel olarak işleme sokulamaz. Dizinin elemanlarına erişlilir. Dizinin elemanları bağımısız nesneler biçiminde işleme sokulur. 
    Dizi elemanlarıne erişmek için [...] operatörü kullanılır. Elemana erişmenin genel biçimi şöyledir:

    dizi_ismi[ifade]

    Dizinin ilk elemanı 0'ıncı indeksli elemandır. Bu durumda n elemanlı bir dizinin son elemanı n - 1'inci indeksli elemanıdır. Dizi elemanlarına erişilirken
    köşeli parantez içerisindeki ifade sabit ifadesi olmak zorunda değildir. Ancak index belirten ifadenin tamsayı türlerine ilişkin olması zorunludur. 
    Aslında eleman erişmekte kullanılan köşeli parantezler "tek operandlı sonek (unary postfix)" operatör belirtmektedir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Dizilerin en önemli kullanılma nedeni bir döngü içerisinde onların tüm elemanlarının işleme sokulmasıdır. Örneğin:

    int a[1000];

    for (int i = 0; i < 1000; ++i)
        a[i] = 0;
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{  
    int a[10];

    for (int i = 0; i < 10; ++i)
        a[i] = i * i;

    for (int i = 0; i < 10; ++i)
        printf("%d\n", a[i]);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Global bir dizinin tüm elemanlarında başlangıçta 0 değerleri bulunur. Ancak yerel dizilerin içerisinde başlangıçta çöp değerler bulunmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir bildirimde tür belirten sözüğün dışındaki atomlara "dekleratör (declarator)" denilmektedir. Örneğin:

    int a, b, c;

    Burada int tür, a, b, ve c dekleratörlerdir. Örneğin:

    double a[10];

    Burada double tür a[10] dekleratördür. C'de bildirimdeki tür tüm dekleratörlerin ortak türüdür. Örneğin:

    int a[10], b;

    Bu tanımlama geçerlidir. Burada a 10 elemanlı int bir dizi b ise int bir nesnedir. Yani dizilerle normal nesneler beraber tanımlanabilirler. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir diziye tanımlar tanımlamaz küme parantezleri içerisinde ilkdeğer verebiliriz. Bju durumda verilen ilkdeğerler sırasıyla dizi elemanlarına 
    yerleştirilir. Örneğin:

    int a[5] = {10, 20, 30, 40, 50};

    Babii bu işlem daha sonra yapılamaz. Örneğin:

    intr a[5];

    a = {10, 20, 30, 40, 50};		/* geçersiz! */


----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{  
    int a[5] = {10, 20, 30, 40, 50};

    for (int i = 0; i < 5; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Dizinin az sayıda elemanına ilkdeğer verilebilir. Bu durumda gerei kalan elemanlar dizi yerel de olsa, global da olsa kesinlikle derleyici tarafından 
    sıfırlanmaktadır. Ancak dizinin fazla sayıda elemanına ilkdeğer vermek geçersizdir. Örneğin:

    int a[5] = {10, 20, 30};				/* geri kalan 2 eleman 0 */
    int b[5] = {10, 20, 30, 40, 50, 60};	/* error! dizi 5 elemanlık ancak 6 elemana ilkdeğer verilmiş */
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{  
    int a[5] = {10, 20};

    for (int i = 0; i < 5; ++i)
        printf("%d ", a[i]);    /* 10 20 0 0 0 */
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Dizilere ilkdeğer verme işleminde küme parantezlerinin içi boş bırakılamaz. Örneğin:

    int a[10] = {};		/* geçersiz! */

    Yerel dizinin tüm elemanlarının 0 olmasını istiyorsak bunu en yalın ancak aşağıdaki gibi sağlayabiliriz:

    int a[10] = {0};	/* a'nın tüm elemanları 0 */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Dizilere ilkdeğer verilirken dizi uzunlukları belirtilmeyebilir. Bu durumda derleyici verilen ilkdeğerleri syar ve dizinin o uzunlukta açılmış olduğunu kabul eder. 
    Örneğin:

    int a[] = {10, 20, 30};		/* burada dizi 3 uzunlukta */
    int b[];					/* geçersiz! dizi uzunluğu belirtilmek zorunda */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte dizilere ilkdeğer vermede "designated initializer" denilen bir sentaks da fdile eklenmiştir. Bu sentaks sayesinde dizinin 
    ardışıl olmayan elemanlarına ilkdeğerverilebilmektedir. Örneğin biz 100 elemanlı int bir dizide dizinin yalnızca 25, 50, 75 ve 99'uncu elemanlarına değer 
    atamak isteyebiliriz. "Designated initializer" sentaksı şöyledir:

    [<sabit ifadesi>] = değer

    Örneğin:

    int a[100] = {[25] = 100, [50] = 200, [75] = 300, [99] = 400};

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[100] = {[25] = 100, [50] = 200, [75] = 300, [99] = 400};
    
    for (int i = 0; i < 100; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Designated initilizer sentaksından sonra normal ilkdfeğer vermelere devam edilebilir. Bu durumda sonra verilen ilkdeğerler designated initilizer'da 
    belirtilen indeksi izlemektedir. Örneğin:

    int a[10] = {1, 2, 3, [6] = 100, 4, [8] = 200};

    Burada 4 değeri 7'inci elemana yerleştirilecektir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[10] = {1, 2, 3, [6] = 100, 4, [8] = 200};
    
    for (int i = 0; i < 10; ++i)
        printf("%d ", a[i]);        /* 1 2 3 0 0 0 100 4 200 0 */
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Designated initlizer sentaksında köşeli parantez içerisindeki indeks değerlerinin artan sırada olma zorunluluğu yoktur. Örneğin:

    int a[10] = {[5] = 100, 200, [1] = 300};

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[10] = {[5] = 100, 200, [1] = 300};
    
    for (int i = 0; i < 10; ++i)
        printf("%d ", a[i]);        /* 0 300 0 0 0 100 200 0 0 0 */
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Designated initializer sentaksında dizinin aynı elemanına birden fazla kez değer atama durumu oluşabilir. Bu tür işlemler anlamsız olsa dayasaklanmamıştır. 
    Örneğin:

    int a[10] = {10, 20, 30, [0] = 100, 200};		/* geçerli ama anlamsız */

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[10] = {10, 20, 30, [0] = 100, 200};
    
    for (int i = 0; i < 10; ++i)
        printf("%d ", a[i]);        /* 100 200 30 0 0 0 0 0 0 0 */
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Designated initializer sentaksında dizi uzunluğu yine belirtilmeyebilir. Bu durumda sentaksta belirtilen en yüksek indeks temel alınarak dizi uzunluğu
    belirlenmektedir. Örneğin:

    int a[] = {10, 20, 30, [90] = 100, 200};

    Burada dizi 92 eleman uzunluğunda açılacaktır.

    Ancak dizi uzunluğu belirtilmişse designated initializer sentaksında indeks değeri dizinb uzunluğuna eşit ya da ondan büyük olamaz. Örneğin:

     int a[50] = {10, 20, 30, [90] = 100};		/* geçersiz! */

     Tabii köşeli parantez içerisindeki indeks belirten ifadenin sabit ifadesi olması zorunludur:

    int i = 20;
    int a[50] = {10, 20, 30, [i] = 100};		/* geçersiz! i sabit ifadesi değil */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizinin en büyük elemanı şöyle bulunur: Önce ilk eleman en büyük kabul edilir ve bir değişkende saklanır. Sonra diğer tüm elemanlar tek tek gözden geçirilir. 
    Daha büyük eleman bulununca o eleman saklanır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10

int main(void)
{  
    int a[SIZE] = {3, 5, 7, 7, 12, 67, 3, 34, 11, 23};
    int max;

    max = a[0];

    for (int i = 1; i < SIZE; ++i)
        if (a[i] > max)
            max = a[i];

    printf("%d\n", max);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıda bir dizinin aritmetik ortalamasını bulan bir program örneği verilmektedir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10

int main(void)
{  
    int a[SIZE] = {3, 5, 7, 7, 12, 67, 3, 34, 11, 23};
    int total;
    double avg;

    total = 0;
    for (int i = 0; i < SIZE; ++i)
        total += a[i];
    
    avg = (double)total / SIZE;

    printf("%f\n", avg);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir diziyi ters çevirmek için baştaki sondaki elemanları yer değiştirebiliriz. Tabii bu işlemi dizi uzunluğunun yarısı kadar yapmak gerekir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10

int main(void)
{  
    int a[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int temp;
   
    for (int i = 0; i < SIZE / 2; ++i) {
        temp = a[i];
        a[i] = a[SIZE - 1 - i];
        a[SIZE - 1 - i] = temp;
    }

    for (int i = 0; i < SIZE; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizide bir elemanı arayıp onu bulduğunda bulduğu yerin indeksini ekrana yazdıran bir program örneği aşağıda verilmiştir. Bir dizinin tüm elemanlarını
    kontrol ederek arama işlemine "sıralı arama (sequential search)" denilmektedir. ASıralı aramda eğer arama başarılı ise (successful search) 
    ortalama karşılaştırma sayısı n / 2'dir. Ancak arama başarısız olursa n karşılaştırma yapılır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10

int main(void)
{  
    int a[SIZE] = {23, 12, 76, 45, 23, 65, 11, 98, 42, 81};
    int val;
    int i;

    printf("Bir değer giriniz:");
    scanf("%d", &val);

    for (i = 0; i < SIZE; ++i)
        if (a[i] == val)
            break;

    if (i == SIZE)
        printf("bulunamadi!\n");
    else
        printf("%d. indekste bulundu\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                            33. Ders 29/09/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Dizilerin sıraya dizilmesine İngilizce "sorting" denilmektedir. Dizileri sıraya dizmek için pek çok algoritma vardır. Bunlardan en yalını 
    "kabarcık sıralaması (bubble sort)" denilen yöntemdir. Bu yöntemde yan yana iki eleman karşılaştırılır. Duruma göre yer değiştirilir. Bu işlem bir kez
    yapıldığında dizi sıraya dizilmiş olmaz. Ancak en büyük eleman (ya da en küçük eleman) sona gider. O halde bu işlemi diziyi daraltarak tekrar tekrar yapmak 
    gerekir. Algoritmanın döngü yapısı şöyledir: Dizinin uzunluğu n olmak üzere iç içe iki döngü vardır. Dıştaki döngü n - 1 kez, içteki döngü n - 1 - i kez 
    döndürülür. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10

int main(void)
{  
    int a[SIZE] = {23, 12, 76, 45, 23, 65, 11, 98, 42, 81};
    int temp;
  
    for (int i = 0; i < SIZE - 1; ++i)
        for (int k = 0; k < SIZE - 1 - i; ++k) 
            if (a[k + 1] < a[k]) {
                temp = a[k];
                a[k] = a[k + 1];
                a[k + 1] = temp;
            }

    for (int i = 0; i < SIZE; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Kabarcık sıralamasının değişik gerçekleştirimi yapılabilir. Örneğin eğer yan yana elemanlar karşılaştırılıp hiç yer değiştirme yapılmıyorsa 
    dizi zaten sıraya dizilmiş demektir. Döngünün devam ettirilmesine gerek yoktur. 

    Aşağıdaki gerçekleştirimde eğer dizi zaten sıraya dizilmişse dış döngü devam ettirilmemektedr.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10

#define TRUE        1
#define FALSE       0

int main(void)
{  
    int a[SIZE] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    int temp;
    int replace_flag;
  
    for (int i = 0; i < SIZE - 1; ++i) {
        replace_flag = FALSE;
        for (int k = 0; k < SIZE - 1 - i; ++k)
            if (a[k + 1] < a[k]) {
                temp = a[k];
                a[k] = a[k + 1];
                a[k + 1] = temp;
                replace_flag = TRUE;
            }
        if (!replace_flag)
            break;
    }

    for (int i = 0; i < SIZE; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki biçim do-while döngüsüyle de ifade edilebilir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10

#define TRUE        1
#define FALSE       0

int main(void)
{  
    int a[SIZE] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    int temp;
    int replace_flag;
    int n = SIZE - 1;
  
    do {
        replace_flag = FALSE;
        for (int k = 0; k < n; ++k)
            if (a[k + 1] < a[k]) {
                temp = a[k];
                a[k] = a[k + 1];
                a[k + 1] = temp;
                replace_flag = TRUE;
            }
        --n;
    } while (replace_flag);

    for (int i = 0; i < SIZE; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Diğer çok bilinen bir sıralama yöntemine "seçerek sıralama (selection sort)" denilmektedir. Bu yöntemde dizinin en küçük elemanı bulunur. İlk elemanla
    yer değiştirilir. Sonra dizi daraltılır. Aynı daraltılmış dizi için yapılır. İşlemler böyle böyle devam ettirilir. Örneğin:

    | 8 3 6 1 5

    1 | 3 6 8 5
    1 3 | 6 8 5
    1 3 5 | 8 6
    1 3 5 6 | 8

    Bu algoritmada iç içe iki döngü kullanılır. Dıştaki döngü diziyi daraltmakta kullanılır. İçteki döngü ise daraltılmış dizinin en küçük elemanını 
    bulup daraltılmış dizinin ilk elemanı ile yer değiştirir.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10

int main(void)
{  
    int a[SIZE] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
    int min, min_index;
    
    for (int i = 0; i < SIZE - 1; ++i) {
        min = a[i];
        min_index = i;
        for (int k = i + 1; k < SIZE; ++k) 
            if (a[k] < min) {
                min = a[k];
                min_index = k;
            }
        a[min_index] = a[i];
        a[i] = min;
    }

    for (int i = 0; i < SIZE; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizinin olmayan bir elemanına erişmeye çalışmak "tanımsız davranışa" yol açar. Örneğin:

    int a[10];

    for (int i = 0; i <= 10; ++i)       /* tanımsız davranış! */
        a[i] = 0;

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Daha önceden de belirttiğimiz gibi aslında bir yazı karakterlerden oluşan bir dizi belirtmektedir. Karakterler ise aslında o karakterlerin karakter tablosundaki
    sıra numarasını belirtir. O halde aslında bir yazı bir sayı dizisi gibi ele alınabilir. Pekiyi mademki bir yazı bir sayı dizisi gibidir. O halde yazınıun karakterlerine
    karşı gelen sayıları hangi türden dizide tutmalıyız? Tabii bunun için en uygun tür char türüdür. Çünkü zaten C'de char bir kadarkterin sıra numarasını tutabilecek 
    büyüklüğü temsil etmektedir. C'de karakterler 1 byte içerisnde tutulmaktadır. Karakterlerin sıra numaralarını tutmak için en uygun tür char türüdür. 
    O halde bir yazı char türden bir dizide tutulmalıdır. Yazının her bir karakteri char türden dizinin bir elemanında tutulursa dizi yazıyı tutar hale gelir. 

    Genel olarak yazıyı tutan char dizi yaznının uzunluğundan büyük olur. Yani bir char dizinin içerisindeki yazı onun başından itibaren belli bir kısmındadır. 
    Bu char diziyi alan programcı yazının bu dizinin başından başladığını bilir ancak nerede bittiğini de anlaması gerekir. İşte C'de char bir dizi 
    içerisindekiş yazının bitişl yeri özel bir karakterle belirtilmektedir. Bu karaktere "null kartakter" denir. Programcılar ve C'nin bazı semantik kuralları
    bir yazının sonunda null karakter olması gerektiği konusunda anlaşmış durumdadırlar. null karakter karakter tablosunun ilk karakteridir ve sayısal değeri 
    0'dır. (Bunu '0' karakteri ile karıştırmayınız.) Null karakter '\0' ile temsil edilir. Aslında '\0' teknik olarak 0 sabiti ile aynı anlamdadır. 
    Ancak programcılar null karakter için '\0' gösterimini tercih ederler. Çünkü '\0' gösterini bir karakter görüntüsünde olduğu için okunabilirliği daha fazladır. 
    
    Programcı char türden bir dizinin içerisine bir yazı yerleştirecekse null karakteri yazının sonuna yerleştirmek onun sorumlulupundadır. Örneğin:

    char s[10];

    s[0] = 'a';
    s[1] = 'l';
    a[2] = 'i';
    a[3] = '\0';

    Null karakter dizinin içerisinde bir yer kaplar. Bu durumda n eleman uzunluğundaki bir char diziye biz en fazla n - 1 karakterli bir yazı yerleştirebiliriz. 
    Örneğin elimizde 10 elemanlı bir char dizi varsa biz onun içerisine en fazla 9ı karakterli bir yazı yerleştirebiliriz. Çünkü son elemanda null karakter olmak zorundadır. 
    Null karakterin dizinin sonunda değil dizinin içerisindeki yazının sonunda olması gerektiğine dikkat ediniz. 

    Biz bir char diziye ilkdeğer verme sentaksıyla da bir yazı yerleştirebiliriz. Örneğin:

    char s[100] = {'a', 'n', 'k', 'a', 'r', 'a', '\0'};
    
    Tabii burada aslında null karakteri hiç belirtmeseydik de dizinin geri kalan elemanları sıfırlanacağından dolayı, null karakter de 0 olduğu için 
    sanki null karakter yazının sonuna eklenmiş gibi olacaktı. Örneğin:

    char s[100] = {'a', 'n', 'k', 'a', 'r', 'a'};		/* geçerli zaten 0 ile null karakter aynı */

    Tabii null karakterin açıkça belirtilmesi daha anlaşılabilir bir görüntü sunmaktadır. Örneğin:

    char s[] = {'a', 'l', 'i'};		/* geçerli ancak null karakter yazının sonuna eklenmemiş */

    Burada dizi uzunluğu belirtilmediği için verilen ilkdeğerler kadar dizi açılır. Ancak null karakter yazının sonuna eklenmemiştir. Programcının 
    yazının sonuna null karakter eklenmesini sağlaması gerekir:

    char s[] = {'a', 'l', 'i', '\0'};

    C'de char, unsigned char ya da signed char türünden bir diziye iki tırnak ile bir yazı pratik bir biçimde de yerleştirilebilmektedr. Örneğin:

    char s[100] = "ankara";

    C'de char, unsigned char ve signed char türünden bir diziye iki tırnak ile ilkdeğer verilmişse bu durumda derleyici bu iki tırnak içerisindeki 
    karakterleri tek tek diziye yerleştirir. Yazının sonuna null karakteri kendisi ekler. Örneğin:

    char s[] = "ankara";

    Burada derleyici null karakteri kendisi ekleyeceği için dizinin 7 eleman uzunluğunda açıldığını kabul eder. İki tırnak ile yalnızca char, signed char ve 
    unsigned char türünden dizilere ilkdeğer verilebilmektedir. Örneğin:

    int s[] = "ankara"; 	/* geçersiz! iki tırnak ile int bir diziye ilkdeğer verilemez! */

    Daha sonra bir diziye iki tırnak ile atama yapamayız. İki tıornak sentaksının ilkdeğer verme sırasında geçerli olduğuna dikkat ediniz. Örneğin:

    char s[100];

    s = "ankara";		/* geçersiz! */

    Bir diziye fazla sayıda elemanla ilkdeğer veremediğimizi belirtmiştik. Örneğin:

    char s[3] = "ankara";		/* geçersiz! */

    Özel bir durum olarak eğer iki tırnak içerisindeki karakter sayısı dizi unluğu kadar ise bu durum C'de geçerli kabul edilmektedir. Ancak derleyici bu duurmda
    null karakteri yazının sonuna eklememektedir. Örneğin:

    char s[3] = "ali";		/* geçerli ama dikkat null karakter yazının sonuna eklenmeyecek */

    Bu durumda hata kaynağı oluşturabileceği gerekçesiyle C++'ta geçersiz kabul edilmektedir. 

    char, signed char ve unsigned char türünden dizilere iki tırnak ile ilkdeğer veridliğinde diziningeri kalan elemanlarının hepsi yine sıfırlanmaktadır. Örneğin:

    char s[100] = "ali"; 	/* null karakter eklendikten sonra geri kalan elemanların hepsi sıfırlanır */

    İlkdeğer verilirken iki tırnağın içi boş olabilir. Örneğin:

    char s[100] = "";

    Burada diziye null karakter yerleştirilir. Sonra geri kalan tüm elemanlar sıfırlanır. Tabii null karakterin numarası 0 olduğuna göre aslıunda tüm dizi sıfırlanmaktadır. 
    Örneğin:

    char s[] = "";

    Burada dizi 1 eleman olarak açılır ve o bir elemana null karakter yerleştirilir.

    char türdne bir dizinin içerisinde sonu null karakterle biten bir yazı bulunuyor olsun. Bu yazıyı nasıl ekrana (stdout dosyasına) yazdırabiliriz?

    char s[100] = "ankara";

    for (int i = 0; s[i] != '\0'; ++i)
        putchar(s[i]);

    Tabii madem ki null karakterin sayısal değeri 0'dır. o zaman dönü şöyle de ifade edilebilirdi:

    for (int i = 0; s[i]; ++i)
        putchar(s[i]);

    Ancak okunabilirlik bakımından önceki biçim tercih edilebilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{  
    char s[100] = "ankara";

    for (int i = 0; s[i] != '\0'; ++i)
        putchar(s[i]);
    putchar('\n');

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir char dizisinin başındaki yazının karakter uzunluğu aşağıdaki gibi bulunabilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{  
    char s[100] = "istanbul";
    int i;

    for (i = 0; s[i] != '\0'; ++i)
        ;

    printf("%d\n", i);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    char türden bir dizinin içerisindeki yazıyı tersten yazdırmaya çalışalım. Bunun tek yolu önce yazının sonuna gitmek sonra oradan başa doğru giderek 
    karakterleri yazdırmak olabilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{  
    char s[] = "istanbul";
    int i;

    for (i = 0; s[i] != '\0'; ++i)
        ;

    for (--i; i >= 0; --i)
        putchar(s[i]);
    putchar('\n');

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bu tür durumlarda indis değişkenini işaretsiz tamsayı türünden almak istiyorsanız dikkatli olmalısınız. Örneğin aşağıdaki
    döngüde i'nin unsigned int türden olduğunu varsayalım. Bu durumda i >= 0 koşulu her zaman sağlanacak ve dizi taşmasından 
    kaynaklanan bir tanımsız davranış oluşacaktır:
        
    for (--i; i >= 0; --i)
        putchar(s[i]);

    unsigned bir nesnedeki 0 değerini -- operatörü ile azaltmaya çalışırsak -1 değeri o nesne ifade edilebilcek en büyük 
    tamsyaı değer haline gelmektedir. Pekiyi bu durumda yukarıdaki döngü nasıl oluşturulmalıdır? Çözüm için en basit 
    düzenleme koşulda sonek -- operatörü kullanmaktır. Örneğin:

    while (i-- > 0)
        putchar(s[i]);
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    char bir dizinin içerisinedeki yazıyı ters çevirmek isteyelim. Biz daha önce int bir diziyi ters çevirmiştik. Aynı algoritmayı uygulayabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{  
    char s[100] = "ankara";
    int n;
    char temp;

    for (n = 0; s[n] != '\0'; ++n)
        ;

    for (int i = 0; i < n / 2; ++i) {
        temp = s[i];
        s[i] = s[n - 1 - i];
        s[n - 1 - i] = temp;
    }

    for (int i = 0; s[i] != '\0'; ++i)
        putchar(s[i]);
    putchar('\n');

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                        34. Ders 04/10/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'nin prototipi <stdio.h> içerisinde bulunan standart puts isimli fonksiyonu bir yazıyı ekrana (stdout dosyasına) yazdırmak için kullanılmaktadır. 
    Fonksiyonun genel kullanımı şöyledir:

    puts(<dizi_ismi>);

    Aslında ileride görüleceği gibi puts fonksiyonu char türden bir adres almaktadır. Ancak biz şimdilik bu fonksiyonun yazının içinde bukunduğu char türden dizinin
    ismini parametre olarak alacağını belirtelim. C'de programcının dışında başkaları tarafından yazılmış olan (standart fonksiyonlar da dahil) hiçbir fonksiyon
    bir dizinin uzunluğunu bilemez. Dizinin uzunluğunu yalnızca onu açan programcı biliyor durumdadır. 

    puts fonksiyonu dizinin başından başlayarak null karakter görene kadar tüm karakterleri yan yana yazdırır. En sonunda imleci aşağı satırın başına geçirerek 
    orada bırakır. 	
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[100] = "ankara";

    puts(s);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    puts fonksiyonu dizinin başından null karakter görene kadar karakterleri yazdırmaktadır. Eğer null karakter bir biçimde ezilmişse puts durmaz 
    ilk null karakter görene kadar dizinin elemanlarını karakter olarak yazdırır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[100];

    s[0] = 'a';
    s[1] = 'l';
    s[2] = 'i';
    s[3] = '\0';

    puts(s);

    s[3] = 'x';
    puts(s);		/* alix'ten sonra tuhaf karakterler çıkabilir */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yazılar tipik olarak char türden dizilerin içerisine yerleştirilirler. Ama bunun tersi doğru değildir. Yani char türden dizilere yazı yerleştirmek 
    zorunda değiliz. Pekala biz char türden bir diziyi az yer kaplayan bir tamsayı dizisi olarak kullanabiliriz. Bu durumda dizinin sonuna null karakter yerleştirmenin
    bir anlamı yoktur. Örneğin:

    char s[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

    Burada s dizisi bir byte yer kaplayan bir tamsayı dizisi gibi kullanılmak üzere oluşturulmuştur. Bu kullanımın null bir ilgisi yoktur. Biz char türden 
    dizilere yazı yerleştireceksek null karakteri yazının sonuna yerleştirmeliyiz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[] = {1, 2, 3, 4, 5};

    for (int i = 0; i < 5; ++i)
        printf("%d ", s[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Null karakter ile '0' karakterinin ilgisi olmadığına dikkat ediniz. Null karakter gerçekten 0 değerine ilişkin karakterdir. Yani null karakterin sayısal değeri 
    0'dır. Ancak '0' karakterinin sayısal değeri ASCII tablosundan 48'dir. Daha önceden de belirttiğimiz gibi '\0' gösterimi ile 0 gösterimi arasında teknik 
    bir farklılık yoktur. Ancak karakter vurgusu yapmak için null karakteri '\0' biçiminde göstermek iyi bir tekniktir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char c;

    c = '0';

    printf("%d\n", c);		/* 48 */

    c = '\0';				
    printf("%d\n", c);		/* 0 */

    c = 0;
    printf("%d\n", c);		/* 0 */


    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bellekte karakter diye bir bilginin olmadığına dikkat ediniz. Karakterler aslında birer sayı olarak bellek bulunurlar. Biz C'de bir karakteri tek tırnak 
    içerisine aldığımızda o karakterin ilgili karakter tablosundaki sıra numarısını belirtmiş oluruz. Yani örneğin ASCII tablosunun kullanıldığı bir C derleyicisinde
    'a' ile 97 arasında bir farklılık yoktur. Bellekte her şeyin aslında ikilik sistemde sayılar biçiminde bulunduğuna onun nasıl yorumlanacağına programcının karar verdiğine
    dikkat ediniz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[10] = {97, 98, 99, 0};
    
    puts(s);		/* abc */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C standartlarında bir özelliğin "deprecated" olması "ileride kaldırabileceği, ancak şimdilik muhafaza edildiği" anlamına gelmektedir. Mademki deprecated
    özellikler ileride kaldırılabilecek özelliklerdir. O halde programcıların deprecated özellikleri kullanmaması gerekir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    gets isimli standart C fonksiyonu C'nin ilk günlerinden beri vardı. Ancak bir tasarım bozukluğundan dolayı C99'da deprecated yapıldı ve C11'de 
    standart fonksiyon listesindne çıkartıldı. Bugünkü derleyiclerde gets halen desteklenmektedir. Ancak bu fonksiyon kullanılırken derleme ya da link aşamasında
    uyarı oluşabilmektedir. Her ne kadar gets fonksiyonu C11 ile C'den çıkartılmışsa da kursumuzda eğitim amaçlı nedenlerle bu fonksiyonu kullanacağız. 
    C11 gets yerine gets_s isimli yeni bir fonksiyonu kütüphaneye eklemiştir. Ancak maalesef bu fonksiyon da standartlarda "optional" yapılmıştır. Optional
    özellik demek ilgili derleyicinin barındırp barındırmayacağı derleyiciyi yazanlara bağlı demektir. Gerçekten de gets_s fonksiyonu gcc derleyicilerinde henüz 
    bulunmamaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    gets fonksiyonu klavyeden (stdin dosyasından) bir yazı okumak için kullanılmaktadır. tipik kullanımı şöyledir:

    gets(dizi_ismi);

    gets aslında char türdne bir adres almaktadır. Ancak ileride bu konu ele alınacaktır. gets fonksiyonu ENTER tuşuna basılana kadar girilen karakterleri 
    (yani onların sayısal karşılıklarını) diziye tek tek yerleştirir. Yazının sonuna null karakteri ekler ve işlemini sonlandırır. Örneğin:

    char s[100];

    gets(s);

    gets fonksiyonun kusuru kullanıcı uzun yazı girerse diziyi taşırabilmesidir. Biz gets ile n eleman uzunluğundaki bir char diziye en fazla n - 1 karakterli bir 
    yazı girmeliyiz. Çünkü gets null karakteri de yazının sonuna eklemektedir. Null karakter de diznin içerisinde kalmak zorundadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[1024];

    printf("Bir yazi giriniz:");
    gets(s);

    puts(s);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir dizinin taşırılması "tanımsız davranış (undefined behavior)" oluşturmaktadır. Eğer biz gets fonksiyonu için küçük bir dizi açarsak ve klavyeden 
    çok karakter girersek dizi taşar ve programımız çökebilir. n elemanlı bir char diziye gets ile en fazla n - 1 karakterli bir yazı girebiliriz. 
    Aşağıdaki örnekte uzun bir yazı girerek sonucu gözleyniz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[5];			/* dikkat dizi çok kğçük, taşabibilir! */

    printf("Bir yazi giriniz:");
    gets(s);

    puts(s);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir char dizi içerisindeki karakterleri null karakter görene kadar yazdırmak için puts fonksiyonun yanı sıra printf fonksiyonu da kullanılabilir. 
    printf fonksiyonunda %s format karakterine char türdne bir dizi ismi (aslında bir adres) karşı getirilirse printf null karakter görene kadar 
    dizinin içerisindeki karakterleri yan yana ekrana (stdout dosyasına) yazar. Tabii printf imleci aşağı satıra otomatik geçirmemektedir. O halde:

    puts(char_dizi_ismi);

    işleminin eşdeğeri:

    printf("%s\n", char_dizi_ismi);

    biçimindedir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[1024];

    printf("Bir yazi giriniz:");
    gets(s);

    printf("%s\n", s);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizi içerisindeki yazıyı başka bir dizye nasıl kopyalayabiliriz? İlk akla gelen yöntem null karakter görene kadar kopyalama yapmaktır. Tabii 
    null karakterin de hedef diziye kopyalanması gerekir. 

    char s[100] = "this is a test";
    char d[100];
    int i;

    for (i = 0; s[i] != '\0'; ++i)
        d[i] = s[i];
    d[i] = '\0';

    Aslında bu işlem daha kısa şöyle yapılabilir:

    for (int i = 0; (d[i] = s[i]) != '\0'; ++i)
        ;
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[100];
    char d[100];
    
    printf("Bir yazi giriniz:");
    gets(s);

    for (int i = 0; (d[i] = s[i]) != '\0'; ++i)
        ;

    puts(s);
    puts(d);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'nin en önemli konularından biri "göstericiler (pointers)" konusudur. C bir gösterici dilidir. Kurusumuzun bu 
    bölümünde göstericiler konusunu aytıntılı bir biçimde ele alacağız.
----------------------------------------------------------------------------------------------------------------------*/


/*----------------------------------------------------------------------------------------------------------------------
    Adres bilgilerinin yerleştirildiği nesnelere "gösterici (pointer)" denilmektedir. Bir adres bilgisi int bir nesneye, double bir nesneye yerleştirilemez. 
    Adresler iki bileşenli özel türlerdir. Elimizde bir adres bilgisi varsa biz onu ancak bir göstericiye atayabiliriz. Yani adres tutan nesnelere 
    gösterici denilmektedir. C'de bir gösterici bildiriminin genel biçimi şöyledir:

    <tür> *<gösterici_ismi>;

    Örneğin:

    int *pi;
    long *pl;

    Burada '*' atomunun çarpmayla bir ilgisi yoktur. Buradaki '*' gösteriyi belirtmektedir. Atomlar arasında istenildiği kadar boşluk karakteri bırakılabileceğine 
    göre bu bildirimler örneğin aşağıdaki gibi de yazılabilir:

    int 
        *
    pi;

    long* pl;

    Ritchie/Kernighan yazım biçiminde * atomu gösterici ismine bitiştirilmektedir. Biz de bu yazım biçimi kullanacağız. 

    Bir göstericiye herhangi bir adres bilgisi atanamaz. Ancak tür bileşeni uygun olan bir adres bilgisi atanabilir. Örneğin:

    int *pi;

    Burada pi göstericisine biz ancak tür bileşeni int olan bir adres bilgisi atayabiliriz. Örneğin:

    pi = (int *)0x1FC14;		/* geçerli pi int türdne gösterici, ona int türden bir adres bilgisi atanmış */

    Örneğin:

    int *pi;

    pi = (double *)0x1B12C0;		/* geçersiz! pi'ye int türdne bir adres bilgisinin atanması gerekirdi. Halbuki double türden bir adres bilgisi atanmıştır */

    Bir adres bilgisi gösterici olmayan bir nesneye de atanamaz. Örneğin:

    int a;

    a = (int *) 0x1FC90;	/* geçersiz! adres bilgileri temel türlere atanamaz, göstericilere atanabilir */

    Bir göstericiye bir tamsayı da atayamayız. Ancak aynı türden bir adres bilgisi atayabiliriz. Örneğin:

    int *pi;

    pi = 0x1FC10;	/* geçersiz! int türden göstericiye adi bir int atanamaz, int türden adres bilgisinin atanması gerekir */

    Yani özetle bir göstericiye aynı türden bir adres bilgisi atanabilir. Bir adres bilgisi de yalnızca aynı türden bir göstericiye atanabilir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir adres bilgisi yanı türden bir göstericiye atandığında göstericiye adresin yalnızca sayısal bileşeni yerleştirilir. Çünkü tür bileşeni 
    zaten bildirimde derleyici tarafından bilinmektedir. Örneğin:

    int *pi;

    pi = (int *) 0x1FC14;

    Burada pi'nin içerisinde 1FC14 değeri bulunur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Tek operandlı önek & operatörüne C'de "address of" operatör denilmektedir. Bu operatörün operandı bir nesne olmak zorundadır. Bu operatör ilgili nesnenin 
    bellek adresini elde eder. & operatörü ile elde edilen adresin tür bileşeni operandı olan nesnenin türüyle aynı olan türdendir. Sayısal bileşeni ise 
    nesnenin bellekteki doğrual adresidir. Tabii nesne bellekte bir byte'tan daha uzun yer kaplıyorsa onun en düşük anlamlı adresi doğrusal adresi olur. 

    Bir nesnenin adresini aldığımızda biz onu aynı türden bir göstericiye yerleştirebiliriz. Örneğin:

    int a;
    int *pi;

    pi = &a;	/* geçerli */

    Burada &a ile elde edilen adres bilgisinin tür bileşeni int biçimdedir. O zaman bizim bu adresi int türden bir göstericiye atamamız gerekir. Tabii bu 
    atamadan sonra pi göstericisi aslında adresin sayısal bileşenini tutar durumda olur. Örneğin:

    char a;
    int *pi;

    pi = &a;		/* geçersiz! char türden bir adres bilgisi int türden göstericiye atanmış */

    Örneğin:

    int a;
    int b;

    a = &b;			/* geçersiz! bir adres bilgisi int bir nesneye atanamaz! */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir göstericinin içerisinde bir adresin bulunması o göstericinin o adresi gösterdiği anlamına gelmektedir. Yani biz "falanca gösterici şu adresi gösteriyor"
    dediğimizde anlaşılması gereken şey o göstericinin içerisinde o adresin bulunduğudur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de adres bilgileri sembolik olarak "tür *" biçiminde gösterilir. Örneğin:

    int a;

    &a ifadesinin türü "int *" biçiminde ifade edilir. "int *" demek int türden bir adres bilgisi demektir. Örneğin:

    int a;
    int *pi;

    Burada a'nın türü int, pi'nin türü int * biçimindedir. Buradaki '*' adres anlamına gelmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        35. Ders 06/10/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de diziler onların bütün elemanlarından oluşan bileşik bir nesne gibi ele alınmaktadır. Örneğin:

    char s[10];

    Burada s dizisi 10 elemalı ve char türdendir. C'de bir dizinin ismi bir ifade içeerisinde kullanıldığında otomatik olarak derleyici tarafından 
    o dizinin başlangıç adresine dönüştürülür. Yani dizi isimleri C'de dizilerin başlangıç adreslerini belirtmektedir. Dizi isimleri ile belirtilen 
    adreslerin tür bileşeni dizi türü ile aynı olan türdendir. Sayısal bileşeni ise dizinin bellekteki başlangıç adresidir. Diziler elemanları ardışıl bulunduğuna 
    göre ve dizinin elemanları ilk eleman düşük adreste olacak biçimde yerleştirildiğine göre aslında bir dizinin ismi yani dizinin adresi aynı zamanda
    dizinin ilk elemanının adresidir. Örneğin:

    int a[10];

    Burada a ifadesi tamamen &a[0] aynı anlamdadır. a adresi int türden bir adres belirtir. O halde C'de dizi isimleri aynı türden göstericlere atanabilir. 
    Örneğin:

    int a[10];
    int *pi;

    pi = a;		/* geçerli */

    Örneğin:

    char s[10];
    int *pi;

    pi = s;		/* geçersiz! */

    Burada göstericiye farklı türden bir adres bilgisi atanmıştır. Normal nesnelerin adreslerini & operatörüyle almaktayız. Ancak dizi isimleri zaten 
    adres belirtmektedir. Dolayısıyla dizi isimlerine & operatörünü uygulamamalıyız. (Aslında bu başka bir anlama gelmektedir.)

    C'de dizi isimleri nesne belirtmemektedir. Biz bir dizi ismini kullandığımızda adeta derleyici o dizi ismini bir adres sabitine dönüştürmektedir. 
    Örneğin:

    int a[3];

    Burada a[0], a[1], a[2] birer nesne belirtir. Ancak a bir nesne belirtmez. Yani a için bir yer ayrılmamaktadır. a ifadesi tüm diziyi temsil etmektedir. 

    a = 10;		/* geçersiz! a bir nesne belirtmiyor */

    Örneğin:

    char s[3];
    char *pc;

    Burada s dizisinin bellekte 1B12C0 adresinden itibaren yerleştirildiğini varsayalım:

    pc = s;

    Aslında bu işlemde derleyici aşağıdaki gibi bir kod üretmektedir:

    pc = (char *)0x1B12C0;


    char *pc;
    char *p2;

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de önemli bir adres operatörü de "* (indirection)" operatördür. Bu operatörün çarpma işlemini yapan * operatörü ile bir ilgisi yoktur. Tamamen 
    farklı bir operatördür. * operatörü tek operandlı önek bir adres operatörüdür. * operatörünün operandı bir adres bilgisi olmak zorundadır. * operatörü 
    openadı olan adresteki nesneye erişimi sağlar. * operatörü ile erişilen nesnenin türü operand olarak kullanılan nesnenin türü ile aynı türdendir. 
    Örneğin:

    int a = 10;
    int *pi;

    pi = &a;

    Burada pi'nin içerisinde a nesnesinin adresi vardır. Şimdi biz *pi dediğimizde pi adresindeki int nesneye erişmiş oluruz. Yani *pi ile a tamamen 
    aynı nesneyi belirtmektedir. *pi ifadesi burada int türdendir. Çünkü pi adresi int türden bir adres bilgisidir. Böylece biz bir nesnenin adresini 
    alıp onu bir göstericiye yerleştirdikten sonra o göstericiyi * operatörü ile kullandığımızda adresini aldığımız nesneye erişmiş oluruz. Örneğ.n:

    int a;
    int *pi;

    pi = &a;

    Burada artık *pi ile a aynı nesnelerdir. Bu nesneye a ifadesi ile erişmekle *pi ifadesi ile erişmek arasında hiçbir farklılık yoktur. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 10;
    int *pi;

    pi = &a;
    
    printf("%d\n", *pi);		/* 10 */
    *pi = 20;			
    printf("%d\n", a);			/* 20 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizinin ismi o dizinin bellekteki adresini (yani ilk elemanının adresini) belirtiyordu. O halde biz dizinin ismini aynı türden bir gösteriye atayıp
    o göstericiyi * operatörü ile kullanırsak dizinin ilk elemanına erişmiş oluruz. Örneğin:

    int a[] = {10, 20, 30};
    int *pi;

    pi = a;

    Burada *pi aslında a[0] nesnesidir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[] = {10, 20, 30};
    int *pi;
    
    pi = a;

    printf("%d\n", *pi);		/* 10 */

    *pi = 100;

    printf("%d\n", a[0]);		/* 100 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    * adres operatörüne İngilizce "indirection" operatörü denilmektedir. Biz bu operatöre Türkçe "içerik operatörü de diyeceğiz". İçerik operatörünün 
    operandının bir adres bilgisi olması gerekir. Örneğin:

    int a = 0x1FC12D;

    *a = 10;		/* geçersiz! * operatörünün operandı adi bir int, bir adres bilgisi değil */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir adres bilgisi ile tamsayı türlerine ilişkin bir bilgi toplanabilir. Bir adres bilgisinden tamsayı türlerine ilişkin bir bilgi çıkartılabilir. 
    Yani p bir adres bilgisi i de bir tamsayı belirtmek üzere p + i ya da i + p işlemi geçerlidir. p - i işlemi geçerlidir, ancak i - p işlemi geçerli değildir. 
    Adres bilgileriyle tamsayı türlerine ilişkin bilgiler çarpılıp bölünemezler. Bir adres bilgisi bir tamsayı ile toplandığında ya da çıkartıldığında elde edilen ürün
    aynı türden bir adres bilgisi olur. Bir adres bilgisi 1 artırılıdğında adresin sayısal bileşeni adresin türünün uzunluğu kadar artmektedir. Benzer biçimde bir adres 
    bilgisinden 1 çıkartıldığında adresin sayısal bileşeni adresinin türünün uzunluğu kadar eksiltilir. Örneğin pi int türden bir adres bilgisini temsil etsin. 
    İlgili sistemde de int türünün 4 byte olduğunu varsayalım. pi + 1 ifadesi ile edilen adresin sayısal bileşeni pi'nin sayısal bileşeninden 4 fazla olacaktır. 
    Örneğin pc char türden bir adres belirtiyor olsun. pc + 1 işleminden elde edilen adresin sayısal bileşeni pc adresinin sayısal bileşeninden 1 fazla olacaktır. 
    Çünkü char 1 byte uzunluktadır. Benzer biçimde pi bir gösterici ise ++pi işlemi sonucunda pi'nin içerisinde adresin sayısal bileşeni 4 artacaktır. Yani pi bir 
    sonraki int nesneyi gösterir duruma gelecektir. Örneğin:

    int a[] = {10, 20, 30};
    int *pi;

    pi = a;

    Burada pi dizinin ilk elemanını göstermektedir. Biz *pi'yi yazdırırsak 10 görürüz. pi'yi 1 artıralım:

    ++pi;

    Şimdi pi'nin içerisindeki adresin sayısal bileşeni 4 artmış olacaktır. Şimdi *pi'yi yazdırırsak 20'yi göreceğiz. Çünkü dizi elemanları ardışıl olmak zorunddır. 
    Yani bizim pi'yi 1 artırdığımızda dizinin sonraki elemanına erişebilmemiz için dizi elemanları arasında hiç boşluk olmayacağını garanti etmiş olmamız gerekir. Örneğin:

    int a = 10, b = 20, c = 30;
    int *pi;

    pi = &a;

    Burada a, b ve c nesnelerinin bellekte peşi sıra dizilmelerinin hiçbir garantisi yoktur. Dolayısıyla burada biz pi'yi artırarak b ve c'ye erişemeyiz. 
    Ancak dizilerde bu ardışıllık garanti edilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[] = {10, 20, 30};
    int *pi;

    pi = a;

    printf("%d\n", *pi);		/* 10 */
    ++pi;
    printf("%d\n", *pi);		/* 20 */
    ++pi;
    printf("%d\n", *pi);		/* 30 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir yazıyı char türden bir dizinin içerisine yerleştirip dizinin başlangıç adresini de char türden bir göstericiya atayabiliriz. Bu durumda göstericiyi 
    artıra artıra yazının karakterlerine erişebiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[] = "ankara";
    char *pc;

    pc = s;

    putchar(*pc);		/* a */
    ++pc;
    putchar(*pc);		/* n */
    ++pc;
    putchar(*pc);		/* k */
    ++pc;
    putchar(*pc);		/* a */
    ++pc;
    putchar(*pc);		/* r */
    ++pc;
    putchar(*pc);		/* a */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii yukarıdaki örneği bir döngü içerisinde de yapabilirdik.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[] = "ankara";
    char *pc;

    pc = s;
    while (*pc != '\0') {
        putchar(*pc);
        ++pc;
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de adres ile ilgili işlem yapan 4 operatör vardır: &, *, [] ve -> operatörleri. Biz burada biraz daha ayrıntılı olarak bu operatörleri inceleyeceğiz. 
    Ancak -> operatörü "yapılar (structures)" konusu ile ilgili olduğu için onu yapılar konusunda göreceğiz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    & operatörü tek operandlı önek (unary prefix) bir adres operatörüdür. Bu operatör operandı olan nesnenin bellek adresini verir. Daha önceden de belirtildiği gibi
    & operatörü ile elde edilen adresin tür bileşeni operand olan nesnenin türü ile aynı olan türden, sayısal bileşeni ise operand olan nesnenin bellekteki doğrusal adresinden 
    oluşmaktadır. Biz bir nesnenin adresini aldığımızda onu aynı türden bir göstericiye yerleştirebiliriz. Örneğin:

    int a;
    int *pi;
    char *pc;

    pi = &a;
    pc = &a;		/* geçersiz! */

    & operatörü öncelik tablosunda tablonun ikinci düzeyinde sağdan sola grupta bulunmaktadır:

    
    ()					Soldan-Sağa
    + - ++ -- !	& (tür)	Sağdan-Sola
    * / %				Soldan-Sağa
    + -					Soldan-Sağa
    <  > <= >=			Soldan-Sağa
    != ==				Soldan-Sağa
    &&					Soldan-Sağa
    ||					Soldan-Sağa
    ?:					Sağdan-Sola
    =, +=, /=, *=,...	Sağdan-Sola
    ,					Soldan-Sağa

    Örneğin a bir nesne belirtmek üzere &a + 1 gibi bir ifadede önce a'nın adresi alınır, sonra bu adrese 1 toplanır. & operatörünün operandının bir nesne 
    belirtmesi gerekir. Çünkü yalnızca nesnelerin adresleri vardır. Örneğin &10 ifadesi geçersizdir. Çünkü operand olan 10 bir nesne belirtmez. Örneğin &(a + 1)
    Burada a + 1 ifadesi bir nesne belirtmez yani sol taraf değeri (lvalue) değildir. Bu nedenle biz a'nın adresini alabiliriz ancak a + 1'in adresini alamayız. 
    Dizi isimleri zaten adres belirtmektedir. Dizi isimlerine yeniden & operatörü uygulanmaz. (Ancak C'de aslında dizi isimlerinin adresleri alınabilir. Ancak bu durum
    tamamen farklı bir anlam ifade etmektedir. İleride ele alınacaktır.)
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    * (indirection) operatörü tek operandlı önek bir adres operatörüdür. Bu operatörün operandı bir adres bilgisi olmak zorundadır. Operatör operandı olan 
    adresteki nesneye erişmekte kullanılır. * operatörü ile erişilen nesnenin türü operandı olan adresin türüyle aynı türdendir. Yani örneğin *p işleminde 
    elde edilen nesne p adresi hangi türdense o türden olacaktır. * operatörü de öncelik tablosunun ikinci düzeyinde sağdan sola grupta bulunmaktadır. 

    ()					Soldan-Sağa
    + - ++ -- !	& *		Sağdan-Sola
    * / %				Soldan-Sağa
    + -					Soldan-Sağa
    <  > <= >=			Soldan-Sağa
    != ==				Soldan-Sağa
    &&					Soldan-Sağa
    ||					Soldan-Sağa
    ?:					Sağdan-Sola
    =, +=, /=, *=,...	Sağdan-Sola
    ,					Soldan-Sağa

    * operatörünün operandı bir adres bilgisi olmak zorunadır. Örneğin göstericiler, dizi isimleri birer adres belirtmektedir:

    int a[] = {10, 20, 30}

    Bırada *a bu dizinin ilk elemanını belirtir. 

    a bir nesne belirtmek üzere *&a işleminde öce & operatörü sonra * operatörü yapılacaktır. Çünkü bu iki operatör sağdan sola aynı öncelik grubundadır. 
    O halde *&a ile a arasında hiçbir farklılık yoktur. Yani biz bir nesnenin adresini alıp ona nesneye erişirsek aynı nesneyi elde ederiz. Örneğin:

    int a = 0x1FC20D;

    printf("%d\n", *a);		/* geçersiz! * operatörünün operandı adres bilgisi değil adi bir int */

    Tabii adres sabitleri de adres belirttiğine göre onlara da * operatörü uygulanabilir. Örneğin *(int *)0x1FCD0 burada bellekte 1FCD0 adresinden başlayan 
    4 byte (int türünün 4 byte olduğunu varsayıyoruz) int olarak değerlendirilip oryaa erişilecektir. Tabii aslında bizim bellekte rastegele bölgelere bu yolla 
    erişmememiz gerekir. Bu konu ileride "gösteri hataları" başlığı ile ele alınacaktır.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 10;
    int b[] = {10, 20, 30};

    printf("%d\n", *&a);	/* 10 */

    *&a = 20;

    printf("%d\n", a);		/* 20 */ 

    printf("%d\n", *b);		/* 10 */
    *b = 100;			
    printf("%d\n", b[0]);	/* 100 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Dizi elemanlarına erişmekte kullandığımız [] aslında bir adres operatörüdür. Köşeli parantez operatörü tek operandlı sonek (unary postfix) bir operatördür. 
    p[n] ifadesi tamamen *(p + n) ile eşdeğerdir. Yani p[n] "p adresinden n ilerinin içeriği" anlamına gelmektedir. Tabii burada p adresinden n ileri demekle 
    p adresinden n byte ileriyi kastetmiyoruz. p adresinden n * p'nin türünün uzunluğu kadar byte ilerinin içeriğini kastediyoruz. [] operatöründe köşeli parantez 
    içerisindeki ifadenin tamsayı türlerine ilişkin olması gerekir. [] opeatörü öncelik tablosunun en yukarısında soldan öncelikli grupta bulunmaktadır:

    () []						Soldan-Sağa
    + - ++ -- !	& *	(tür)		Sağdan-Sola
    * / %						Soldan-Sağa
    + -							Soldan-Sağa
    <  > <= >=					Soldan-Sağa
    != ==						Soldan-Sağa
    &&							Soldan-Sağa
    ||							Soldan-Sağa
    ?:							Sağdan-Sola
    =, +=, /=, *=,...			Sağdan-Sola
    ,							Soldan-Sağa

    [] operatörünün operandı bir adres bilgisi olmak zorundadır. Yani operand örneğin bir gösterici olabilir, bir dizi ismi olabilir. Biz daha önce [] 
    operatörünü dizi elemanlarına erişmekte kullanmıştık. Örneğin a[i] ifadesini a dizisinin i'inci indisli elemanına erişmek için kullanmıştık. a dizi ismi 
    dizinin başlangıç adresi anlamına geldiğine göre a[i] ifadesi tamamen *(a + i) ile eşdeğerdir. Tabii [] operatörünü biz daha önce hep dizi ismiyle kullanıştık. 
    Aslında bu operatörün operandı herhangi bir adres bilgisi olabilir. Örneğin [] operatörünü bir gösterici ile kullanabiliriz:

    int a[] = {10, 20, 30, 40, 50};
    int *pi;

    pi = a;

    Örneğin burada a[3] ile pi[3] arasında hiçbir farklılık yoktur. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[] = {10, 20, 30, 40, 50};
    int *pi;

    for (int i = 0; i < 5; ++i) 
        printf("%d %d\n", a[i], *(a + i));

    pi = a;

    printf("%d\n", pi[3]);		/* 40 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii aslında [] operatöründe köşeli parantezler içerisindeki ifade negatif olabilir. Örneğin pi[-2] gibi bir ifade tamamen normaldir. Bu işlem *(pi - 2)
    anlamına gelmektedir. Yani biz burada pi'nin belirttiği adresten iki önceki elemana erişmiş oluruz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[] = {10, 20, 30, 40, 50};
    int *pi;

    pi = a + 3;
    printf("%d\n", *pi);		/* 40 */

    printf("%d\n", pi[-2]);		/* 20 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    p bir adres belirtmek üzere p[0] ile *(p + 0) ve *p tamamen aynı anlamdadır. Yani örneğin biz a isimli bir dizinin ilk elemanına a[0] ifadesi ile de *a 
    ifadesi ile de erişebiliriz.

    p bir adres belirtmek üzere *(p + n) ile *p + n tamaen farklı anlamlara gelmektedir. *(p + n) ifadesinde önce parantez içi yapılacak ve p adresinden n ilerideki 
    adres elde edilecektir. Sonra * operatörü ile bu adresin içeriği elde edilecektir. Halbuki *p + n ifadesinde önce *p ile p adresindeki nesneye erişilecek 
    o nesnenin değeri n ile toplanacaktı.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de aslında [] operatörünün operand'ları yer değiştirebilmektedir. Yani p[n] ifadesi aslında n[p] biçiminde de yazılabilmektedir. Bu çok az bilinen 
    bir özelliktir. Zaten programcılar tarafından hiç kullanılmaz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[] = {10, 20, 30, 40, 50};

    printf("%d\n", a[2]);		/* 30 */
    printf("%d\n", 2[a]);		/* 30 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    p bir adres belirtmek üzere örneğin ++*p gibi bir ifadede biz asılında *p'yi bir artırmış oluruz. Yani bu ifade *p = *p + 1 ile aynı anlamdadır. 
    Ancak *++p ifadesinde biz önce p göstericisinin içerisinde adresi bir artırıp (bir byte değil) sonra artırılmış adresteki nesneye erişiriz. p bir adres 
    belirtmek üzere &*p ifadesinde önce p adresindeki nesneye erişilip sonra onun adresi alınmıştır. Bu da tabii p adresiyle aynıdır. a bir nesne belirtmek üzere
    *&a ifadesi de daha önce belirttiğimiz gibi a ile aynı anlamdadır. 

    [] operatörünün & operatöründen daha öncelikli olduğuna dikkat ediniz. Örneğin &a[n] ifadesi a adresinden n ilerinin içeriğinin adresi anlamına gelmektedir. Bu ifade
    &*(a + n) ifadesi ile eşdeğer olduğuna göre aslında a + n ile de eşdeğerdir. Yani a adresinden n ilerinin içeriğinin adresi aslında a adresinden n ilerinin adresi aynı anlamdadır. 
    Örneğin:

    int a = 10;

    &a[0] = 20;		/* geçersiz */

    Buarad [] operatörü önceliklidir. Dolayısıyla [] operatörünün operandı adres bilgisi olmadığı için ifade geçersizdir. İfadeyi şöyle düzeltelim:

    (&a)[0] = 20;		/* geçerli */

    Bu ifade geçerlidir. Burada a'ya 20 atanmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        36. Ders 11/10/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki gibi bir gösterici bildirimi olsun:

    int *pi;

    Bu bildirimden iki şey anlaşılmaktadır. Birincisi pi nesnesi int * türündendir. Burada "int *" int türden adres bilgisi anlamına gelir. (pi'yi 
    parmağınızla kapatıp sola bakın). İkincisi *pi yani pi'nin gösterdiği yer int türdendir. (*pi'yi parmağınızla kapatıp sola bakın).

    T türünden adres türü C'de T * biçiminde temsil edilmektedir. T1 türünden T2 türüne otomatik dönüştürme olması T1 * türünden T2 * türüne otomatik dönüştürme olacağı 
    anlamına gelmez. Örneğin int türünden double türüne otomatik dönüştürme vardır. Ancak int * türünden double * türüne otomatik dönüştürme 
    yoktur. Bir adres bilgisini ancak aynı türden bir göstericiye atayabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir göstericiye ilkdeğer verebiliriz. Tabii verilen ilkdeğerin göstyerici ile aynı türden bir adres bilgisi olması gerekir. Örneğin:

    int a;
    int *pi = &a;		/* geçerli */

    Tabii burada verilen ilkdeğer pi'nin içerisine yerleştirilmektedir. *pi'ye yerleştirilmemektedir. Zaten buradaki * bir operatör görevinde 
    değildir dekleratörün bir parçasıdır. Örneğin:

    int a[] = {10, 20, 30, 40, 50};
    int *pi = a;		/* geçerli */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun parametre değişkeni bir gösterici olabilir. Bu durumda fonksiyon aynı türden bir adres bilgisi ile çağrılmalıdır. Örneğin:

    void foo(int *pi)
    {
        /* ... */
    }
    ...
    int a;
    foo(&a);		/* geçerli */
    foo(a);			/* geçersiz */

    Bir fonksiyonun parametre değişkeni bir gösterici ise biz de o fonksiyonu aynı türden bir nesnenin adresi ile çağırmışsak fonksiyonun içerisinde 
    * operatörü kullanıldığında biz aslında adresini aldığımız nesneye erişiriz. İşte bir fonksiyonun başka bir fonksiyonun yerel değişkenini değiştirebilmesi 
    için onun adresiniş alması gerekir. 
    
    Bir fonksiyonu bir değrle çağırmaya İngilizce "call by value", bir adresle açğırmaya "calal by reference" denilmektedir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void foo(int *pi)
{
    *pi = 20;
}

int main(void)
{  
    int a = 10;

    printf("%d\n", a);      /* 10 */

    foo(&a);     

    printf("%d\n", a);      /* 20 */
   
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İki değişken içerisindeki değeri yer değiştiren swap isimli bir fonksiyon yazmak isteyelim. Bu fonksiyonu aşağıdaki gibi yazamayız:

    void swap(int x, int y)
    {
        int temp = x;
        x = y;
        y = temp;
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void swap(int x, int y)
{
    int temp = x;
    x = y;
    y = temp;
}

int main(void)
{  
    int a = 10, b = 20;

    printf("a = %d b = %d\n", a, b);

    swap(a, b);

    printf("a = %d b = %d\n", a, b);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bu fonksiyonu yazabilmek için nesnelerin adreslerini parametre olarak almak gerekir. Örneğin:

    void swap(int *x, int *y)
    {
        int temp = *x;
        *x = *y;
        *y = temp;
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void swap(int *x, int *y)
{
    int temp = *x;
    *x = *y;
    *y = temp;
}

int main(void)
{  
    int a = 10, b = 20;

    printf("a = %d b = %d\n", a, b);

    swap(&a, &b);

    printf("a = %d b = %d\n", a, b);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Şimdi artık scanf fonksiyonun neden neden nesnenin adresini aldığını anlayabiliriz. Eğer scanf nesnenin adresini almasaydı o nesnenin içerisine 
    bir şey yerleştiremezdi. Bir fonksiyonun bizim yerel değişkenimize bir şey yerleştirebilmesi için bizim değişkenimizin adresini 
    alması gerekir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>


int main(void)
{  
    int a;

    printf("Bir deger giriniz:");
    scanf("%d", &a);		/* scanf a'nın adresini alarak oraya değeri yerleştirmektedir */

    printf("%d\n", a * a);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir dizinin fonksiyona parametre yoluyla aktarılması tipik olarak iki parametre ile yapılmaktadır. Fonlsiyonun parametrelerinden biri bir gösterici 
    olur. Bu gösterici dizinin başlangıç adresini alır. Diğer parametre de int, unsigned int gibi tamsayı türlerine ilişkin bir türden olur. Bu parametre de 
    dizinin uzunlupunu alır. Böylece fonksiyon dizinin başlangıç adresini ve uzunluğunu aldığında o göstericiyi artırarak dizinin elemanlarının hepsine 
    erişebilir. Tabii bu biçiminde aktarımın mümkün olmasının asıl nedeni dizi elemanlarının ardışıllığıdır. Fonksiyona dizinin uzunluğunun da geçirilmesinin 
    nedeni fonksiyonun dizinin sonunu tespit eddebilmesi içindir. 

    Aşağıdaki int bir dizinin elemanlarını yazdıran disp isimli fonksiyon örnek olarak verilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void disp(int *pi, int size)
{
    for (int i = 0; i < size; ++i) {
        printf("%d ", *pi);
        ++pi;
    }
    printf("\n");
}

int main(void)
{  
    int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
    int b[5] = {100, 200, 300, 400, 500};

    disp(a, 10);
    disp(b, 5);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki örnekte dizi elemanlarına [] operatörü ile de erişebiliriz. Aslında bu tür fonksiyonlarda * yerine daha çok [] operatörü tercih edilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void disp(int *pi, int size)
{
    for (int i = 0; i < size; ++i)
        printf("%d ", pi[i]);
    printf("\n");
}

int main(void)
{  
    int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
    int b[5] = {100, 200, 300, 400, 500};

    disp(a, 10);
    disp(b, 5);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizinin uzunluğunun int bir tür ile ifade edilmesi bazı sistemlerde yetersiz kalabilir. Örneğin bir sistemin bellek büyüklüğü int sınırlarını
    aşıyor olabilir. Bu durumda çok büyük dizilerin uzunluklarını int türü ile ifade edemeyiz. Bu tür durumlarda unsigned int, long, unsigned long gibi türler
    denenebilir. Tabii bir bir sistemde açılabilecek maksimum dizi uzunluğu o sistemde bellek büyüklüğü ile ilgilidir. Bunu da ancak derleyicileri yazanlar
    biliyor olabilirler. İşte C'de ilgili sistemdeki bellek büyüklüğünü etkin bir biçimde temsil edebilmek için size_t isimli bir tür ismi düşünülmüştür. 
    size_t aslında bir tür değildir. Başka bir türün alternatif bir ismidir. Bu biçimdeki alternatif isimler typedef bildirimi ile oluşturulurlar. 
    Biz kursumuzda ileride typedef bildirimlerini göreceğiz. size_t türünün typedef bildirimleri <stdio.h>, <stdlib.h>, <stddef.h> , <string.h> gibi 
    dosyalarda yapılmış durumdadır. Yani bu tür ismini kullanabilmemeiz için bu dosyalardan birini include etmiş olmamız gerekir. size_t bir anahtar sözcük değildir. 
    Bir değişken olan sembolik bir isimdir. C standartlarına göre size_t işaretsiz bir tamsayı türü olmak üzere derleyicileri yazanlar tarafından 
    typedef edilmiş bir tür olmak zorundadır. Aslında programcının size_t türünün hangi tür olarak belirlediğini programcının bilmesine gerek yoktur.

    İşte C'de dizi uzunlukları da genellikle programcılar tarafından size_t türü ile temesil edilmektedir. Biz de kursumuzda her ne kadar henüz typedef 
    işlemlerini görmemiş olsak da bu size_t türünü kullanacağız. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void disp(int *pi, size_t size)
{
    for (size_t i = 0; i < size; ++i)
        printf("%d ", pi[i]);
    printf("\n");
}

int main(void)
{
    int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
    int b[5] = {100, 200, 300, 400, 500};

    disp(a, 10);
    disp(b, 5);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıda int bir dizinin en büyük elemanını bulan bir fonksiyon örneği verilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

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(void)
{
    int a[10] = {45, 23, 11, 67, 21, 7, 32, 76, 22, 47};
    int max;

    max = getmax(a, 10);
    printf("%d\n", max);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıda double bir dizinin ortalamasına geri dönen mean isimli bir fonksiyon örneği verilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

double mean(double *pd, size_t size)
{
    double total;

    total = 0;
    for (size_t i = 0; i < size; ++i)
        total += pd[i];

    return total / size;
}

int main(void)
{
    double a[5] = {1, 2, 3, 4, 5};
    double result;

    result = mean(a, 5);
    printf("%f\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir göstericiye aynı türden bir adres bilgisinin atanabildiğini anımsayınız. Bu durumda int bir dizinin en büyük elemanına geri dönen aşağıdaki 
    gibi bir fonksiyon olsun:

    int getmax(int *pi, size_t size);

    Burada bu fonksiyon int bir dizinin en büyük elemanını bulabilir, long, double gibi türşere ilişkin dizilerin en büyük elemanlarını bulamaz. 
    Çünkü örneğin double bir türden dizinin adresi double türden adres belirtir. Halbuki fonksiyonun parametresi int türden bir göstericidir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki int türden bir diziyi bubble sort algoritmasıyla sıraya dizen bir fonksiyon örneği verilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void bsort(int *pi, size_t size);

int main(void)
{
    int a[10] = {2, 56, 11, 1, 58, 23, 32, 43, 67, 15};

    bsort(a, 10);

    for (size_t i = 0; i < 10; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

void bsort(int *pi, size_t size)
{
    int temp;
    int flag;
    size_t i;
 
    i = 0;
    do {
        flag = 0;
        for (size_t k = 0; k < size - 1 - i; ++k) {
            if (pi[k] > pi[k + 1]) {
                flag = 1;
                temp = pi[k];
                pi[k] = pi[k + 1];
                pi[k + 1] = temp;
            }
        }
        ++i;
    }  while (flag == 1);
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıda int bir diziyi ters çeviren bir fonksiyon örneği verilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void reverse(int *pi, size_t size);

int main(void)
{
    int a[10] = {2, 56, 11, 1, 58, 23, 32, 43, 67, 15};

    for (size_t i = 0; i < 10; ++i)
        printf("%d ", a[i]);
    printf("\n");

    reverse(a, 10);

    for (size_t i = 0; i < 10; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

void reverse(int *pi, size_t size)
{
    for (size_t i = 0; i < size / 2; ++i) {
        int temp = pi[i];
        pi[i] = pi[size - 1 - i];
        pi[size - 1 - i] = temp;
    }
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki örneği yazarken iki int değeri yer değiştiren swap fonksiyonundan da faydalanabilirdik.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void reverse(int *pi, size_t size);

int main(void)
{
    int a[10] = {2, 56, 11, 1, 58, 23, 32, 43, 67, 15};

    for (size_t i = 0; i < 10; ++i)
        printf("%d ", a[i]);
    printf("\n");

    reverse(a, 10);

    for (size_t i = 0; i < 10; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

void swap(int *pi1, int *pi2)
{
    int temp = *pi1;
    *pi1 = *pi2;
    *pi2 = temp;
}

void reverse(int *pi, size_t size)
{
    for (size_t i = 0; i < size / 2; ++i)
        swap(&pi[i], &pi[size - 1 - i]);
}

/*----------------------------------------------------------------------------------------------------------------------
    [] operatörünün * ve & operatörlerinden daha öncelikli olduğunu anımsayınız. Bu durumda p bir adres belirtmek üzere &p[n] ile p + n aynı anlamdadır. 
    Yani p adresinden n ilerinin içeriğinin adresi aslında p adresindne n ilerinin adresidir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yazıların fonksiyonlara parametre yoluyla aktarılması içimn tipik olarak fonksiyonun parametre değişkeni char tüdrden bir gösterici olur. Fonksiyon da
    yazının başlangıç adresiyle çağrılır. Yazının uzunluğunun fonksiyona geçirilmesine gerek yoktur. Çünkü yazının sonunda zaten null karakter vardır. 
    Fonksiyon da null karakter görene kadar yazının tüm karakterlerini elde edebilir. 

    Örneğin aslında puts fonksiyonun prototipi şöyledir:

    void puts(char *str);

    Fonksiyon yazının başlangıç adresini alır null karakter görene kadar tüm karakterleri yan yana yazdırır.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void myputs(char *str)
{
    while (*str != '\0') {
        putchar(*str);
        ++str;
    }
    putchar('\n');
}

int main(void)
{
    char s[] = "ankara";

    myputs(s);

    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
                                                            37. Ders 13/10/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    str bir yazıyı gösteren bir gösterici olmak üzere null karakter görene kadar ilerleyen döngü iki biçimde oluşturulabilir:

    1) Göstericiyi artırarak

    while (*str != '\0') {
        /* ... */
        ++str;
    }

    2) [] operatör ile 

    for (size_t i = 0; str[i] != '\0'; ++i) {
        /* ... */
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void myputs(char *str);

int main(void)
{
    char s[] = "ankara";

    myputs(s);
    myputs(s + 2);

    return 0;
}

void myputs(char *str)
{
    for (size_t i = 0; str[i] != '\0'; ++i)
        putchar(str[i]);
    putchar('\n');
}

/*----------------------------------------------------------------------------------------------------------------------
    Şimdi bir yazıyı tersten yazdıran putsrev isimli bir fonksiyon yazalım. Biz yazıda önce null karakter görene kadar ilerleriz. Sonra geri geri giderek
    karakterleri yazdırırız. Ancak kullanacağımız indisin türü konusunda dikkat ediniz. size_t türü her ne kadar dizi uzunlukları, indeksleri için 
    uygun bir türse de C standartlarına göre size_t işaretsiz bir tamsayı türü olarak typedef edilmektedir. İşaretsiz bir tamsayı türünden bir nesnenin içerisinde
    0 varsa biz bu değerden 1 çıkartırsak o türün en büyük pozitif tamsayı değerini elde ederiz. 

    Aşağıdaki örneği int yerine size_t kullanarak deneyiniz ve problemi belirlemeye çalışınız.  
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void putsrev(char *str);

int main(void)
{
    char s[] = "ankara";

    putsrev(s);

    return 0;
}

void putsrev(char *str)
{
    int i;

    for (i = 0; str[i] != '\0'; ++i)
        ;
    for (--i; i >= 0; --i)
        putchar(str[i]);

    putchar('\n');
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki örnekte i değişkeni int değil de size_t türünden yapılırsa oluşacak sorun aşağıdaki gibi giderilebilir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void putsrev(char *str);

int main(void)
{
    char s[] = "ankara";

    putsrev(s);

    return 0;
}

void putsrev(char *str)
{
    size_t i;

    for (i = 0; str[i] != '\0'; ++i)
        ;
    if (i > 0) {
        for (--i; i > 0; --i)
            putchar(str[i]);

        putchar(str[i]);
    }
    putchar('\n');
}

/*----------------------------------------------------------------------------------------------------------------------
    İşaretsiz bir tamsayı türü ile işaretli bir tamsayı türünü iki operandlı bir operatörler (karşılaştırma operatörleri dahil) işleme soktuğumuzda 
    dönüştürmenin işaretsiz türe doğru yapılacağını belirtmiştik. O halde yukarıdaki problem aşağıdaki gibi de çözülebilir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void putsrev(char *str);

int main(void)
{
    char s[] = "ankara";

    putsrev(s);

    return 0;
}

void putsrev(char *str)
{
    size_t i;

    for (i = 0; str[i] != '\0'; ++i)
        ;

    for (--i; i != -1; --i)
        putchar(str[i]);

    putchar('\n');
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki problem sonek eksiltim ile de çözülebilir. 
----------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>

void putsrev(char *str);

int main(void)
{
    char s[] = "";

    putsrev(s);

    return 0;
}

void putsrev(char *str)
{
    size_t i;

    for (i = 0; str[i] != '\0'; ++i)
        ;

    while (i-- > 0)
        putchar(str[i]);

    putchar('\n');
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun geri dönüş değerinin türü yerine T bir tür belirtmek üzere T * kullanılırsa bu durum fonksiyonun bir adresle geri döndüğü anlamına 
    gelmektedir. Örneğin:

    int *foo(void)
    {
        /* ... */
    }

    Burada foo fonksiyonu int türden bir adres bilgisi ile geri dönmektedir. Ritchi/Kernighan yazım stilinde * atomu fonksiyon ismine bitiştirilmektedir. 
    Ancak bazı programcılar bu tür durumlarda * atomunu tür ile bitiştirirler. 
    
    Örneğin:

    char *bar(void)
    {
        /* ... */
    }

    Burada bar fonksiyonun geri dönüş değeri char değildir. char türden bir adres bilgisidir. Tabii böyle fonksiyonları çağırdıktan sonra onların geri 
    dönüş değerlerini aynı türden bir göstericiye atayabiliriz. Örneğin:

    int *pi;
    char *pc;

    pi = foo();
    pc = bar();

    Bir fonksiyonun geri dönüş değerinin bir adres olması demek aslında return ifadesinin atanacağı geçici değişkenin bir gösterici olması demektir. 
    O halde geri dönüş değeri adres olan fonksiyonlara aynı türden bir adres değeir ile return uygulamak gerekir. 

    Aşağıdaki bir dizinin en büyük elemanının dizi içerisindeki adresine geri dönen bir fonksiyon örneği verilmiştir. Bu örnekte önce dizinin ilk elemanı en büyük 
    varsayılmıştır. Onun adresi pmax isimli göstericide tutulmuştur. Sonra daha büyük elemanla karşılaşıldığında pmax adresi bu elemanı gösterecek biçimde değiştirilmiştir.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int *getmax_addr(int *pi, size_t size);

int main(void)
{
    int a[10] = {34, 23, 12, 67, 25, 12, 89, 11, 26, 67};
    int *pi;

    pi = getmax_addr(a, 10);
    printf("%d\n", *pi);

    return 0;
}

int *getmax_addr(int *pi, size_t size)
{
    int max = pi[0];
    int *pmax = pi;

    for (size_t i = 1; i < size; ++i)
        if (pi[i] > max) {
            max = pi[i];
            pmax = &pi[i];
        }

    return pmax;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii aslında yukarıdaki örnekteki getmaxx_addr fonksiyonu daha kolay yazılabilir. Şöyle ki, biz zaten en büyük elemanın adresini tutuyorsak
    en büyük elemanı ayrıca tutmamıza gerek yoktur.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int *getmax_addr(int *pi, size_t size);

int main(void)
{
    int a[10] = {34, 23, 12, 67, 25, 12, 89, 11, 26, 67};
    int *pi;

    pi = getmax_addr(a, 10);
    printf("%d\n", *pi);

    return 0;
}

int *getmax_addr(int *pi, size_t size)
{
    int *pmax = &pi[0];

    for (size_t i = 1; i < size; ++i)
        if (pi[i] > *pmax) 
            pmax = &pi[i];

    return pmax;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de hiçbir nesnenin ve fonksiyonun adresi olamayacak kullanılmayan boş bir byte'ın adresi NULL adres olarak belirlenmiştir. Ancak C standartlarına göre 
    NULL adresin sayısal bileşeninin ne olacağı derleyicileri yazanların isteğine bırakılmıştır. Yani değişik sistemlerde NULL adresin sayısal bileşeni 
    farklı olabilir. Derleyicileri yazanlar işletim sistemi tarafından boş bırakılmış olan bir alandaki adresi NULL adres olarak belirleyebilirler. 
    Her ne kadar standartlar NULL adresin sayısal bileşeninin ne olacağını derleyicileri yazanların isteğine bırkmışsa da yaygın sistemlerin hemen hepsinde 
    NULL adres belleğin tepesindeki 0 numaralı adrestir. Windows, UNIX/Linux, ve macOS sistemlerindeki tüm C derleyicileri NULL adresi 0 numaralı adres olarak 
    kullanmnaktadır. 

    C'de NULL adres düz bir 0 sabiti ile ya da 0 değerini veren tamsayı türlerine ilişkin bir sabit ifadesi ile temsil edilmektedir.  (Aynı zamanda 0 değerini veren 
    (void *) türüne dönüştürülmüş tamsayı türlerine ilişkin sabit ifadeleri de NULL adres anlamına gelmektedir.) Biz C'de 0 sabitini bir adresle ilişkilendirdiğimizde
    artık bu 0 sabiti int olan 0 sabiti değil o sistemde derleyicinin belirlediği NULL adres anlamına gelmektedir. NULL adres herhangi türden bir göstericiye
    atanabilir. Biz C'de bir göstericiye 0 atadığımızda o göstericiye int olan 0'ı atamış olmamaktayız. O sistemde NULL adres neyse onu atamış olmaktayız. Örneğin:

    int *pi;

    pi = 0;		/* geçerli */

    Burada pi'ye 0 sayısı atanmamıştır. 0 adresi de atanmamıştır. Çalışılan sistemde NULL adres olarak hangi adres temsil edildiyse o adres atanmıştır. 
    Yukarıda da belirtitğimiz gibi yaygın sistemlerin hepsinde NULL adres gerçekten 0 adresi olarak seçilmiştir. Tabii aslında standartlara göre NULL adres 
    yalnızca 0 sabiti ile değil 0 değerini veren tamsayı türlerine ilişkin sabit ifadeleriyle de oluşturulabilir. Örneğin:

    int *pi = 3 - 3;		/* geçerli, pi'ye o sistemdeki NULL adres atanıyor */

    Tabii programcılar tipik olarak NULL adresi düz 0 sabiti olarak kullanırlar. Örneğin:

    int a = 0;
    int *pi = a;		/* geçersiz! göstericiye int bir değer atanmış */

    Burada göstericiye NULL adres atanmamıştır. Çünkü standartlara göre 0 değerini veren sabit ifadesi NULL adresi temsil etmektedir. Oysa bu örnekte 
    göstericiye bir sabit ifadesi atanmamıştır. 

    Benzer biçimde bir gösterici 0 ile (ya da 0 değerini veren tamsayı türlerine ilişkin bir sabit ifadesi ile) karşılaştırıldığında aslında 
    karşılaştırma göstericinin içerisinde o sistemdeki NULL adresin olup olmadığını anlamaya yönelik yapılmaktadır. Örneğin:

    if (pi == 0) {
        /* ... */
    }

    Burada pi göstericisinin içerisinde 0 adresi olup olmadığına bakılmamaktadır. pi agöstericisinin içerisinde o sistemdeki NULL adresin olup olmadığına bakılmaktadır. 
    Örneğin falanca sistemde NULL adres FFFFFFFF adresi olsun. Ve pi göstericisinin içerisinde bu adresin olduğunu düğünelim bu durumda pi == 0 karşılaştırması 
    doğru yani 1 değerini verecektir. Benzer biçimde bir gösterici != operatörü kullanılarak 0 ile karşılaştırılabilir:

    if (pi != 0) {
        /* ... */
    }

    Burada pi'nin içerisinde o sistemdeki NULL adres yoksa if deyimi doğrudan sapacaktır. 

    if deyiminde (while deyimde de) parantez içerisindeki ifade yalnızca bir adres bilgisinden oluşabilir. Bu durumda karşılaştırma o adres bilgisinin
    o sistemdeki NULL adres olup olmadığına göre yapılır. Örneğin p bir gösterici olsun:

    if (p) {
        /* p NULL adres değilse bu kısım yapılacak */
    }
    else {
        /* p NULL adres ise bu kısım yapılacak  */
    }

    Burada p'nin içerisinde o sistemdeki NULL adres varsa if deyimi yanlıştan, yoksa doğrudan sapar. Örneğin falanca sistemde NULL adres FFFFFFFF olsun. 
    p'nin içerisinde de FFFFFFFF değerinin olduğunu varsayalım. Bu durumda if deyimi yanlıştan sapacaktır. Görüldüğü gibi if parantezi içerisinde bir adres 
    bilgisi varsa burada o adresin 0 adresi olup olmadığına değil o sistemdeki NULL adres olup olmadığına bakılmaktadır. Başka bir deyişle:

    if (p) {
        /* ... */
    }

    ile

    if (p != 0) {
        /* ... */
    }

    aynı anlamdadır.

    Bir gösterici (genel olarak adres bilgisi) ! operatörü ile kullanılabilir. Örneğin p bir gösterici olsun !p ifadesi geçerlidir. Bu durumda eğer 
    göstericisinin içerisinde NULL adres varsa bu ifade 1 değerini, yoksa 0 değerini üretir. Örneğin:

    if (!p) {
        /* p NULL ise bu kısım yapılacak */
    }

    Burada p'nin içerisinde NULL adres varsa birtakım şeyler yapılmak istenmiştir. 

    C99 ile birlikte C'ye _Bool isimli bir bool türünün eklendiğini belirtmiştik. İşte bir adres türü doğrudan bool türüne atanabilmektedir. Bu durumda 
    adres NULL adres değilse 1 değeri NULL adres ise 0 değeri _Bool türünden değişkene atanmış olur. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    _Bool b;
    int *pi;

    pi = NULL;

    b = pi;
    printf("%d\n", b);		/* 0 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    NULL adres sabiti okunabilirliği artırmak için bazı başlık dosyalarında da define edilmiştir. Böylece biz NULL adres için 0 kullanmak yerine 
    NULL sözcüğünü kullanabiliriz:

    #define NULL	0

    <stdio.h>, <stdlib.h>, <stddef.h>, <string.h> dosyalarında NULL sembolik sabiti NULL adresin okunabilir kullanımı için define edilmiş durumdadır. Bu sayede 
    örneğin biz:
    
    p = 0;
    
    yerine:
    
    p = NULL;
    
    gibi bir ifade yazabiliriz. Ya da örneğin:

    if (p == NULL) {
        /* ... */
    } 

    Burada yine p'nin NULL adres içerip içermediğine bakılmaktadır. NULL sembolik sabiti int 0 olarak kullanılmamalıdır. NULL sembolik sabitinden amaçlanan
    NULL adresin okunabilir bir biçimde ifade edilmesidir. Aslında standartlara göre yukarıda da belirttiğimiz gibi NULL sembolik sabiti aşağıdaki gibi 
    define edilmiş de olabilir:

    #define NULL	((void *)0)

    Biz henüz void adresleri görmediğimiz için (void *)0 ifadesini açıklamayacağız. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    NULL adres sabitinin tamsayısal sıfır değeri ile temsil edilmesi C'de programcıların kafasını karıştıran bir durumdur. 
    Örneğin 0 sabitinin hem int türden sabit olması hem de göstericilerle ilişkilendirildiğinde NULL adres sabiti anlamına 
    gelmesi özellikle C'yi yeni öğrenenlerde karışıklık duygusu uyandırmaktadır. İşte C++'a C++11 ile birlikte NULL adres
    sabiti için nullptr isimli ayrı bir anahtar sözcük de eklenmiştir. Bu anahtar sözcük C'nin henüz basılmayan ama içerik 
    olarak oluşturulmuş olan C23 versiyonuna da sokulmuştur. Yani C23 ile birlikte artık C'de de bir göstericiye NULL 
    adres sabiti aşağıdaki gibi atanabilecektir:

    int *pi = nullptr;
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        38. Ders 18/10/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de prototipleri <string.h> içerisinde bulunan yazılar üzerinde işlem yapan, ismi str ile başlayan bir grup standart C fonksiyonu vardır. Bunlara string
    fonksiyonları denilmektedir. Bu bölümde bu string fonksiyonlarının önemli olanlarını tanıtacağız. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    strlen fonksiyonu bir yazının uzunluğu ile geri dönen bir string fonksiyondur. Fonksiyonun orijinal prototipi şöyledir:

    size_t strlen(const char *str);

    Fonksiyonun protipindeki const anahtar söczüğünü henüz görmedik. Bu anahtar sözcüğe şimdilik dikkat etmeyiniz. Fonksiyonun geri dönüş değeri size_t türündendir. 
    size_t işaretsiz bir tamsayı türü olmak üzere standart türlerden birini temsil etmektedir. Ancak bu türün hangi işaretsiz tamsayı türünü temsil ettiği derleyicileri 
    yazanların isteğine bırakılmıştır. size_t türü printf fonksiyonu ile yazdırılırkan %z format karakteri kullanılır. Buradaki z'nin yanında d (decimal), x (hex), o (octal)
    karakterlerinden biri getirilmelidir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    size_t result;

    result = strlen(s);
    printf("%zd\n", result);		/* 6 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strlen fonksiyonunu biz de yazabiliriz. Tek yapacağımız şey null karakter görene kadar karakterlerin sayısını hesaplamaktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

size_t mystrlen(char *str)
{
    size_t i;

    for (i = 0; str[i] != '\0'; ++i)
        ;

    return i;
}

int main(void)
{
    char s[] = "ankara";		/* dikkat Türkçe karakterleri editörünüz UTF-8 olarak iki byte halinde kodluyor olabilir */
    size_t result;

    result = mystrlen(s);
    printf("%zd\n", result);		/* 6 görebilirsiniz */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte klavyeden (stdin dosyasından) girilen bir yazının uzunluğu ekrana (stdout dosyasına) yazdırırlmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[1024];

    printf("Bir yazi giriniz:");
    gets(s);

    printf("%zd\n", strlen(s));

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bu tür örneklerde Türkçe karakterleri kullanmayınız. Çünkü Türkçe karakterleri günümüz editörlerinin çoğu UTF-8 olarak kodlamaktadır. Türkçe karakterler
    UTF-8 kodlamasında iki byte yer kaplarlar. Bu nedenle örneğin "ağrı" gibi bir yazı için strlen fonksiyonu uygulanırsa 4 değil 6 değeri bulabilirsiniz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ağrı";		/* dikkat Türkçe karakterleri editörünüz UTF-8 olarak iki byte halinde kodluyor olabilir */
    size_t result;

    result = strlen(s);
    printf("%zd\n", result);		/* 6 görebilirsiniz */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strcpy fonksiyonu char türden bir dizi içerisindeki yazıyı başka bir diziye kopyalamak için kullanılır. Orijinal prototipi şöyledir:

    char *strcpy(char *dest, const char *source);

    Burada const anahtar sözcüğünü henüz görmedik. Fonksiyon ikinci parametresi ile belirtilen adresten başlayarak birincici parametresiyle belirtilen adrese 
    null karakter görene kadar (null karakter de dahil) kopyalama yapar. Fonksiyon birinci parametresiyle verilen adresin aynısına geri dönmektedir. Tabii 
    genellikle bu geri dönüş değerine gereksinim duyulmaz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    char d[1024];

    strcpy(d, s);

    puts(d);		/* ankara */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strcpy fonksiyonu birinci parametresiyle belirtilen yani kopyalamanın yapıldığı hedef adresin aynısına geri dönmektedir. Genellikle programcılar
    bu geri dönüş değerini kullanmazlar. Ancak bazen aşağıdaki gibi kodlarda bu geri dönüş değerinin kullanıldığını görebilirsiniz:

    printf("%s\n", strcpy(d, s));

    Burada strcpy s adresindeki yazıyı d adresinden itibaren kopyalar. d adresinin aynısına geri döndüğü için buradaki yazı aynı zamanda printf ile yazdırılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    char d[1024];

    printf("%s\n", strcpy(d, s));		/* ankara */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strcpy fonksiyonu genellikle düz bir mantıkla programcılar tarafından aşağıdaki gibi yazılmaktadır:

    char *mystrcpy(char *dest, char *source)
    {
        char *temp = dest;

        while (*source != '\0') {
            *dest = *source;
            ++source;
            ++dest;
        }
        *dest = '\0';

        return temp;
    }

    Aslında atama işlemi while parantezi içerisinde yapılabilir. Böylece null karakter de atanmış olur:

    char *mystrcpy(char *dest, char *source)
    {
        char *temp = dest;

        while ((*dest = *source) != '\0') {
            ++source;
            ++dest;
        }

        return temp;
    }

    Tabii aslında en sade yazım aşağıdaki gibi olabilir:

    char *mystrcpy(char *dest, char *source)
    {
        for (size_t i = 0; (dest[i] = source[i]) != '\0'; ++i)
            ;

        return dest;
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

char *mystrcpy(char *dest, char *source);

int main(void)
{
    char s[] = "ankara";
    char d[1024];

    mystrcpy(d, s);
    printf("%s\n", d);

    return 0;
}

char *mystrcpy(char *dest, char *source)
{
    for (size_t i = 0; (dest[i] = source[i]) != '\0'; ++i)
        ;

    return dest;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte s adresinden iki ilerinin adresi fonksiyona gönderilmiştir. Bu durumda fonksiyon "kara" yazısını hedef diziye kopyalayacaktır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    char d[1024];

    strcpy(d, s + 2);
    printf("%s\n", d);	/* kara */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strcat fonksiyonu bir yazının sonuna başka bir yazıyı eklemek için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:

    char *strcat(char *dest, const char *source);

    Buradaki const anahtar sözcüğünü henüz görmedik. Fonksiyon birinci parametresiyle belirtilen adreste bulunan yazının sonuna oradaki null karakteri ezerek 
    ikinci parametresiyle belirtilen adresten başlayarak null karakter görene kadar (null karakter dahil) karakterleri kopyalar. Birinci parametresiyle
    belirtilen hedef adresin aynısına geri döner. (Buradaki "cat" sözcüğü "concatenate" sözcüğünden gelmektedir.) Burada programcı eklemenin yapılacağı hedef dizinin
    yeterince büyük olmasına dikkat etmelidir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    char d[1024] = "istanbul";

    strcat(d, s);

    puts(d);		/* istanbulankara */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki gibi bir kullanım tanımsız davranışa yol açar:

    char s[1024] = "ankara";
    char d[1024];

    strcat(d, s);

    Çünkü burada d dizisinin içerisinde çöp değerler olduğu için o çöp değerlerin sonunda tesadüfen bulunan bir null karakterden sonra karakter kopyalanacaktır. Örneğin:

    char s[1024] = "ankara";
    char d[1024] = "";

    strcat(d, s);

    Burada d dizisinin tüm elemanları sıfırlanacaktır. Tabii 0 aynı zamanda null karakter anlamındadır. Bu durumda yapılan işlemin strcpy işleminden bir farkı kalmayacaktır.

    Aşağıdaki örnekte 5 kez klavyeden (stdin dosyasından) girilen yazı diğer bir dizinin içerisindeki yazının sonuna eklenmiştir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[1024];
    char d[1024] = "";

    for (int i = 0; i < 5; ++i) {
        printf("Bir yazi giriniz:");
        gets(s);
        strcat(d, s);
    }
    printf("%s\n", d);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strcat fonksiyonunu da kolay bir biçimde yazabiliriz. İlk akla gelen yazım biçimi şöyle olabilir:

    char *mystrcat(char *dest, char *source)
    {
        char *temp = dest;

        while (*dest != '\0')
            ++dest;

        strcpy(dest, source);

        return temp;
    }

    Burada dest göstericisi artırılarak null karakteri gösterir hale getirilmiştir. Ondan o adrese kopyalama yapılmıştır. Tabii buradaki stcpy kısmını da
    kod olarak yazabilirdik:

    char *mystrcat(char *dest, char *source)
    {
        char *temp = dest;

        while (*dest != '\0')
            ++dest;

        while ((*dest = *source) != '\0') {
            ++source;
            ++dest;
        }

        return temp;
    }

    Ya da [] operatörü ile de aynı fonksiyonu yazabilirdik:

    
    char *mystrcat(char *dest, char *source)
    {
        size_t i;

        for (i = 0; dest[i] != '\0'; ++i)
            ;
        for (size_t k = 0; (dest[i + k] = source[k]) != '\0'; ++k)
            ;

        return dest;
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

char *mystrcat(char *dest, char *source);

int main(void)
{
    char s[] = "ankara";
    char d[1024] = "istanbul";

    mystrcat(d, s);

    puts(d);		/* istanbulankara */

    return 0;
}

char *mystrcat(char *dest, char *source)
{
    size_t i;

    for (i = 0; dest[i] != '\0'; ++i)
        ;
    for (size_t k = 0; (dest[i + k] = source[k]) != '\0'; ++k)
        ;

    return dest;
}

/*----------------------------------------------------------------------------------------------------------------------
    strchr fonksiyonu bir yazı içerisinde bir karakteri aramak için kullanılır. Fonksiyonun orijinal prototipi şöyledir:

    char *strchr(const char *str, int ch);

    Buradaki const anahtar sözcüğünü henüz görmedik. Fonksiyon birinci parametresiyle belirtilen adresten başlayarak null karakter görene kadar ikinci parametresiyle belirtilen 
    karakteri arar. Eğer bulursa ilk bulduğu yerin yazı içerisindeki adresiyle geri döner. Eğer bulmazsa NULL adresle geri döner. Fonksiyon null karakterin
    kendisini de arayabilmektedir.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    char *str;

    str = strchr(s, 'k');
    if (str != NULL)
        printf("%s\n", str);		/* kara */
    else 
        printf("cannot find!");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    foo fonksiyonu bir adres bilgisine geri dönüyor olsun. *foo() gibi bir işlem geçerlidir. Burada önce foo fonksiyonu çağrılır. Ele edilen adrese * 
    operatörü uygulanmıştır. Benzer biçimde foo()[n] ifadesi de geçerlidir. Burada fonksiyon çağırma operatörü ile [] operatörü soldan sağa eşit öncelikli 
    gruptadır. Bu durumda önce fonksiyon çağrılır. Fonksiyonun geri döndürdüğü adresten n ilerinin içeriği elde edilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    char *str;

    *strchr(s, 'k') = 'x';
    
    puts(s);	/* anxara */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıda benzer bir örnek verilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    char *str;

    strchr(s, 'k')[2] = 'x';
    
    puts(s);	/* ankaxa  */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strchr fonksiyonu ile null karakterin kendisi de aranabilir. Örneğin biz null karakterin adresini bulmak için şöyle yapabiliriz:

    str = strchr(s, '\0');

    Örneğin aslında strcat aşağıdaki ile eşdeğerdir:

    strcpy(strchr(dest, '\0'), source);

    s char türden bir adres olmak üzere biz s adresinde bulunan yazının sonundaki null karakterin adresini s + strlen(s) biçiminde ya da strchr(s, '\0')
    biçiminde elde edebiliriz. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

char *mystrcat(char *dest, char *source)
{
    strcpy(strchr(dest, '\0'), source);

    return dest;
}

int main(void)
{
    char s[] = "ankara";
    char d[1024] = "istanbul";
    
    mystrcat(d, s);
    puts(d);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strchr fonksiyonunu basit bir biçimde yazabiliriz. Ancak gerçekleşirimde null karakterin de aranabileceğini göz önünde bulundurmalısınız:

    char *mystrchr(char *str, int ch)
    {
        while (*str != '\0') {
            if (*str == ch)
                return str;
            ++str;
        }

        return ch == '\0' ? str : NULL;
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

char *mystrchr(char *str, int ch);

int main(void)
{
    char s[] = "ankara";
    char *str;
    
    str = mystrchr(s, 'k');
    puts(str);			/* kara */

    return 0;
}

char *mystrchr(char *str, int ch)
{
    while (*str != '\0') {
        if (*str == ch)
            return str;
        ++str;
    }

    return ch == '\0' ? str : NULL;
}

/*----------------------------------------------------------------------------------------------------------------------
    strchr fonksiyonunun karakteri son blduğu yerin adresiyle geri dönen (yani başka bir deyişle aramayı sondan başa doğru yapan) strrchr isimli bir 
    benzeri de vardır. strrchr fonksiyonunun orijinal prototipi şöyledir:

    char *strrchr(const char *str, int ch);

    Biz henüz buradaki const anahtar sözcüğünü görmedik. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "izmir";
    char *str;

    str = strrchr(s, 'i');
    if (str)
        puts(str);		/* ir */
    else
        printf("cannt find!..\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte UNIX/Linux işletim sistemlerinde bir yol ifadesinin (path) sonundaki dosya ismi yazdırılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char path[] = "/home/kaan/study/test.c";
    char *fname;

    fname = strrchr(path, '/');
    if (fname)
        puts(fname + 1);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii aslında bir yol ifadesi hiç '/' karakteri içermeyebilir. O halde yukarıdaki programı bu durumu da ele alacak biçimde aşağıdaki gibi düzeltebiliriz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char path[4096];
    char *fname;

    printf("Bir yol ifadesi giriniz:");
    gets(path);

    fname = strrchr(path, '/');
    if (fname)
        puts(fname + 1);
    else
        puts(path);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki gibi durumlarda programcılar genellikle daha kompakt bir yazım sağlamak için atama işlemini if parantezi içerisinde yaparlar. Tabii bu durumda
    atama operatörünün paranteze alınarak önceliklendirilmesi gerekir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char path[4096];
    char *fname;

    printf("Bir yol ifadesi giriniz:");
    gets(path);

    if ((fname = strrchr(path, '/')) != NULL)
        puts(fname + 1);
    else
        puts(path);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strrchr fonksiyonunu şöyle yazabiliriz:

    char *mystrrchr(char *str, int ch)
    {
        char *result = NULL;

        while (*str != '\0') {
            if (*str == ch)
                result = str;
            ++str;
        }

        return ch == '\0' ? str : result;	
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

char *mystrrchr(char *str, int ch);

int main(void)
{
    char path[4096];
    char *fname;

    printf("Bir yol ifadesi giriniz:");
    gets(path);

    if ((fname = mystrrchr(path, '/')) != NULL)
        puts(fname + 1);
    else
        puts(path);

    return 0;
}

char *mystrrchr(char *str, int ch)
{
    char *result = NULL;

    while (*str != '\0') {
        if (*str == ch)
            result = str;
        ++str;
    }

    return ch == '\0' ? str : result;	
}

/*----------------------------------------------------------------------------------------------------------------------
                                                            39. Ders 18/10/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    strcmp fonksiyonu iki yazıyı karşılaştırmak için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:

    int strcmp(const char *s1, const char *s2);

    Biz henüz const anahtar sözcüğünü görmedik. Fonksiyonun iki parametresi karşılaştırılacak iki yazının adreslerini almaktadır. Fonksiyon "leksikografik" 
    bir karşılaştırma yapar. Leksikografik karşılaştırma demek "eşit olduğu sürece ilerle, ilk eşit olmayanın durumuna bak" demektir. Yani sözlükteki sıraya 
    göre karşılaştırma anlamına gelir. Örneğin:

    - "ali" yazısı "alm" yazısından küçüktür.
    - "aliye" yazısı "ali" yazısından büyüktür.
    - "ali" yazısı "Ali" yazısından ASCII karakter tablosu kullanılıyorsa büyüktür. 
    - "ali" yazısı ile "ali" yazısı biribirine eşittir. 

    strcmp fonksiyonu birinci yazı ikinci yazıdan büyükse pozitif herhangi bir değere, ikinci yazı birinci yazıdan büyükse negatif herhangi bir değere 
    ve iki yazı eşitse sıfır değerine geri döner. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char passwd[] = "maviay";
    char s[1024];

    printf("Enter password:");
    gets(s);

    if (!strcmp(s, passwd))
        printf("Ok\n");
    else
        printf("Incorrect password!..\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki programı üç kere denemeli hale getirebiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char passwd[] = "maviay";
    char s[1024];
    int i;

    for (i = 0; i < 3; ++i) {
        printf("Enter password:");
        gets(s);
        if (!strcmp(s, passwd)) 
            break;
        printf("Incorrect password!..\n");
    }

    if (i == 3)
        printf("Sorry you must wait 5 minutes to try again...\n");
    else
        printf("Ok\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strcmp fonksiyonunu aşağıdaki gibi yazabiliriz:

    int mystrcmp(char *s1, char *s2)
    {
        while (*s1 == *s2) {
            if (*s1 == '\0')
                return 0;
            ++s1;
            ++s2;
        }
        return *s1 - *s2;
    }

    if karşılaştırmasında return yerine break deyimini de kullanabilirdik:

    int mystrcmp(char *s1, char *s2)
    {
        while (*s1 == *s2) {
            if (*s1 == '\0')
                break;
            ++s1;
            ++s2;
        }
        return *s1 - *s2;
    }
    
    Ya da örneğin if deyimi yerine while parantezinin içerisinde && operatörü de kullanılabilir. 

    int mystrcmp(char *s1, char *s2)
    {
        while (*s1 == *s2 && *s1 != '\0') {	
            ++s1;
            ++s2;
        }
        return *s1 - *s2;
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int mystrcmp(char *s1, char *s2);

int main(void)
{
    char passwd[] = "maviay";
    char s[1024];

    printf("Enter password:");
    gets(s);

    if (!mystrcmp(s, passwd))
        printf("Ok\n");
    else
        printf("Incorrect password!..\n");

    return 0;
}

int mystrcmp(char *s1, char *s2)
{
    while (*s1 == *s2) {
        if (*s1 == '\0')
            return 0;
        ++s1;
        ++s2;
    }
    return *s1 - *s2;
}

/*----------------------------------------------------------------------------------------------------------------------
    strcpy, strcat ve strcmp fonksiyonlarının n'li versiyonları da vardır. Bunlar strncpy, strncat ve strncmp ismindedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    strncpy fonksiyonu bir yazının belli sayıda karakterini bir diziye kopyalamak için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:

    char *strncpy(char *dest, const char *source, size_t n);

    Burada fonksiyon ikinci parametresiyle belirtilen adresten başlayarak birinci parametresiyle belirtilen adrese üçüncü parametresiyle belirtilen miktarda 
    karakteri kopyalar. Fonksiyon eğer n <= strlen(source) ise null karakteri hedefe yerleştirmez. Ancak n > strlen(source) ise n - strlen(source) kadar null karakteri 
    hedefe kopyalamaktadır. Örneğin "ankara" yazısını kopyalama istediğimizi ve n değerinin 3 olduğunu düşünelim. Fonksiyon "ank" karakterlerini kopyalar ve 
    null karakteri eklemez. Ancak örneğin n değeri 30 ise fonksiyon "ankara" yazısının hepsini kopyalar. 30 - 6 = 24 tane null karakteri hedefe ekler. 
    Yani fonksiyon her zaman n tane karakteri hedefe kopyalamaktadır. strncpy fonksiyonu genellikle bir yazının belli bir kısmını başka bir yazıyla yer değiştirmek 
    amacıyla kullanılmaktadır. Bu durumda:

    strcpy(dest, source);

    çağısının strncp ile eşdeğeri şöyledir:

    strncpy(dest source, strlen(source) + 1);

    strncpy fonksiyonu da kopyalamanın yapıldığı hedef adrese geri dönmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char d[] = "yenisehir";
    char s[] = "eskihisar";

    strncpy(d, s, 4);

    puts(d);		/* eskisehir */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strncpy fonksiyonu çeşitli biçimlerde yazılabilir. Örneğin:

    char *mystrncpy(char *dest, char *source, size_t n)
    {
        size_t i;

        for (i = 0; i < n && source[i] != '\0'; ++i)
            dest[i] = source[i];

        for (; i < n; ++i)
            dest[i] = '\0';

        return dest;
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

char *mystrncpy(char *dest, char *source, size_t n);

int main(void)
{
    char d[1024] = "yenisehir";
    char s[] = "eski";

    mystrncpy(d, s, 10);

    puts(d);		/* eskisehir */

    return 0;
}

char *mystrncpy(char *dest, char *source, size_t n)
{
    size_t i;

    for (i = 0; i < n && source[i] != '\0'; ++i)
        dest[i] = source[i];

    for (; i < n; ++i)
        dest[i] = '\0';

    return dest;
}

/*----------------------------------------------------------------------------------------------------------------------
    strncat fonksiyonu bir yazının sonuna başka bir yazının ilk n karakterini eklemek için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:

    char *strncat(char *dest, const char *source, size_t n);

    Fonksiyon ikinci parametresiyle belirtilen yazının ilk n karakterini birinci parametresiyle belirtilen yazının sonuna ekler. Fonksiyon her zaman hedefe 
    null karakteri eklemektedir. Eğer n değeir büyükse yani n > strlen(source) ise en son null karakteri ekler ve işlemini sonlandırır. Yani başka bir deyişle
    eğer n değeri büyükse fonksiyon strcat gibi davranmaktadır. Fonksiyon yine birinci parametreyle girilen adresin aynısına geri dönmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char d[1024] = "eski";
    char s[] = "hisarustu";

    strncat(d, s, 5);

    puts(d);		/* eskihisar */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strncat fonksiyonu da değişik biçimlerde yazılabilir. Aşağıda bunlardan bir örnek verilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

char *mystrncat(char *dest, char *source, size_t n);

int main(void)
{
    char d[1024] = "eski";
    char s[] = "hisarustu";

    mystrncat(d, s, 5);

    puts(d);		/* eskihisar */

    return 0;
}

char *mystrncat(char *dest, char *source, size_t n)
{
    size_t i, k;

    for (i = 0; dest[i] != '\0'; ++i)
        ;

    for (k = 0; k < n && source[k] != '\0'; ++k)
        dest[i + k] = source[k];
    dest[i + k] = '\0';

    return dest;
}

/*----------------------------------------------------------------------------------------------------------------------
    strncmp fonksiyonu iki yazının ilk n karakterini karşılaştırmak için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:

    int strncmp(const char *s1, const char *s2, size_t n);

    Biz henüz const anahtar sözcüğünü görmedik. Burada n değeri strlen(s1) ya da strlen(s2)'den büyükse karşılaştırma sonlandırılmaktadır. Başka bir deyişle 
    iki yazının herhangi birinde null karakter görülürse karşılaştırma sonlandırılmaktadır. Yani n > strlen(s1) ya da n > strlen(s2) ise bu durumda fonksiyonun 
    strcmp'den bir farkı kalmaz. Fonksiyon yine birinci yazı ikinci yazdıdan büyükse pozitif herhangi bir değere, ikinci yazı birinci yazıdan büyükse negatif herhangi bir 
    değere ve iki yazı birbirine eşitse sıfır değerine geri dönmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s1[] = "eskis";
    char s2[] = "eskihisar";
    int result;

    result = strncmp(s1, s2, 4);

    if (result > 0)
        printf("s1 > s2\n");
    else if (result < 0)
        printf("s1 < s2\n");
    else
        printf("s1 == s2\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strncmp fonksiyonunu aşağıdaki gibi yazabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>

int mystrncmp(char *s1, char *s2, size_t n)
{
    size_t i;

    for (i = 0; i < n && s1[i] == s2[i]; ++i) 
        if (s1[i] == '\0')
            return 0;
 
    if (i == n)
        return 0;

    return s1[i] - s2[i];
}

int main(void)
{
    char s[] = "eskihisar";
    char k[] = "eskisehir";

    if (!mystrncmp(s, k, 4))
        printf("Ok\n");
    else
        printf("Not Ok\n");

    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
                                                    40. Ders 25/10/2022 - Sali
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Sayısal karakterlerden oluşan bir yazıyı int, long ve double türlerine dönüştüren üç klasik standart C fonksiyonu vardır. 
    Fonksiyonların orijinal prototipleri şöyledir:

    int atoi(const char *str);
    long atol(const char *str);
    double atof(const char *str);

    Buradaki const anahtar sözcüğünü henüz görmedik.

    Fonksiyonların prototipleri <stdlib.h> dosyası içerisinde bulunmaktadır. atoi (alphabetic to int), atol(alphabetic to long) ve atof (alphabetic to floating point)
    fonksiyonları bizden sayısal karakterlerden oluşan yazının bulunduğu dizinin başlangıç adreslerini alırlar ve bize sırasıyla int, long ve double 
    değerler verirler. C99 ile birlikte C'ye long long türü eklenince atoll fonksiyonu da standartlara eklenmiştir:

    long long atoll(const char *str);

    Bu fonksiyonun da geri dönüş değerinin long long olduğuna dikkat ediniz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char s[] = "1234";
    int val;

    val = atoi(s);
    printf("%d\n", val);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    atoi, atol ve atof fonksiyonları yazının başındaki boşluk karakterlerini (leading sapce) atarlar. İlk sayısal olmayan karakterde işlemini sonlandırırlar.
    Tabii sayının başında "-" ve "+" sembolleri de bulunabilir. atof fonksiyonunda "." karakteri de kullanılabilir. Örneğin aşağıdaki yazıların
    bu fonksiyonlarla dönüştürülmesinden elde edilen değerler yanlarına yazılmıştır:

    "    123   "    ---> 123
    "  123ali456 "  ---> 123
    "ali"           ---> 0
    ""				---> 0

    Bu fonksiyonlar bir taşma olduğunda (yani yazı içerisindeki sayılar ilgili türün sınırlarını negatif ya da pozitif bakımdan aştığında) errno değerini
    ERANGE olarak set etmektedir. Ancak biz bu errno konusunu henüz bilmiyoruz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char s[] = "123ali";
    int val;

    val = atoi(s);
    printf("%d\n", val);		/* 123 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Örneğin biz bu fonksiyonlar sayesinde klavyeden (stdin dosyasından) int, long ve double okuyan fonksiyonlar yazabiliriz. Önce yazıyı gets fonksiyonu ile 
    okuyup sonra bu fonksiyonlara sokabiliriz. gets fonksiyonunun C99'da "deprecated" yapıldığını ve C11'de C'den çıkartıldığını anımsayınız. Ancak derleyiciler
    bu fonksiyonu halen bulundurmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int read_int(void)
{
    char s[4096];

    gets(s);

    return atoi(s);
}

int main(void)
{
    int val;

    printf("Bir sayi giriniz:");
    val = read_int();
    printf("%d\n", val);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    atoi ve atol fonksiyonları nasıl yazılabilir? Aslında yazının sonundan başa gitmeye gerek yoktur. Sayıyı 10'la çarpıp sağındakiyle ekleyerek 
    işlemler yürütülebilir. Örneğin:

    "12345"

    1 * 10 + 2 = 12
    12 * 10 + 3 = 123
    123 * 10 + 4 = 1234
    1234 * 10 + 5 = 12345

    Tabii yazının başındaki boşluk karakterlerini atıp ilk boşluksuz karakterin "+" ya da "-" karakteri olup olmadığına da bakmak gerekir. Tipik 
    bir gerçekleştirim aşağıdaki gibi yapılabilir. atol fonksiyonun gerçekleştirimi "çalışma sorusu" olarak verilecektir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int myatoi(char *str)
{
    int total = 0;
    int sign = 1;

    while (isspace(*str))
        ++str;

    if (*str == '-' || *str == '+') {
        if (*str == '-')
            sign = -1;
        ++str;
    }

    while (isdigit(*str)) {
        total = total * 10 + *str - '0';
        ++str;
    }

    return sign * total;
}

int read_int(void)
{
    char s[4096];

    gets(s);

    return myatoi(s);
}

int main(void)
{
    int val;

    printf("Bir sayi giriniz:");
    val = read_int();
    printf("%d\n", val);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii yukarıdaki gerçekleştirim * operatörü yerine [] operatör ile de yapılabilirdi.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <ctype.h>

int myatoi(char *str)
{
    int total = 0;
    int sign = 1;
    size_t i;

    for (i = 0; isspace(str[i]); ++i)
        ;

    if (str[i] == '-' || str[i] == '+') {
        if (str[i] == '-')
            sign = -1;
        ++i;
    }

    for (; isdigit(str[i]); ++i) 
        total = total * 10 + str[i] - '0';

    return sign * total;
}

int read_int(void)
{
    char s[4096];

    gets(s);

    return myatoi(s);
}

int main(void)
{
    int val;

    printf("Bir sayi giriniz:");
    val = read_int();
    printf("%d\n", val);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    atoi, atol, atof ve atoll fonksiyonlarının biraz daha yetenekli versiyonları da vardır. Bunların listesi şöyledir: strtol, strtoul, strtoll, strtoull, 
    strtod strtof, strtold. Buradaki strtoll ve strtoull, strtof ve strtold fonksiyonları C99 ile eklenmiştir. Bu fonksiyonların prototipinde henüz görmediğimiz
    göstericileri gösteren göstericiler geçmektedir. Bu nedenle biz burada kaba bir açıklama ile yetineceğiz. 

    strtol, strtoul, strtoll ve strtoull fonksiyonlarının yine birinci parametreleri yazının başlangıç adresini almaktadır. Bunların ikinci parametreleri
    ilk geçersiz karakterin tespit edilmesi amacıyla bulundurulmuştur. Bu parametreyi şimdilik NULL adres geçebilirsiniz. Bu fonksiyonların diğer bir fazlalığı da
    taban belirtmeleridir. Fonksiyonların üçüncü parametreleri girilen yazıdaki sayıların tabanını belirtmektedir. 

    strtol yazdıdaki sayıyı long türüne, strtoul unsigned long türüne, strtoll long lon türüne, strtoull unsigned long long türüne, strtof float türüne,
    strtod double türüne ve strtold long double türüne dönüştürmektedir. Burada int ve unsigned int türüne dönüştüren bir strtoxxx fonksiyonu yoktur.

    Ayrıca bu fonksiyonlarda taban belirten üçüncü parametre 0 girilirse bu durumda taban yazı biçiminde sayının öneklerinden anlaşılacaktır. Eğer önek 
    0x ya da 0X ile başlatılmışsa taban 16, 0 ile başlatılmışsa 8, hiçbir şey ile başlatılmamışsa 10 kabul edilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char s[] = "    100    ";
    char k[] = " 0x100  ";
    long val;

    val = strtol(s, NULL, 10);
    printf("%ld\n", val);           /* 100 */

    val = strtol(s, NULL, 2);
    printf("%ld\n", val);           /* 4 */

    val = strtol(k, NULL, 0);
    printf("%ld\n", val);           /* 256  */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    strstr isimli standart C fonksiyonu bir yazının içerisinde başka bir yazıyı arar. Orijinal protipi şöyledir:

    char *strstr(const char *str1, const char *str2);

    Buradak const anahtar sözcüğünü henüz görmedik. Fonksiyon birinci parametresiyle belirtilen yazı içerisinde ikinci parametresi ile belirtilen yazıyı 
    aramaktadır. Eğer bulursa birinci parametre ile belirtilen yazıda bulduğu yerin adresiyle geri döner. Bulamazsa NULL adresle gei döner. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "this is a test yes!";
    char k[] = "test";
    char *str;

    str = strstr(s, k);
    if (str != NULL)
        puts(str);		/* test yes! */
    else
        printf("cannot find!\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yazı içersinde başka bir yazının aranması basit bir biçimde yapılabilir. Önce aranacak yazının uzunluğu bulunur. 
    Sonra ana yazı içerisinde aranacak yazı kaydırılır ve strncmp fonksiyonuyla arama yapılır. Aslında bu klasik 
    yöntemin çeşitli alternatifleri de vardır. Bu alternatiflerden biri ""Knuth-Moriss-Pratt" algoritması denilen algoritmadır. 
    Ancak standart kütüphaneler genellikle klasik kaydırma yöntemini uygulamaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi biz yukarıdaki işlemin tersini yapacak olsak nasıl yaparız? Yani int bir sayıyı yazıya dönüştürmek istersek?
    Bazı C derleyicilerinde bu işi yapan itoa isimli bir eklenti fonksiyon bulunmaktadır. Ancak bu işlemi yapan genel 
    sprintf isimli genel bir fonksiyon bulunmaktadır. Pekiyi aşağıdaki prototipe sahip itoa fonksiyonunu nasıl yazabiliriz:
        
    char *itoa(int val, char *str);

    Fonksiyonun birinci parametresi dönüştürülecek int değeri, ikinci parametresi dönüştürülmüş olan yazının adresini 
    almaktadır. Fonksiyon ikinci parametresiyle belirtilen adresin aynısına geri döner. Bu tür fonksiyonlar özyinelemeli 
    yazılabilmektedir. Ancak özyineleme olmadan bu fonksiyonu yazmak için önce sayının kaç basamaklı olduğunu belirlemek 
    sonra da basamakları sayıyı sürekli 10'a bölerek elde etmek gerekir. Aşağıda böyle bir gerçekleştirim verilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>

char *itoa(int val, char *str)
{
    int digit;
    int ndigits;
    int flag = 0;
    
    if (val < 0) {
        flag = 1;
        val = -val;
    }

    ndigits = log10(val) + 1 + flag;

    for (int i = ndigits - 1; val != 0; --i) {
        digit = val % 10;
        str[i] = digit + '0';
        val /= 10;
    }
    if (flag)
        str[0] = '-';

    str[ndigits] = '\0';

    return str;
}

int main(void)
{
    char s[32];
    int val = -12345;

    itoa(val, s);

    printf(":%s:\n", s);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi bir göstericiye farklı türden bir adresin doğrudan atanması geçerli bir işlem değildir. Daha teknik ifade edilirse farklı türden adresler 
    arasında otomatik (implicit) dönüştürme yoktur. Ancak bazen biz bir gösteriye gerçekten farklı türden bir adresin atanmasını isteyebiliriz. İşte bu tür
    durumlarda tür dönüştürme operatörü kullanılmalıdır. Derleyici tür dönüştürme operatörnü gördüğünde bu işlemin yanlışlıkla değil bilinçli bir biçimde 
    yapılmak istendiğini anlamaktadır. Örneğin:

    char s[10];
    int *pi;

    pi = s;					/* geçersiz! */
    pi = (int *)s;			/* geçerli, tür dönüştürmesi yapılmış */

    Dönüştürmede * atomuna dikkat ediniz "(int *)" gibi bir dönüştürme "int türünden adrese dönüştürme" anlamına gelmektedir. Dönüştürme sırasında kaynak 
    adresin sayısal değeri değiştirilmeden hedef adresin türüne dönüştürülür. Yani yukarıdaki örnekte adresin sayısal bileşeninde bir değişiklik olmayacaktır. 
    Örneğin biz int bir nesnenin tüm byte'larını yazdırmak isteyebiliriz. Bu duurmda int nesnenin adresini unsigned char türünden bir adrese dönüştürürüz. 
    Sonra byte byte nesnenin parçalarını elde ederiz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 0x12345678;
    unsigned char *puc;

    puc = (unsigned char *)&a;
    printf("%02X %02X %02X %02X\n", puc[0], puc[1], puc[2], puc[3]);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir tamsayı türünden bilginin de doğrudan bir göstericiye atanması geçerli değildir. Daha teknik bir anlatımla tamsayı türlerinden adres türlerine otoimatik (implicit)
    tür dönüştürmesi yoktur. Ancak bu işlem de tür dönüştürme operatör ile yapılabilir. Aslında biz bunu "adres sabiti" olarak zaten görmüştük. Bu tür durumlarda 
    ilgili tamsayı değer değer adresin sayısal bileşeni olarak atanmaktadır. Örneğin:

    int *pi;
    int a = 12345;

    pi = a;						/* geçersiz */
    pi = (int *)a;				/* geçerli */
    pi = (int *) 12345678;		/* geçerli, adres sabiti olarak görmüştük */
    pi = (int *) 0x1FC1234;		/* geçerli */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir adres bilgisi de adres olmayan bir nesneye atanamaz. Yani teknik olarak ifade edersek adres türlerinden diğer türlere otomatik dönüştürme yoktur. 
    Ancak eğer istenirse bir adres türü tür dönüştürme operatörü ile bir tamsayı türüne dönüştürülebilir. Bu durumda dönüştürme sonucunda adres sayısal 
    bileşeni elde edilir. Örneğin:

    int a;
    long b;

    b = &a;			/* geçersiz! int türden adres bilgisi long bir nesneye atanmış */
    b = (long)&a;	/* geçerli, tür dönüştürmesi yapılmış */

    Bu tür işlemler bazen gerekebilmektedir. Örneğin adresler üzerinde bit operatörleriyle işlem yapılamadığından (bit operatörleri ileride ele alınacaktır)
    programcı önce onu bir tamsayı türüne dönüştürüp bir işlemlerini yapıp geri dönüştürmek isteyebilir. Ancak bu tür durumlarda seçilen tamsayı türünün 
    adresin sayısal bileşenini içerecek büyüklükte olmasına özen gösterilmelidir. Bu konuda ileride açıklamalar yapılacaktır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi C'de void anahtar sözcüğü fonksiyonun geri dönüş değerinin türü yerine kullanıldığında bu durum fonksiyonun geri dönüş değerine 
    sahip olmadığı anlamına geliyordu. void anahtar sözcüğü fonksiyon tanımlamasında ve prototipinde fonksiyonun parametreye sahip olmadığı anlamına geliyordu.
    Yine anımsanacağı gibi tanımlama sırasında parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında bir farklılık yoktu. Ancak 
    prototip bildiriminde parametre parantezinin içinin boş bırakılması parametre kontrolünün yapılmayacağı anlamına geliyordu. İşte void anahtar sözcüğü
    gösterici tanımlamada da kullanılabilmektedir. Böyle göstericilere "void göstericiler" denilmektedir. Örneğin:

    void *pv;

    Burada pv vir void göstericidir. void türden nesne tanımlanamaz ancak gösterici tanımlanabilir. Örneğin:

    void a;		/* geçersiz! */

    void gösterici türü olmayan göstericidir. void göstericinin türü olmadığı için void göstericiler * ve [] operatörleriyle kullanılamazlar. Örneğin:

    void *pv;
    ...
    a = *pv;		/* geçersiz! */
    a = pv[i]		/* geçersiz! */

    void göstericileri (genel olarak void adresleri) artırıp eksiltemeyiz. Çünkü onların türleri olmadığı için içerisindeki adreslerin sayısal bileşenlerinin
    ne kadar artıp eksileceği belli değildir. 

    O zaman void bir gösterici ne işe yarar? 

    void bir göstericiye herhangi bir türden adres doğrudan atanabilir. Bunun nedeni zaten void göstericilerin zararlı bir duruma yol açamamasıdır. Tabii void 
    göstericilere adres olmayan bir bilgi atayamayız. Örneğin:

    int a;
    double b;
    char c;
    void *pv;

    pv = &a;		/* geçerli */
    pv = &b;		/* geçerli */
    pv = &c;		/* geçerli */

    C'de void bir adres de herhangi bir türden göstericiye doğrudan atanabilmektedir. Yani yukarıdakinin tersi de C'de geçerlidir. Örneğin:

    void *pv;
    int *pi;
    double *pd;

    pi = pv;		/* geçerli */
    pd = pv;		/* geçerli */

    Yani C'de void bir göstericiye herhangi türden bir adres atanabileceği gibi void bir adres de herhangi bir türden göstericiye atanabilmektedir. 

    void bir adresin başka bir göstericiye atanması C'de serbest olsa da C++'ta yasaklanmıştır. Yani C++'ta yine void göstericiye herhangi bir türden
    adres atanabilir. Ancak void bir adres herhangi bir türden gösteriye atanamaz. C++'ta bunun yasaklanmasının nedeni aşağıdaki gibi "arkadan dolaşma" 
    yönteminin bertaraf edilmesi içindir:

    int *pi;
    char *pc;
    void *pv;

    pv = pi;	/* hem C'de hem C++'ta geçerli */
    pc = pv;	/* C'de geçerli ama C++'ta geçersiz! */

    Brada aslında pc = pi işlemi yapılmıştır. Ancak bu işlem C'de tamamen yasal bir kılıfa uydurulmuştur. İşte C++ bu duruma itiraz etmiş, void bir adresin
    başka türden bir göstericiye atanmasını geçersiz hale getirmiştir. 

    Madem ki C++'ta void bir adres türü belirli bir göstericiye C'deki gibi atanamıyor. O halde C++'ta void bir adres türü belirli göstericiye tür dönüştürme operatörü ile 
    atanmalıdır. Örneğin:

    int *pi;
    char *pc;
    void *pv;

    pv = pi;			/* hem C'de hem C++'ta geçerli */
    pc = pv;			/* C'de geçerli, C++'ta geçersiz */
    pc = (char *)pv;		/* C'de de geçerli C++'ta da geçerli */

    C programcılarının bazıları void bir adresi türü belirli bir göstericiye atarken de gerekmediği halde C++ uyumunu korumak için tür dönüştürmesi yapmaktadır. 
    Biz de kursumuzda aslnda C'de gerekmediği halde void bir adresi C++ uyumunu da korumak için türü belirli bir göstericiye tür dönüştürme operatörü ile atayacağız. 
    
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    41. Ders 01/11/2022 - Sali
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    void göstericilere neden gereksinim duyulduğuna ilişkin iyi bir örnek memcpy fonksiyonudur. memcpy fonksiyonu prototipi <string.h> içerisinde olan 
    standart bir C fonksiyonudur. Fonksiyon bir adresten diğer bir adrese koşulsuz n byte kopyalamaktadır. memcpy fonksiyonu strcpy fonksiyonunu çağrıştırıyorsa da 
    ondanm farklıdır. memcpy fonksiyonu null karakter görünce durmaz. Koşulsuz n byte kopyalar. Örneğin:

    #include <stdio.h>
    #include <string.h>

    int main(void)
    {
        int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
        int b[10];

        memcpy(b, a, 40);

        for (int i = 0; i < 10; ++i)
            printf("%d ", b[i]);
        printf("\n");

        return 0;
    }

    Burada a adresinden itibaren b adresine 40 byte kopyalanmıştır. Eğer sistemimizde bir int 4 byte uzunluktaysa bu durum a dizisinin hepsisinin b'ye
    kopyalanacağı anlamına gelir. Biz memcpy fonksiyonuyla double bir diziyi de, long bir diziyi de aynı biçimde kopyalayabilirdik. Pekiyi o zaman
    memcpy fonksiyonun parametresi hangi türden göstericidir? İşte memcpy fonksiyonunun parametreleri her türden adresi kabul ettiğine göre void gösterici 
    olmalıdır. memcpy fonksiyonun orijinal prototipi şöyledir:

    void *memcpy(void *dest, const void *source, size_t n);

    Buradaki const anahtar sözcüğünü henüz görmedik. Fonksiyon ikinci parametresiyle belirtilen adreten başlayarak birinci parametresiyle belirtilen adrese 
    üçüncü parametresiyle belirtilen miktarda koşulsuz byte kopyalar. Fonksiyon birinci parametresiyle belirtilen adresin aynısına geri döner. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int b[10];
    double c[10] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.10};
    double d[10];

    memcpy(b, a, 40);			/* int 4 byte olmalı */

    for (int i = 0; i < 10; ++i)
        printf("%d ", b[i]);
    printf("\n");

    memcpy(d, c, 80);		/* double 8 byte olmalı */

    for (int i = 0; i < 10; ++i)
        printf("%f ", d[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    s adresindeki yazıyı d adresine kopyalayacak olalım. Bunu strcpy fonksiyonu ile yaparız:

    strcpy(d, s);

    Bu işlemi memcpy fonksiyonu ile yapmak istesek fonksiyonu şöyle çağırmamız gerekir:

    memcpy(d, s, strlen(s) + 1);

    Buradaki "+ 1" null karakteri de kopyalamak için gerekmektedir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "ankara";
    char d[100];

    memcpy(d, s, strlen(s) + 1);
    puts(d);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi memcpy fonksiyonunu nasıl yazabiliriz? void göstericler * ve [] operatörleriyle kullanılamazlar, artırılıp eksiltilemezler. O zaman 
    bizim fonksiyon içerisinde bu void adresleri belli bir türden adrese dönüştürüp işleme sokmamız gerekir. Tabii bunun için en uygun tür char türüdür. Örneğin:

    void *mymemcpy(void *dest, void *source, size_t n)
    {
        char *pcdest = (char *)dest;		/* C'de dönüştürme gerekmez, C++'ta gerekir */
        char *pcsource = (char *)source;	/* C'de dönüştürme gerekmez, C++'ta gerekir */

        for (size_t i = 0; i < n; ++i)
            pcdest[i] = pcsource[i];

        return dest;
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void *mymemcpy(void *dest, void *source, size_t n)
{
    char *pcdest = (char *)dest;		/* C'de dönüştürme gerekmez, C++'ta gerekir */
    char *pcsource = (char *)source;	/* C'de dönüştürme gerekmez, C++'ta gerekir */

    for (size_t i = 0; i < n; ++i)
        pcdest[i] = pcsource[i];

    return dest;
}

int main(void)
{
    int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int b[10];

    mymemcpy(b, a, 40);

    for (int i = 0; i < 10; ++i)
        printf("%d ", b[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    memcpy fonksiyonu ile çakışık blok kopyalaması "tanımsız davranışa (undefined behavior)" yol açmaktadır. Çakışık blok kopyalaması dizi kaydırma (expand)
    ve sıkıştırma (shrink) işlemlerinde sıkça kullanılmaktadır. Örneğin elimizde 10 elemanlı int türden bir a dizisi olsun. Bunun 9 elemanı dolu olsun:

    1 2 3 4 5 6 7 8 9 ? 

    Bizim amacımız dizinin başına bir değer insert etmek olsun. Bunun için bizim 0'dan sonrasını bir int'lik kaydırmamız gerekir:
    
    ? 1 2 3 4 5 6 7 8 9  
    
    Programcı bu işlemi memcpy kullanarak aşağıdaki gibi yapmak isteyebilir:

    memcpy(a + 1, a,  9 * 4);

    Artık dizinin ilk elemanını kaydırdığımıza göre ilk elemana insert işlemini yapabiliriz:

    a[0] = 100;

    Ancak bu işlemde bir hata vardır. Eğer memcpy kopyalamayı baştan itibaren yaparsa bu durumda kopyalamayı yaparken aynı zamanda dizinin sonraki elemanlarını
    bozar. İşte memcpy'nin çakışık bloklarda sonraki elemanları bozmadan kopyalama yapan memmove isimli bir versiyonu da vardır. memmove fonksiyonun memcpy'den tek 
    farkı çakışık bloklarda kopyalamayı duruma göre baştan duruma göre sondan başlatarak problemsiz yapabilmeisidir. Bu nedenle yukarıdaki gibi 
    insert, delete işlemlerinde memcpy yerine memmove fonksiyonu kullanılmalıdır. memmove fonksiyonunun orijinal prototipi memcpy gibidir:

    void *memmove(void *dest, const void *source, size_t n);

    Pekiyi madem ki memmove fonksiyonu aslında memcpy fonksiyonunu işlevsel olarak kapsamaktadır neden iki ayrı fonksiyon bulundurulmuştur? İşte memmove 
    fonksiyonunun çakışık bloklarda düzgün davranabilmesi için işin başında source ve dest adesleri kontrol etmesi gerekmektedir. Bu kontrol memcpy fonksiyonunda 
    yapılmamaktadır. Bu durumda memmove fonksiyonun çakışık olmayan bloklarda memcpy fonksiyonundan daha yavaş olduğu söylenebilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Örneğin biz memmove fonksiyonunu kullanarak int bir dizide insert işlemi yapan genel bir fonksiyon yazmak isteyelim. int türünün de 4 byte olduğunu varsayalım.
    Fonksiyonu şöyle yazabiliriz:

    void insert(int *pi, size_t size, size_t index, int val)
    {
        memmove(pi + index + 1, pi + index, (size - index) * 4);
        pi[index] = val;
    }

    Burada pi dizinin başlangıç adresini belirtir. size dizideki dolu eleman sayısıdır. index insert edilecek elemanın indeks numarasıdır. val ise insert edilecek 
    değeri belirtir. Bizim memmove fonksiyonu ile aslında her elemanı bir kaydırmamız gerekir. Sonra ilgili index pozisyonunu açıp oraya insert işlemi yapmamız gerekir. 
    Tabii işlem sonucunda artık dizinin dolu olan eleman sayısı 1 artacaktır. Bu tür durumlarda memcpy yerine memmove fonksiyonunu kullanmalısınız.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

void insert(int *pi, size_t size, size_t index, int val)
{
    memmove(pi + index + 1, pi + index, (size - index) * 4);
    pi[index] = val;
}

int main(void)
{
    int a[100] = {1, 2, 3, 4, 5};

    insert(a, 5, 0, 100);

    for (int i = 0; i < 6; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                    42. Ders 03/11/2022 - Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    void göstericilerin kullanımına diğer bir örnek de memset fonksiyonudur. Bu fonksiyonun da prototipi <string.h> içerisindedir. memset fonksiyonu 
    belli bir adresten itibaren belli bir miktarda belli bir byte'ı doldurmak için kullanılır. Örneğin biz bellekte belli bir yeri 0'larla doldurmak 
    isteyebiliriz. Doldurulacak yerin türünün bir önemi yoktur. Orası memset fonksiyonu tarafından bir byte yığını olarak ele alınmaktadır. Fonksiyonun 
    prototipi şöyledir:

    void *memset(void *ptr, int ch, size_t n);

    Fonksiyonun ikinci parametresindeki değer 0 il2 255 arasında olmalıdır. Fonksiyon birinci parametresiyle belirtilen adresten başlayarak üçüncü parametresiyle belirtilen miktarda byte'ı 
    ikinci parametresiyle belirtilen değerle doldurur. Eğer ikinci parametredeki değer büyük ise onun düşük anlamlı byte değeri işleme sokulur. 

    Bazı derleyicilerde eklenti olarak strset isimli bir fonksiyon da bulundurulmaktadır. Bu fonksiyon bir yazıyı null 
    karakter görene kadar belli bir karakterle doldurmaktadır. Ancak strset fonksiyonu bir standart C fonksiyonu değildir. 
    s
    Aşağıdaki örnekte int türünün 4 byte olduğu bir sistemde 10 elemanlı int bir dizinin tüm byte'ları sıfırlanmıştır. Dolayısıyla dizi elemanları da
    sıfırlanmış olmaktadır. Tabii bir int dizinin tüm byte'larına örneğin 1 gibi bir değer atarsak biz dizi elemanlarını 1'lemiş olmayız. Çünkü tüm byte'ları 1 olan bir 
    int değer aslında farklı değerdir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    int a[10];

    memset(a, 0, 10 * 4);

    for (int i = 0; i < 10; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    memcmp fonksiyonu iki adres alır, o adreslerden itibaren karşılıklı  n byte'ı bçimde karşılaştırır. strcmp fonksiyonuna 
    benzemekle birlikte null karakter görünce işlemini bitirmez. Bu fonksiyon yazıları değil bellekteki byte yığınlarını 
    karşılaştırmak için kullanılmaktadır. Bu fonksiyon için null karakterin de sıradan bir byte olduğuna dikkat ediniz. 
    Fonksiyonun prototipi yine <string.h> dosyası içerisidedir. Orijinal prototipi şöyledir:

    int memcmp(const void *ptr1, const void *ptr2, size_t n);

    Fonksiyon ilk iki paraetresiyle aldığı adresten itibaren karşılıklı byte'ları karşılaştırır. İlk eşit olmayan byte'ın 
    durumuna göre eğer birinci adresteki byte yığını ikinci adresteki byte yığınından büyükse pozitif bir değere, küçükse 
    negatif herhangi bir değere ve eşitse sıfır değerine geri döner. İki byte yıpınının eşit olması için buların n byte'ının 
    da eşit olması gerekir. Uygulamada memcmp fonksiyonu bellekte belli byte kalıplarını bulmak için sıkça kullanılmaktadır. 

    Aşağıdaki örnekte int türünün 4 byte olduğu bir sistemde iki int dizinin içeriklerinin aynı olup olmadığı memcmp 
    fonksiyonuyla tespit edilmeye çalışılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};
    int b[5] = {1, 2, 3, 4, 5};
    int result;

    result = memcmp(a, b, 5 * 4);
    
    printf(result ? "icerikler ayni degil\n" : "icerikler ayni\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yazılar üzerinde işlem yapan bazı fonksiyonlar bazı derleyicilerde aynı isimle bulunduğu için kişiler onları standart 
    C fonksiyonu sanabilmektedir. Aşağıdaki fonksiyonlar Microsoft ve Borland derleyicilerinde bulunmakla birlikte gcc 
    derleyicilerinde bulunmamaktadır. Bunlar standart C fonksiyonları değillerdir:

    char *stricmp(const char *s1, const char *s2);
    char *strrev(char *str);
    char *strset(char *str, int ch);
    char *strupr(char *str);
    char *strlwr(char *str);

    stricmp büyük harf küçük harf duyarlılığı olmadan yazı karşılaştırmasını yapmaktadır. strrev bir yazıyı ters yüz 
    etmektedir. strset bir yazının karakterlerini başka bir karakterle doldurmaktadır. strupr ve strlwr fonksiyonları 
    yazının tüm karakterlerini büyük harf ve küçük harfe dönüştürmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Göstericiler potansiyel olarak bellekte herhangi bir yeri gösterebilirler. Ancak göstericiler yoluyla bizim tarafımızdan tahsis edilmemiş olan alanlara erişmeye 
    çalışmak "tanımsız davranışa (undefined behavior)" yol açmaktadır. Örneğin:

    char *pc = (char *)0x1FC020;

    Burada pc göstericisine rastgele bir adres atanmıştır. Bu işlemde henüz bir sorun yoktur. Ancak bu gösterici yoluyla o adrese erişilmeye çalışılırsa 
    tanımsız davranış oluşur:

    *pc = 0;		/* rastgele bir adrese erişiliyor */

    Burada bu rastgele adese veri aktarmak değil erişmek de tanımsız davranışa yol açar. 

    Tanımlama yoluyla tahsis edilmiş alanlar bizim için ayrılan alanlardır. Biz bu alanlara herhangi bir biçimde erişebiliriz. Ancak tanımlama yoluyla
    tahsis etmediğimiz, bizim için ayrılmayan rastgele alanlara göstericiler yoluyla erişmemeiz gerekir. Örneğin:

    char ch;
    char *pc = &ch;

    Burada *pc erişiminin hiçbir sorunu yoktur. Çünkü aslında eriştiğimiz yer zaten bizim için ayrılan bir yerdir. Örneğin:

    int a[10];
    int *pi = a;

    Burada pi göstericisi ile biz int dizinin 10 elemanına da erişebiliriz. Ancak bu dizinin ötesine ya da gerisine erişirsek bu durum tanımsız davranışa yol açar. 

    Pekiyi biz bir göstericiye rastgele bir adres atayarak bellekte istediğimiz bir yere erişmeye çalışırsak ne olur? Bu durum çalıştığımız sistemde göre değişebilir. 
    Örneğin bir mikrodenetleyici sisteminde oradaki verileri bozmuyorsak bir şey olmayabilir. Ancak Windows, Linux, macOS gibi sistemlerde kullanılan mikroişlemcilerin
    "koruma mekanizması (protection mechanism)" vardır. Bu mekanizma sayesinde bir program zaten kendi alanının dışına çıkıp başka programların alanlarına erişmeye
    çalışırsa mikroişlemci durumu tespit eder bunu işletim sistemine bildirir. İşletim sistemi de ceza olarak programı sonlandırır. Ancak her sistemde 
    koruma mekanizması olmak zorunda değildir. Tabii bir sistemde koruma mekanizması olsa bile bir program yanlışlıkla kendini de bozabilir. Bu nedenle 
    genel olarak tahsis edilmemiş alanlara erişim tanımsız davranışa yol açmaktadır. 

    Buada bir analoji olarak şöyle düşünebiliriz: Bizim parmağımız bir gösterici olsun. Bizim başkasının evini göstermemizde bir sakınca yoktur. Ancak oraya
    erişmemiz bir suç oluşturur. Ancak biz parmağımızla kendi evimizi gösteriyorsak oraya erişebiliriz. Çünkü evimiz zaten bizim için ayrılmış bir yerdir. 
    Biz kendi arazimizi parmağımızla gösteriyor olabiliriz. Oraya erişmemizde sakınca yoktur. Ancak arazinin bizim sınırlarının ötesindeki kısmına 
    erişmemeliyiz. Bu durumu bir dizinin bizim için ayrılan kısmının ötesine erişmeye benzetebiliriz. 

    Ancak bazı sistemlerde bazı alanlar herkesin erişimi için ayrılmış olabilir. Bu tür sistemlerde bu alanlara erişmekte bir sorun ortaya çıkmaz. 
    Tabii C standartları genel olarak tahsis edilmemiş alanlara erişimin tanımsız davranışa yol açacağını belirtmiş olsa da bu tür sistemlerde biz o sisteme 
    özgü olarak bu alanlara erişebiliriz. (Bu durumu parmağımızla bir parkı gösterme durumuna benzetebiliriz. Park herkesin kullanımı için ayrılmıştır. Dolayısıyla
    dolayısıyla park bize tahsis edilmemiş olsa da oraya erişmekte bir sakınca olmaz.)

    Gösterici hatası göstercilerle tahsis edilmemiş bellek alanlarına erişmekler oluşan hatalardır. Gösterici hatalarının tipik olarak ortaya çıkmasının 
    çeşitli biçimleri vardır. Burada bu biçimler üzerinde duracağız.

    - İlkdeğer verilmemiş göstericilerin gösterdiği yerlere erişmek tipik gösterici hatalarındadır. Bir yerel göstericinin içerisinde çöp değer, global 
    bir gsötericinin içerisinde NULL adres bulunur. Bu gösterici * ya da [] operatörleriyle kullanırsak tanımsız davranış oluşur. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        char *pc;		/* pc'nin içerisinde rastgele bir adresin sayısal bileşeni var */

        *pc = 0;		/* tanımsız davranış! rastegele bir yere erişiliyor! */

        return 0;
    }

    Örneğin:

    char *s;

    gets(s);

    Burada gets fonksaiyonu klavyeden (stdin dosyasından) girilen karakterleri s göstericisinin içerisindeki adresten itibaren yerleştirir. s göstericisinin 
    içerisinde rastgele bir adres olduğuna göre klavyeden girilenler rastgele bir yere yerleştirilecektir. Ancak örneğin:

    char s[1024];

    gets(s);

    Burada klavyeden (stdin dosyasından) girilen karakterler tahsis edilmiş olan diziye yerleştirilmektedir.

    - Bir göstericinin içerisine rastgele bir adres yerleştirip o bölgeye erişmememiz gerekir. Örneğin bellekteki belli bir yerde ne var diye 
    o bölgeyi yazdırmaya çalışmamalıyız:

    #include <stdio.h>

    int main(void)
    {
        unsigned char *pc = (unsigned char *) 0x123456;		/* Bu adresteki nesneleri biz tahsis etmemişiz! dikkat */

        for (int i = 0; i < 16; ++i)
            printf("%02X ", pc[i]);
        printf("\n");

        return 0;
    }

    - Bir dizi için bizim belirttiğimiz miktarda yer ayrılmaktadır. Göstericilerle diziler taşırılırsa tanımsız davranış oluşur. Örneğin:

    int a[10];

    for (int i = 0; i <= 10; ++i)		/* dikkat dizi taşırılmış! gösterici hatası! */
        a[i] = 0;

    - Dizilerin taşırılması standart C fonksiyonlarıyla da yanlışlıkla yapılabilmektedir. Örneğin:

    int main(void)
    {
        char s[] = "ankara";
        char d[] = "izmir";

        strcat(d, s);			/* dikkat dizi taşırılmış! */
        puts(d);		

        return 0;
    }

    Örneğin:

    #include <stdio.h>
    #include <string.h>

    int main(void)
    {
        char s[] = "ankara";
        char d[6];

        strcpy(d, s);			/* dikkat dizi taşırılmış! */
        puts(d);		

        return 0;
    }

    - NULL adrese dolaylı bir biçimde erişmeye çalışmak da çok karşılaşılan gösterici hatalarındadır. Örneğin strchr fonksiyonuyla bir yazı içerisinde bir karakteri 
    bulup onu başka bir karakterle yer değiştirmek isteyelim. Pekiyi ya karakteri bulamazsak?

    #include <stdio.h>
    #include <string.h>

    int main(void)
    {
        char s[] = "ankara";
        char *str;

        str = strchr(s, 'x');		/* dikkat! kontrol yapılmamış! */
        *str = 'y';		

        return 0;
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    43. Ders 08/11/2022 - Sali
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Diziler ve göstericiler bazen birbirlerine karıştırılabilmektedir. Bu iki kavram arasındaki benzerlikler ve farklılıklar şunlardır:

    - Hem dizi isimleri hem de göstericller birer adres belirtir.
    - Göstericiler nesne belirtmektedir: Yani biz göstericinin içerisine bir şeyler yerleştirebiliriz. Ancak dizi isimleri nesne belirtmez. 
    Bir dizi ismine bir şey yerleştiremeyiz.
    - Göstericilerin gösterdiği yer tahsis edilmiş olmak zorunda değildir. Yani biz bir göstericiye bir adres atadığımızda adres rastgele bir adres 
    olabilir. Dolayısıyla bu adresteki alan bizim tarafımızdan tahsis edilmemiş olabilir. Ancak dizi isimleri ike belirtilen adresten itibaren bizim
    için ayrılmış olan bir alan vardır. 

    Örneğin:

    int a[] = {10, 20, 30};
    int *pi = a;

    Burada biz ++a gibi bir ifade oluşturamayız. Ancak ++pi gibi bir ifade oluşturabiliriz. a bu dizinin tamamını temsil 
    etmektedir. Bu a ismi program içerisinde kullanıldığında derleyici dizinin başlangıç adresini adreta bir adres sabiti 
    gibi kullanır. Burada a bir nesne belirtmez ancak pi bir nesne belirtmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir gösterici ne kadar yer kaplar? Göstericinin kapladığı yer onun türüyle ilgili midir? Bir göstericinin kendi uzunluğu genel olarak göstericinin 
    türü ile ilgili değildir. Yani bir sistemde int türden gösterici ile long türden gösterici arasında uzunluk bakımından bir fark yoktur. Göstericinin 
    türü onun gösterdeği yer ile ilgilidir. Bir sistemde göstericilerin byte uzunluğu o sistemin teorik bellek kapasitesi ile ilgildir. Örneğin 32 bitlik 
    işlemcilerde belleğin maksimum uzunluğu 4 GB olabilir. 2 üzeri 32 4GB'dir. O halde 32 bit bir sistemde tipik olarak göstericiler 4 byte yer kaplarlar. 
    Ancak 64 bit sistemlerde teorik bellek uzunluğu 16EB'dir. Bu durumda 64 bit sistemlerdeki göstericiler de 8 byte uzunlukta olur. 

    Ancak işletim sistemlerinin önemli bir bölümünde "geriye doğru uyum (backward compatibility)" bulunmaktadır. 64 bit bir işletim sistemi 32 bit prosesörler için
    yazılmış programları da sanki sistemde 32 bitlik prosesör varmış gibi çalıştırabilmektedir. 

    C standartlarında çeşitli gerekçelerle "türleri farklı olan göstericilerin aynı uzunlukta olduğuna yönelik" bir ibare yoktur. Yani burada açıklanan durum
    sistemlerdeki tipik durumdur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    * ve [] operatörlerinin ++ ve -- operatörleriyle işleme sokulmasında programcılar tarafından bazı tereddütler oluşabilmektedir. Biz de bu tereddütleri 
    gidermek için bazı açıklamalar yapacağız. 

    1) [] operatörünün ++ ve -- operatörleriyle kullanımı

    a) ++p[i] ifadesinde p[i] bir artırılmaktadır. Benzer biçimde a = p[i]++ ifadesinde de p[i]'nin değeri a'ya atanıp p[i] bir artırılacaktır.
    
    b) (++p)[i] ifadesinde önce p bir artırılır. Artırılmış adresten i ilerinin içeriğine erişilir. Tabii burada p bir dizi ismi olamaz. Gösteri olmak zorundadır. 
    
    c) p[++i] gibi bir ifadede önce i bir artırılır sonra p adresindne artırılmış indeksin belrriği yere erişilir.
    
    d) p[i++] Burada i bir artılır ancak [] operatörüne i'nin artırılmamış değeri sokulur. Yani p[i] değerine erişilip i bir artırılacaktır. Örneğin:

    int a[3];
    int i = 0;

    a[i++] = 10;
    a[i++] = 20;
    a[i++] = 30;

    e) p++[i] ifadesinde p bir artırılır (tabii p'nin bir gösterici olması gerekir, p bir dizi ismi olamaz) ancak artırılmamış p'den i ilerinin içeriğine erişilr. 

    2) * operatörü ile ++ ve -- operatörlerinin kullanımı

    a) ++*p ifadesinde önce *p'ye erişilir sonra *p bir artırılır. Yani ifade *p = *p + 1 anlamına gelir. 

    b) a = *p++ gibi bir ifade en çok tereddüt edilen ifadelerin başında gelmektedir. Burada ++'ın operandı *p değildi, p'dir. Dolayısıyla burada a'ye *p 
    atanır ancak p de bir artırılmış olur. Bunu şöyle de ifade edebiliriz. Burada p bir artırılır ancak * işlemine p'nin artırılmamış hali sokulur. 
    Bu ifade programlarda çok sık karşımıza çıkar. Örneğin:

    char *mystrcpy(char *dest, char *source)
    {
        char *temp = dest;

        while ((*dest++ = *source++) != '\0')
            ;

        return temp;
    }

    Burada *dest++ = *source++ ifadesinde ++ operatörleri sonek durumundadır. Dolayısıyla source ve dest bir artırılacaktır. Ancak * işlemine bunlarn artmamış değerleri 
    dokulacaktır. Yani burada aslında *dest = *source işlemi yapılıp source ve dest bir artırılmış gibidir. Karşılaştırma işlemine atanan değer sokulmaktadır. 
    Dolayısıyla null karakter de önce hedefe atanacak sonra döngüden çıkılacaktır.

    c) a = *++p ifadesinde önce p bir artırılır sonra artmış adresin içeriği a'ya atanır. 

    d) a = (*p)++ ifadesinde ++ operatörünün operandı artık p değildir, *p'dir. Dolayısıyla burada *p bir artırılır ancak sonraki işlem olan atamaya *p'nin 
    artmamış değeri sokulur. Başka bir deyişle burada *p önce a'ya atanır sonra *p bir artırılır. 

    Aşağıdaki döngüden çıkıldığında str göstericisi nereyi göstermektedir?

    while (*str++ != '\0)
        ;

    Burada str döngüden çıkıldığında null karakterden bir sonrayı gösterecektir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de & operatör bir nesnenin adresini elde eder. Ancak elde edilen ürün bir nesne belirtmez. Yani ++&a gibi bir işlem geçersizdir. 	C'de ++ ve -- operatörleri de 
    nesne belirtmez. Bu nedenle aşağıdaki iki ifade de geçersizdir:

    ++a = 10;		/* geçersiz! */
    a++ = 10;		/* geçersiz! */

    ++a ifadesinde a bir artırılmıştır ancak elde edilen ürün a değildir, a'nın artmış değeridir. Benzer biçimde a++ ifadsinde de durum böyledir. 
    Ancak C++'ta bu konuda bir farklılık vardır. C++'ta öncek ++ ve -- operatörleri nesnenin kendisini üretmektedir. Dolayısıyla ++a = 10 gibi bir ifade 
    her ne kadar mantıksal bakımdan anlamsız olsa da geçerlidir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de char türü dışında hiçbir türün kaç byte uzunlukta olduğu önceden bilinmemektedir. Örneğin farklı sistemlerde int türü farklı uzunluklarda olabilmektedir. 
    Ancak bu durum taşınabilirlik bakımından problem doğurmaktadır. Örneğin:

    int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
    int b[10];

    Biz a dizisinin içeriğini memcpy fonksiyonu ile b dizisine kopyalamak isteyelim. Pekiyi fonksiyonun üçüncü parametresi için ne girmeliyiz?

    memcpy(b, a, 40);

    Bu kod int türünün 4 byte uzunlukta olduğu varsayımı ile yazılmıştır. Eğer int türü ilgili sistemde 8 byte uzunluktaysa biz dizinin ancak yarısını kopyalayabilriz.
    Eğer int türü 2 byte uzunluktaysa b dizisi taşacaktır. 

    sizeof operatörü bir türün o andaki sistemde kaç byte yer kapladığını anlamakta kullanılan bir operatördür. sizeof operatörünü gören derleyici 
    koda size_t türünden sabit bir sayı yerleştirmektedir. sizeof operatörünün üç kullanım biçimi vardır:

    1) sizeof <ifade> ya da sizeof (<ifade>)
    2) sizeof (<tür_ismi>)
    3) sizeof <dizi ismi> ya da sizeof (<dizi ismi>)

    sizeof tek opereand'lı önek bir operatördür. sizeof operatörünün operand'ı bir ifade ise bu ifade paranteze alınabilir ya da alınmayabilir. Ancak 
    sizeof opereatörünün operand'ı bir tür ismi ise bu tür isminin paranteze alınması zorunludur. sizeof operatörün ün operand'ı bir dizi ismi de olabilir. 
    sizoef operatörünün operand'ı bir ifade ise o ifadenin değeri hesaplanmaz. Yalnızca türüne bakılır. sizeof bu durumda o ifadenin türünün kaç byte yer 
    kapladığına ilişkin bir sabit değer üretir. Örneğin:

    size_t result;

    result = sizeof(10 + 2.);

    Burada sizeof operatörü 10 + 2.0 işlemini yapmaz. Bu ifadenin double türünden olduğunu anlarve double türünün ilgili sistemde byte uzunluğuna ilişkin
    sabit bir değer üretir. sizeof operatörü doğrudan bir tür ismi alabilir. Ancak bu durumda tür isminin parantez içerisinde belirtilmesi gerekir. Örneğin:

    result = sizeof (int);

    sizeof operatörünün operand'ı bir dizi ismi olursa sizeof o dizinin tamamının bellekte kaç byte yer kapladığına ilişkin bir değer üretir. Örneğin:

    int a[10];
    size_t result;

    result = sizeof a;

    Burada int 4 byte uzunlukta ise sizeof 40 değerini üretecektir. 
    
    sizeof operatörü öncelik tablosunun ikinci düzeyinde sağdan sola grupta bulunmaktadır:

    () []							Soldan-Sağa
    + - ++ -- !	& *	sizeof	(tür)	Sağdan-Sola
    * / %							Soldan-Sağa
    + -								Soldan-Sağa
    <  > <= >=						Soldan-Sağa
    != ==							Soldan-Sağa
    &&								Soldan-Sağa
    ||								Soldan-Sağa
    ?:								Sağdan-Sola
    =, +=, /=, *=,...				Sağdan-Sola
    ,								Soldan-Sağa	 

    Bu durumda sizeof operatörünün sağındaki ifadenin paranteze alınp alınmaması arasında fark oluşmaktadır. Örneğin:

    result = sizeof 10 + 20.0;

    İ1: sizeof 10 ===> 4 (int'in 4 byte olduğunu varsayalım)
    İ2: İ1 + 20.0
    İ3: result = İ2

    Şimdi ifadeyi paranteze alalım:

    result = sizeof (10 + 20.0)

    Artık burada önce parantez içi ele alınacaktır. Parantez içinin türü double olduğu için sizeof muhtemel olarak 8 değerini üretecektir. Aşağıdaki kullanım 
    geçersizdir:

    result = sizeof int;		/* geçersiz! */

    Burada int paranteze alınmalıydı:

    result = sizeof (int);		/* geçerli */

    Bir göstericinin byte uzunluğu değişik biçimlerde elde edilebilir:

    int *pi;

    result = sizeof (pi);
    result = sizeof (int *);

    sizeof opereatörünün yanındaki ifadenin işletilmeyeceğine dikkat ediniz. Örneğin:

    result = sizeof (a = 10);

    Burada atama yapılmayacaktır. Bu ifade sizeof a ile eşdeğerdir. 

    Dizi ismi adres belirtmektedir. Ancak istisna olarak sizeof operatörüne dizi ismi uygulanırsa adresin kaç byte yer kapladığı değil dizinin 
    toplam kaç byte yer kapladığı bilgisi elde edilir. 

    Bişr fonksiyonun ismine sizeof operatörü uygulanamaz. Yani fonksiyonların kaç byte yer kapladığı sizeof operatörü 
    ile elde edilememektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[10];
    int *pi;
    char *pc;
    size_t result;

    result = sizeof 100 + 2;
    printf("%zd\n", result);        /* 6 */

    result = sizeof (100 + 2);       
    printf("%zd\n", result);        /* 4 */

    result = sizeof(pi);            
    printf("%zd\n", result);        /* 8 */

    result = sizeof(pc);
    printf("%zd\n", result);        /* 8 */

    result = sizeof(a);
    printf("%zd\n", result);        /* 40 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte sizeof operatörü memcpy fonksiyonunda taşıanbilirliği sağlamak için kullanılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
    int b[10];

    /* memcpy(b, a, sizeof a); */
    memcpy(b, a, sizeof(int) * 10);

    for (int i = 0; i < 10; ++i)
        printf("%d ", b[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    sizeof bir operatör olduğu için bir sabit ifadesi oluşturmaktadır. Örneğin:

    int g_a[10];
    char g_b[sizeof g_a];			/* geçerli */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizinin eleman uzunluğunu derleme zamanı sırasında elde edebiliriz. Dizinin ismi a olmak üzere sizeof(a) / sizeof(*a) dizinin eleman uzunluğunu verecektir.
    Böylece biz dizinin eleman sayısı değişse bile onun uzunluğunu her zaman elde etmiş oluruz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    int a[] = {10, 20, 30, 40, 50, 60, 70};
    
    for (int i = 0; i < sizeof(a) / sizeof(*a); ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte C'ye "Değişken Uzunlukta Diziler (Variable Length Arrays)" denilen bir özellik de eklenmiştir. Bu 
    özellik bazıları tarafından eleştirilmektedir. Bu özellik C++'a hiçbir zaman sokulmamıştır. VLA yerel dizilerin 
    uzunluklarının sabit ifadesi biçiminde belirtilme zorunluğunun ortadan kaldırılmasına denilmektedir. Yani C99 
    ve sonrasında biz yerel diziler tanımlarken dizi uzunluğunu sabit ifadesi biçiminde belirtmek zorunda değiliz. Örneğin:

    void foo(void)
    {
        int size;
        
        scanf("%d", &size);

        int a[size];		/* C90'da geçersiz, C++'ta geçerisiz ancak C99 ve ötesinde geçerli */
        
        /* ... */
    }

    Tabii global dizilerde bu durum mümkün değildir. Örneğin:

    int g_size = 10;
    int g_a[g_size];		/* C'nin tüm versiyonlarında geçersiz! */

    Değişken uzunlukta dizilerin tahsis edilebilmesi için derleyici ek makine komutlarıyla stack üzerinde yer açmaktadır. 
    Ayrıca bu özelliğin C++'ta da geçerli olmadığına dikkat ediniz. Bu nedenle bu özelliği gereksiz biçimde kullanmamalısınız.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İngilizce C standartlarında adres bilgisi için de adres tutan nesneler için de "pointer" sözcüğü kullanılmaktadır. Oysa biz adresi tür 
    olarak "adres bilgisi" biçiminde ifade ediyoruz. Biz Türkçe "gösterici (pointer)" terimini adres tutan nesneler için kullanıyoruz. Halbuki İngilizce'de
    "pointer" sözcüğü adres kavramı için genel olarak kullanılmaktadır. C standartlarında address lafı yerine hep "pointer" sözcüğü kullanılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    44. Ders 10.11.2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Biz C'de ne zaman iki tırnak içerisinde bir yazı yazsak derleyici bu iki tırnak içerisindeki yazıyı char türünden statik ömürlü bir dizinin içerisine, 
    dizinin adresini de iki tırnak ifadesi yerine yerleştirir. Örneğin:

    char *str;

    str = "ankara";

    Bu durumda derleyici önce "ankara" yazısını char türden bir diziye yerleştirir, sonuna null karakteri ekler sonra da bu dizinin başlangıç adresini 
    bu string ifadesinin bulunduğu yere yerleştirir. Böylece bu örnekte str göstericisi de içerisinde "ankara" yazısının bulunduğu char türden dizinin 
    adresini gösterecektir. C'de string'ler char türden bir adres belirtirler. Yani bir string'i ancak biz char türden bir göstericiye atayabiliriz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char *str;

    str = "ankara";
    puts(str);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de iki tırnak ifadesinin bu anlamda adres belirtmediği istisna tek bir durum vardır. char türden, unsigned char türden ya da signed char türden
    bir diziye iki tırnak ile ilkdeğer verilirkenki iki tırnak ifadesi istisna bir durumdur ve bir adres belirtmez. Bu istisna durum iki tırnak içerisindeki
    karakterlerin diziye tek tek yerleştirileceği anlamına gelmektedir. Örneğin:

    char s[] = "ankara";

    Buradaki "ankara" bir diziye yerleştirilip o adresi insert edilmez. Bu istisna bir durumdur. Bu durum "ankara" yazısının karakterlerinin tek tek 
    diziye yerleştirileceği anlamına gelir. Başka bir deyişle bu durum aşağıdaki ile eşdeğerdir:

    char s[] = {'a', 'n', 'k', 'a', 'r', 'a', '\0'};

    Ancak char türden bir göstericiye ilkdeğer verirkenki iki tırnak istisna durum değğildir, buradaki iki tırnak adres belirtmektedir. Örneğin:

    char *str = "ankara";

    Burada yine "ankara" yazısı char türden bir diziye yerleştirilir, dizinin adresi de string'in bulunduğu yere yerleştirilir. 

    Anımsanacağı gibi C'de yalnızca char denildiğinde derleyiciye bağlı olarak signed char ya da unsigned char anlaşılmaktadır. Ancak yine de C'de 
    bu üç tür birbirinden farklı türler kabul edilmektedir. Yani çalıştığımız derleyicide char denildiğinde default signed char anlaşılıyor olsa bile adres işlemlerinde
    char ile signed char farklı türler kabul edilir. Bu durumda string'ler char türden adres belirttiğinde göre, ilgili sistemde char türü signed char anlamına gelse bile
    biz bir string'i signed char türünden bir göstericiye yerleştiremeyiz. Örneğin:

    signed char *str;

    str = "ankara";		/* geçersiz! */

    Burada char * türü signed char * türüne atanmıştır. Bu durum geçersizdir. Çünkü char ve signed char türü farklı türler kabul edilmektedir. Halbuki C standartlarında 
    biz iki tırnak ifadesiyle signed char, signed char ya da unsigned char türünden dizilere ilkdeğer verebiliriz:

    char c[] = "ankara";				/* geçerli */
    signed char s[] = "ankara";			/* geçerli */
    unsigned char k[] = "ankara";		/* geçerli */

    Fakat örneğin:

    signed char *str;

    str = "ankara";     				/* geçersiz! */		

    Bir dizi ismine biz bir string'i atayamayız. Dizi isimleri nesne belirtmez. Örneğin:

     char s[] = "ankara";			/* geçerli, dizi elemanlarına ilkdeğer veriliyor, istisna durum */

    s = "izmir";        			/* geçersiz! bir adres dizi ismine atanmaya çalışılıyor */

    İlkdeğer verme tanımlamanın bir parçası olarak yapılan bir işlemdir. Bir değişkeni tanımladıktan sonra ona değer atamak
    ilkdeğer verme anlamına gelmez. Biz char türden, signed char türden, unsigned char türden bir diziye iki tırnak ifadesiyle
    ilkdeğer verebiliriz. Ancak daha sonra dizi isimlerine değer atayamayız. Dizi isimleri nesne belirtmemektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun parametre değişkeni char türden bir gösterici ise biz fonksiyonu bir string ifadesiyle çağırabiliriz. Bu durumda fonksiyonun gösterici 
    parametresine string'in belrttiği yazının başlangıç adresi atanmış olur. Örneğin:

    void foo(char *str)		/* char *str = "ankara" */
    {
        /* ... */
    }
    ...
    foo("ankara");			/* geçerli */

    Örneğin standart puts fonksiyonu char türden bir gösterici parametresine sahiptir. Biz bu fonksiyonu bir string ifadesiyle çağırabiliriz:

    puts("ankara");

    Anımsanacağı gibi dizi isimleri nesne belirtmemektedir. Adeta bunlar birer sembolik sabit gibi düşünülmelidir. Pekiyi bu durumda char türden bir diziye
    ilkdeğer vermenin dışında bir yazı yerleştirmenin en pratik yolu nedir? Akla ilk gelen yöntem karakterleri tek tekl dizi elemanlarına atamaktır. 
    Ancak bu yöntem çok zahmetlidir. Örneğin:

    char s[100];

    s[0] = 'a';
    s[1] = 'n';
    s[2] = 'k';
    s[3] = 'a';
    s[4] = 'r';
    s[5] = 'a';
    s[6] = '\0';

    Bunun en pratik yolu strcpy fonksiyonunu kullanmaktır. Örneğin:

    char s[100];

    strcpy(s, "ankara");

    Burada strcpy fonksiyoınu "ankara" yazısının başlangıç adresinden başlayarak null karakter görene kadar (null karakter dahil) tüm karakterleri 
    bir döngü içerisinde s adresinden itibaren yerleştirecektir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[100];

    strcpy(s, "ankara");
    puts(s);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir string'in karakterlerinin değiştirilmesi "tanımsız davranışa (undefined behavior)" yol açmaktadır. Örneğin:

    char *str = "ankar";

    s[2] = 'x';		/* tanımsız davranış"

    Gerçekten de macOS, Linux ve Windows'taki C derleyicileri string'leri const section'lara yerleştirdikleri için bu 
    durum programın çökmesine yol açmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char *str = "ankara";

    str[2] = 'x';       /* dikkat undefined behavior */
    puts(str);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii char türden, signed char türden ve unsigned char türden dizilere iki tırnakla ilkdeğer verirkenki iki tırnak 
    ifadesi bir string belirtmediği için (istisna durum) yukarıdaki tanımsız davranış söz konusu değildir. Örneğin:

    char str[] = "ankara";

    str[2] = 'x';   	/* tamamen normal */    
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char str[] = "ankara";

    str[2] = 'x';   	/* tamamen normal */    
    puts(str);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki koda bakınız:

    strcpy("veli", "ali");

    Bu kodun derlenmesinde hiçbir problem olmaz. Ancak fonksiyon çağrıldığında "veli" belirtilen string'in karakteri değiştirildiği için "tanımsız davranış"
    oluşacaktır. Aşağıdaki kodu inceleyiniz:

    char s[] = "ankara";

    strcat(s, "izmir");

    Burada derleme aşamsında hiçbir sorun çıkmayacaktır. Ancak program çalışırken strcat s dizisini taşıracaktır. Dolayısıyla bir gösterici hatası 
    söz konusudr ve tanımsız davranış oluşur. Tabii biz s dizisini yeteri kadar büyük açsaydık bir sorun ortaya çıkmayacaktı:

    char s[100] = "ankara";

    strcat(s, "izmir");			/* sorun yok */

    Aşağıdaki gibi bir kodda kişi ne yapmaya çalışıyor olabilir?

    while (*text != '\0' && strchr(";!.,?-*()/ ", *text) != NULL)
        ++text;

    Burada text göstericisinin gösterdiği yerdeki karakterler ";!.,?-*()/ " karakterlerinden biri olduğu sürece yazıda ilerlenmiştir. Bu tema ile biz
    bir yazıdaki sözcüklerin sayısını aşağıdaki gibi bulabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int get_word_count(char *text)
{
    int count = 0;

    for (;;) {
        while (*text != '\0' && strchr(";!.,?-*()/ ", *text) != NULL)
            ++text;
        if (*text == '\0')
            break;
        ++count;
        while (strchr(";!.,?-*()/ ", *text) == NULL)
            ++text;
      
        if (*text == '\0')
            break; 
    }

    return count;
}

int main(void)
{
    char s[] = ";;;;;;   bugun hava guzel! cok guzel...Evet ,,, evet 234 guzel. Sence ali   ";
    int result;

    result = get_word_count(s);
    printf("%d\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Boş bir string söz konusu olabilir. Örneğin:
    
    char *str;

    str = "";

    Burada derleyici yine char türden bir dizinin içerisine yalnızca null karakteri yerleştirir ve yine onun adresi str göstericisine atanacaktır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de iki adres bilgisi toplanamaz. Bu nedenle bazı programlama dilelrinde geçerli olan string toplama işlemi C'de geçerli değildir. Örneğin:

    char *str;

    str = "ali" + "veli";		/* geçersiz! */

    Bu tarzda bir kod Java gibi C# gibi, Python gibi dillerde geçerlidir. Ancak C'de "ali" + "veli" iki adresi toplamak 
    anlamına gelir ve bu durum geçerli değildir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    String'ler char türden adres belirttiğine göre * ve [] operatörleriyle kullanılabilirler. Örneği *"ali" ve "ali"[2] 
    gibi ifadeler geçerlidir.  
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char ch;

    ch = *"ali";
    printf("%c\n", ch);        /* a */

    ch = "ali"[2];
    printf("%c\n", ch);       /* i */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki programda "ankara" yazısının karakter uzunluğu yazdırılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int i;

    for (i = 0; "ankara"[i]; ++i)
        ;
    printf("%d\n", i);      /* 6 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    String'ler aynı satır üzerinde bulunmak zorundadır. String'ler tek bir atom (token) kabul edilmektedir. Örneğin:

    char *str;

    str = "bugun hava
        cok guzel";				/* geçersiz! */

    Pekiyi gerçekten bir string'i farklı satırlara yazamak istersek? Örneğin editörümüzün sütun uzunluğu yeterli olmayabilir. İşte C'de aralarında hiçbir operatör olmayan
    yalnızca boşluk karakterleri olan yan yana iki string derleyici tarafından otomatik olarak birleştirilmektedir. Örneğin:

    char *str;

    str = "ali" "veli";

    Bu işlemin aşağıdakinden bir farkı yoktur:

    str = "aliveli";

    Bu sayede biz string'leri iki farklı satıra bölebiliriz:

    str = "ali"
            "veli";

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char *str;

    str = "bugun hava" 
        " cok guzel";
    puts(str);
        
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de programcıların çok yaptığı hatalarından biri de fonksiyonu yerel bir nesnenin ya da dizinin adresiyle geri döndürmektir. Hiçbir fonksiyon 
    yerel bir nesnenin ya da dizinin adresieyle geri dönmemelidir. Bu durum tanımsız davranışa yol açar. Çünkü fonksiyon sonlandığında bu yerel 
    nesne ya da dizi bellekten yok edileceği için geri döndürülen adres artık tahsis edilmiş olan bir alanın adresi durumunda olmayacaktır. Rastgele 
    bir adres durumunda olacaktır. Örneğin:

    int *foo(void)
    {
        int a = 10;

        return &a;
    }
    ...
    int *pi;

    pi = foo();		/* dikkat! foo fonksiyonun geri döndürdüğü adres güvenli bir adres değil! */

    printf("%d\n", *pi);

    Aşağıdaki tarzda gösterici hataları sık yapılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

char *getname(void)
{
    char s[1024];

    printf("Adi soyadi:");
    gets(s);

    return s;
}

int main(void)
{
    char *str;

    str = getname();        /* gösterici hatası */
    puts(str);
        
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de string'ler statik ömürlü nesnelerdir. Yani string'ler programın belleğe yüklenmesiyle yaratılırlar, program sonlanana kadar bellekte kalırlar.
    Biz bir fonksiyon içerisinde bir string kullandığımızda fonksiyondan çıksak bile string yaşamaya devam eder. Bu nedenle biz örneğin bir string'in 
    başlangıç adresiyle bir fonksiyonu geri döndürebiliriz. Çünkü fonksiyon sonlansa bile string programın sonuna kadar bellekte kalmaya 
    devam edecektir. Örneğin:

    char *name(void)
    {
        return "ali";		/* tammaen normal *()
    }

----------------------------------------------------------------------------------------------------------------------*/
        
/*----------------------------------------------------------------------------------------------------------------------
                                                    45. Ders - 15/11/2022 Sali
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    String'ler için yer derleme aşamasında ayrılır. Program çalışmak üzere belleğe yüklendiğinde string'in adresi bellidir. Yani programın akışı string'i 
    her gördüğünde string için yer ayrılmaz. String için tek bir yer ayrılır. String'in adresi de derleme sırasında elde edilip string yerine yerleştirilir. 
    (Program yüklenmeden derleyicinin string'in adresini nasıl belirlediği konusunda tereddütleriniz olabilir. Ancak bu işlem biraz karmaşık 
    süreçlerle yürütülmektedir.) Dolayısıyla aşağıdaki kodda ekrana hep aynı adres basılır. Farklı adresler basılmaz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char *str;

    for (int i = 0; i < 10; ++i) {
        str = "ankara";
        printf("%p\n", str);			/* hep aynı adres yazdırılır */
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir program içerisinde tamamen özdeş string'ler kullanıldığında bunlar için tek bir yer mi ayrılacağı ya da her özdeş string için ayrı yerler mi 
    ayrılacağı derleyicileri yazanların isteğine bırakılmıştır (implementation dependent). Pek çok derleyicide bu durum derleyici seçeneklerinden 
    ayarlanabilmektedir. Örneğin Microsoft C derleyicilerinde bu ayar Visual Studio'da proje seçeneklerinden ""C-C++/Code Generation/Enable String Pooling" seçeneği ile 
    ayarlanabilmektedir. Komut satırından /GF seçeneği kullanılmaktadır. Microsoft C derleyicilerinde default durumu böyledir. gcc ve clang derleyicilerinde de 
    default durumda özdeş string'ler için tek bir yer ayrılmaktadır. Modern amaç dosya formatlarında bağlayıcının farklı amaç dosyalardaki özdeş string'lerin
    tek bir kopyasının çalıştırılabilir dosyaya yerleştirilmesi de sağlanabilmektedir. Yani pek çok derleyici projenin farklı C dosyalarındaki özdeş string'leri de
    tek bir string olarak ele alabilmektedir.

    Bu durumu aşağıdaki programla tets edebilirsiniz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char *s;
    char *k;

    s = "ankara";
    k = "ankara";

    printf("%p, %p\n", s, k);

    return 0;
}
  
/*----------------------------------------------------------------------------------------------------------------------
    C standartlarına göre İngilizcedeki karakterler ve operatör semboller ve noktalama işaretlerine ilişkin semboller 1 byte yer kaplamalıdır. 
    Yani biz bu karakterlerin 1 byte yer kaplayacağı bir encoding'i kullanmak zorundayız. Ancak diğer karakterler (örneğin Türkçe karakter) için 
    standartlar bu bir byte zorunluluğu belirtmemiştir. Dolayısıyla temel karakterler 1 byte yer kaplarken örneğin Türkçe karakterler birden fazla byte yer kaplayabilir. 
    Bu durumda bir C programı tamamen ASCII karakterleriyle, ASCII tablosunun code page'leriyle ya da Unicode UTF-8 encoding'i ile kodlanmış olabilir. 
    Tabii derleyicilerin bu kodlama biçimlerini destekliyor olması gerekir. Günümüzde Unicode karakter tablosu yaygın kullanılmaktadır. Unicode karakter 
    tablosunun çeşitli encoding'leri bulunmaktadır. UNOCODE'un temel encoding'i UTF-16 denilen her bir karakterin iki byte kodlandığı encoding'tir. 
    Ancak ASCII karakterlerin bir byte ile diğer karakterlerin birden fazla byte ile kodlandığı UTF-8 encoding'i en yaygın kullanılan encoding'tir. 

    C'de karakterler 1 byte yer kaplamaktadır. Ancak 1 byte'tan uzun olan karakter tablolarındaki karakterleri belirtmek için C'de "geniş karakter (wide character)"
    denilen bir tür de oluşturulmuştur. Geniş karakterlerin hangi karakter tablosunu temel aldığı satndartlarda belirtilmemiştir. Ancak Microsoft Unicode UTF-16 
    encoding'i olarak, gcc ve clang derleyicileri Unicode UTF-32 encoding'i ele almaktadır. 

    C'de geniş karakterler wchar_t türü ile temsil edilmektedir. wchar_t bir anahtar sözcük değildir. Bir typedef ismidir. Dolayısıyla aslında başka bir türü 
    temsil etmektedir. C standartlarına göre wchar_t işaretsiz bir tamsayı türü olarak typedef edilmek zorundadır. Örneğin bu tür Microsoft derleyicilerinde 2 byte'lık
    unsigned short olarak, gcc ve clang derleyicilerinde 4 byte'lık unsigned int olarak typedef edilmiştir. wchar_t türü <stddef.h> dosyası içerisinde typede edilmiştir. 
    Dolayısıyla bu tür ismini kullanmak için bu dosyanın include edilmesi gerekmektedir. 

    C'de tek tırnak içerisindeki karakterin önüne onunla yapışık bir L harfi getirilirse böyle karakter geniş karakter sabiti olarak ele alınır. Dolayısıyla 
    geniş karakter sabitlerini biz wchar_t türünden nesnelerin içerisine yerleştirmeliyiz. Örneğin:

    wchar_t ch;

    ch = L'ş';

    Burada derleyicilerin geniş karakterleri kodlamak için hangi karakter tablosunu temel alacağı standartlarda belirtilmemiştir. Ancak yukarıda da belirttiğimiz gibi 
    Microsoft derleyicileri Unicode UTF-16 encoding'ini temel almaktadır. gcc ve clang derleyicileri geniş karakterler için Unicode UTF-32 encoding'ini 
    kullanmaktadırç 

    Özetle geniş karakterler derleyiciler tarafından 1 byte'tan daha uzun byte'larla kodlanmaktadır. Ancak standartlar geniş karakter kodlamasının hangi karakter tablosu
    ve encoding'i dikkate alınarak yapılacağı konusunda bir belirlemede bulunmamıştır. Derleyicilerin çoğu geniş karakterleri Unicode UTF-16 ya da Unicode UTF-32 encoding'ine
    göre kadlamaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stddef.h>

int main(void)
{
    wchar_t ch;

    ch = L'ş';
    printf("%llu\n", (unsigned long long)ch);		/* Muhtemelen 351 yani Unicode UTF-16 'ş' */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Geniş karakter dizileri ve string'leri de söz konusudur. Örneğin:

    wchar_t s[] = L"ağrı dağı";
    wchar_t *str;

    str = L"ağrı dağı";

    Başında L olan string'ler wchar_t türünden bir diziye yerleştirilir. Bunlar wchar_t türünden bir adres belirtirler. Aslında C'de geniş karakterlerle ilgili 
    yazma yapan ek fonksiyonlar da bulunmaktadır. Örneğin printf fonksiyonunun wprintf isminde geniş karakterlerle çalışan bir biçimi de vardır. 
    Ancak bu durum wprintf fonksiyonu ile bizim geniş karakterleri bastıracğımız anlamına gelmemektedir. wprintf fonksiyonu format karakter yazısını da 
    geniş karakter string'i olarak alır. Başında w olan IO fonksiyonları geniş karakterlerle çalışmaktadır. Bunların prototipleri <wchar.h> dosyası
    içerisinde bulunur. Ancak yukarıda da belirttiğimiz gibi örneğin wprintf fonksiyonu ile %s seçeneği kullanılarak bir geniş karakterli yazı yazdırılmak 
    istendiğinde bu yazıyı biz ekranda göremeyebiliriz. Çünkü bir yazının ekranda istediğimiz görüntülenmesi o anda ekran için kullanılan aygıt sürücünün kabul ettiği encoding'e
    de bağlı olmaktadır. wprintf geniş karakterleri terminal aygıt sürücüsüne gönderir. Ancak aygıt sürücü eğer geniş karakterlere uygun bir encoding'e ayarlı değilse 
    bunları gösteremez. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C11 ile birlikte C'ye Unicode UTF-16 ve Unicode UTF-32 encoding'lerine ilişkin karakter sabitleri ve string'ler de eklenmiştir. Unicode UTF-16 karakterleri 
    tek tırnağa yapışık u harfi ile, Unicode UTF-32 karakterleri ise tek tırnağa yapışık U karakteri ile temsil edilmektedir. Bu karakterlerin saklanması için 
    char16_t ve char32_t isimli typedef türleri bildirilmiştir. Bu türler <uchar.h> başlık dosyasında typedef edilmiştir. Örneğin:

    char16_t x = u'ş';			/* Unicode UTF-16 */
    char32_t y = U'ş';			/* Unicode UTF-32 */
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <uchar.h>

int main(void)
{
    char16_t x = u'ş';		/* Unicode UTF-16 */
    char32_t y = U'ş';		/* Unicode UTF-16 */

    printf("%llu\n", (unsigned long long)x);		/* 351 */
    printf("%llu\n", (unsigned long long)y);		/* 351 */

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------
    Benzer biçimde biz C11 ile birlikte bir string'e de yapışık olarak u ve U karakterlerini ekleyebiliriz. Bu durumda bu string'ler Unicode UTF-16 ve 
    Unicode UTF-32 encoding'lerine göre kodlanırlar. Örneğin:

    char16_t s[] = u"ağı dağı";
    char16_t *k = u"ağrı dağı";
    char32_t m[] = U"ağı dağı";
    char32_t *r = U"ağrı dağı";

    Tabii u"xxx" biçimindeki bir string char16_t türünden, U"xxx" biçimindeki string char32_t türünden adres belirtmektedir. char16_t ve char32_t 
    16 bitlik ve 32 bitlik işaretli tamsayı türü olarak typedef edilmek zorundadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C11 ile birlikte Unicode UTF-8 string'leri de C'ye eklenmiştir. Böyle stringlerin önüne onlarla yapışık olarak u8 öneki getirilir. 
    Böyle string'ler için özel bir tür düşünülmemiştir. Bu string'ler de yine char türdendir. Örneğin:

    char s[] = u8"ağrı dağı";		/* Unicode UTF-8 */

    C11'de ve C17'de Unicode UTF-8 karakter sabiti bulunmamaktadır. Ancak C23'te bu karakter sabitlerinin de (yani u8'x' gibi) eklenmesi düşünülmektedir. 
    C23'e kadar u8 önekli string'ler char türden adres belirtiyordu. C23 ile birlikte char8_t biçiminde yeni bir tür daha eklendi. C23 ve sonrasında 
    u8 önekli karakterler ve string'ler char8_t türrüne ilişkindir. C++17 ile birlikte bu karakter sabitleri de C++'a eklenmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi diziler elemanları aynı türden olan ve bellekte ardışıl bir biçimde bulunan veri yapılarıdır. Örneğin 
    int bir dizinin tüm elemanları int türdendir. double bir dizinin double türdendir. İşte her elemanı bir adres tutan dizilere
    gösterici dizileri denilmektedir. Yani gösterici dizilerinin her elemanı bir göstericidir. Gösterici dizileri dekleratörde 
    hem köşeli parantez hem de * ile bildirilir. Örneğin:

    int a[10];		/* int türden bir dizi */
    int *b[10];		/* int türden bir gösterici dizisi, b'nin her elemanı int türden bir adres tutar */
    char *c[5];		/* c'nin her elemanı char türden bir adres tutar */

    Gösterici dizilerindeki * atomu türe ilişkin değil dekleratöre ilişkindir. Örneğin:

    int a, b[10], *c[10];
    
    Burada a int türden bir nesne olarak tanımlanmıştır. b ise int türden 10 elemanlı bir dizidir. c de her elemanı int türden 
    gösterici olan 10 elemanlı bir dizidir. 
    
    Bir gösterici dizisinin her elemanı bir göstericidir. Dolayısıyla bir adres atanmalıdır. Örneğin:

    int x = 10, y = 20, z = 30;
    int *a[3];

    a[0] = &x;
    a[1] = &y;
    a[2] = &z;

    a bir gösterici dizisini belirtiyor olsun. O zaman a[i] bu gösterici dizisinin i'inci indisli elemanıdır. Yani bir adres belirtir. O halde *a[i] 
    ifadesinde [] operatörü öncelikli olduğu için önce dizi elemanına erişir, sonra o adresteki nesne elde edilir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int x = 10, y = 20, z = 30;
    int *a[3];

    a[0] = &x;
    a[1] = &y;
    a[2] = &z;

    for (int i = 0; i < 3; ++i)
        printf("%d\n", *a[i]);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Gösterici dizilerine de küme parantezleri içerisinde ilkdeğer verilebilir. Tabii verilen ilkdeğerlerin adres belirtmesi gerekir. Örneğin:

    int x = 10, y = 20, c = 30;
    int *a[] = {&x, &y, &z};

    Ya da örneğin:

    int x[] = {1, 2, 3, 4, 5}, y[] = {6, 7}, z[] = {8, 9, 10};
    int *a[] = {x, y, z};

    Burada a gösterici dizisi x, y ve z dizilerinin başlangıç adreslerini göstermektedir. Bu dizilerin aynı uzunlukta olması gerekmez. 
    Kme parantezleriyle gösterici dizilerinin az sayıda elemanına ilkdeğer verilirse ilkdeğer verilmeyen elemanlara derleyici 
    tarafından NULL adres yerleştirilmektedir. (Yani 0 adresi yerleştirilmemektedir. Tabii yaygın derleyicilerin hepsinde NULL adres zaten 0 adresidir.)
    
    a bir gösterici dizisi olsun. Bu dizinin i'inci indisli elemanı da bir diziyi gösteriyor olsun. Bu durumda a[i][k] gibi bir ifade geçerlidir. 
    Bu ifade a'nın i'indisli elemanı olan göstericinin gösterdiği yerdeki dizinin k'ıncı elemanı anlamına gelir. [] operatörünün solda-sağa
    öncelikli olduğunu anımsayınız. a[i]'den bir adres elde edilecek o adrese [k] operatörü uygulanacaktır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int x[5] = {1, 2, 3, 4, 5}, y[5] = {6, 7, 8, 9, 10}, z[5] = {11, 12, 13, 14, 15};
    int *a[3] = {x, y, z};

    for (int i = 0; i < 3; ++i) {
        for (int k = 0; k < 5; ++k)
            printf("%d ", a[i][k]);
        printf("\n");
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında C'de en fazla karşılaşılan gösterici dizileri char türden gösterici dizileridir. Çünkü bir grup yazının başlangıç adresleri char türden bir gösterici
    dizisinde saklanabilir. Böylece char türden gösterici dizileri adeta string dizileri gibi kullanılır. Örneğin:

    char *names[3];

    Burada names 3 elemanlı, her elemanı char türden bir adres tutan göstericidir. O halde biz bu dizinin her elemanına char türden bir adres yerleştirebiliriz:

    names[0] = "ali";
    names[1] = "veli";
    names[2] = "selami";

    Burada "ali", "veli" ve "selami" string'leri derleyici tarafından güvenli yerlere (static ömürlü char türden dizilere), 
    bunların sonlarına null karakter eklenecek ve derleyici de bu string'ler yerine char türden adresler, kullanacaktır. 
    O halde buradaki names dizisi aslında bu yazıların başlangıç adreslerini tutan bir dizi haline gelmiştir. Şimdi bu 
    yazıları yazdıralım:

    for (int i = 0; i < 3; ++i)
        puts(names[i]);
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char *names[3];

    names[0] = "ali";
    names[1] = "veli";
    names[2] = "selami";

    for (int i = 0; i < 3; ++i)
        puts(names[i]);

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------
    Tabii char türden gösterici dizilerine doğrudan string'lerle de ilkdeğer verebilirdik. Örneğin:

    char *names[3] = {"ali", "veli", "selami"};

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char *names[3] = {"ali", "veli", "selami"};

    for (int i = 0; i < 3; ++i)
        puts(names[i]);

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------											
                                            46. Ders - 17/11/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bazen programcı gösterici dizisinin sonuna NULL adres yerleştirir(null karakter değil NULL adres). Böylece NULL adres görene kadar dizinin bütün 
    elemanlarına erişebilir. NULL adresin geçerli bir adres belirtmediğine dikkat ediniz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char *names[] = {"ali", "veli", "selami", "suleyman", "fatih", "ayse", NULL};

    for (int i = 0; names[i] != NULL; ++i)
        puts(names[i]);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte bir grup isim arasında en uzun karakterden oluşan isim bulunmaktadır. İsimlerin uzunluklarını strlen fonksiyonuyla elde edebiliriz. 
    Ancak bu tür durumlarda strlen gibi fonksiyonların gereksiz biçimde aynı yazı için yeniden çağrılmasını elimine etmelisiniz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char *names[] = {"ali", "veli", "selami", "suleyman", "fatih", "ayse", NULL};
    size_t max_index, max_length, length;

    max_index = 0;
    max_length = strlen(names[0]);

    for (int i = 1; names[i] != NULL; ++i) {
        length = strlen(names[i]);
        if (length > max_length) {
            max_index = i;
            max_length = length;
        }
    }

    puts(names[max_index]);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Şimdi de bir grup ismi sözlükteki sırasına göre sıraya dizelim. Bunun için strcmp fonksiyonunu kullanabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

#define SIZE		12

int main(void)
{
    char *names[SIZE] = {"ali", "veli", "selami", "suleyman", "fatih", "ayse", "salih", "burhan", "can", "sibel", "jale", "temel"};
    char *temp;
    int flag;
    size_t  i;

    i = 0;
    do {
        flag = 0;
        for (size_t k = 0; k < SIZE - 1 - i; ++k)
            if (strcmp(names[k], names[k + 1]) > 0) {
                temp = names[k];
                names[k] = names[k + 1];
                names[k + 1] = temp;
                flag = 1;
            }
        ++i;
    } while (flag);

    for (size_t i = 0; i < SIZE; ++i)
        printf("%s ", names[i]);
    printf("\n");

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------
    En fazla üç basamak olan bir sayıyı yazı ile yazdıran örnek bir program.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

void disp_number(int number)
{
    char *ones[] = {"", "bir", "iki", "uc", "dort", "bes", "alti", "yedi", "sekiz", "dokuz"};
    char *tens[] = {"", "on", "yirmi", "otuz", "kirk", "elli", "altmis", "yetmis", "seksen", "doksan"};
    int one, ten, hundred;

    if (number == 0) {
        printf("sifir\n");
        return;
    }

    one = number % 10;
    ten = number / 10 % 10;
    hundred = number / 100;

    if (hundred > 0) {
        if (hundred != 1)
            printf("%s ", ones[hundred]);
        printf("yuz");
    }
    if (ten > 0) {
        if (hundred > 0)
            putchar(' ');
        printf("%s", tens[ten]);
    }
    if (one > 0){
        if (number > 10)
            putchar(' ');
        printf("%s", ones[one]);
    }
    putchar('\n');
}

int main(void)
{
    int number;

    for (;;) {
        printf("En fazla 3 basamakli bir sayi giriniz:");
        scanf("%d", &number);
        if (number == -1)
            break;
        if ((int)log10(number) + 1 > 3) {
            printf("sayi 3 basamaktan buyuk!\n");
            continue;
        }

        disp_number(number);
    }

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------
    Aslında orijinal printf fonksiyonunun prototipi şöyledir:

    int printf(const char *format, ...);

    Bu prototipteki const anahtar sözcüğünü henüz görmedik. Prototipteki "..." fonksiyonun değişken sayıda argüman alacağını belirtmektedir. 
    Biz şimdiye kadar hep printf fonksiyonunun format parametresini bir string olarak girdik. Tabii aslında bu parametre char türden bir adres almaktadır. 
    Örneğin biz format yazısını char türden bir dizinin içerisine yerleştirip onun adresini de printf fonksiyonuna verebiliriz:

    #include <stdio.h>

    int main(void)
    {
        int a = 10, b = 20;
        char format[] = "a = %d, b = %d\n";

        printf(format, a, b);
    
        return 0;
    }

    printf fonksiyonunun geri dönüş değeri int türdendir. printf stdout dosyasına (ekrana) yazılan karakterlerin sayısına geri dönmektedir. Tabii printf de 
    başarısız olabilir (böylesi bir şey normalde mümkün değildir) bu durumda printf negatif herhangi bir değerle geri dönmektedir. Tabii printf fonksiyonun geri 
    dönüş değerini genellikle kontrol etmeyiz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 10, b = 20;
    char format[] = "a = %d, b = %d\n";
    int result;

    result = printf(format, a, b);
    printf("%d\n", result);     /* 15 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    sprintf fonksiyonu printf fonksiyonunun kardeşi olan bir fonksiyondur. sprintf bir parametre fazlalığa sahiptir. Orijinal prototipi şöyledir:

    int sprintf(char *buf, const char *format, ...);

    Fonksiyonun printf'e göre ilk parametre fazladır. sprintf tamamen printf gibi çalışır ancak çıktıyı stdout dosyasına yazmak yerine birinci parametresiyle verilen 
    dizinin içerisine yazar. Tabii yazının sonuna null karakteri de yerleştirir. Fonksiyon yine diziye yerleştirdiği karakter sayısına geri dönmektedir (null karakter dahil değildir)
    Örneğin:

    #include <stdio.h>

    int main(void)
    {
        char buf[1024];
        int a = 10, b = 20;
        int result;

        result = sprintf(buf, "a = %d, b = %d", a, b);
    
        printf("buf = %s, result = %d\n", buf, result);
    
        return 0;
    }

    Şimdi printf varken sprintf fonksiyonuna neden gereksinim duyulduğunu düşünebilirsiniz. İşte bazen bir yazıyı bekletip belli koşullarda yazdırabiliriz. 
    Bazen yazıyı yazdırmayız bir sokettten karşı tarafa yollamak isteyebiliriz. Bazen de bir GUI ortam söz konusu olabilir. Bu ortamda yalnızca bir yazı yazdırılıyor olabilir. 
    Biz de mecburen önce yazdırmak istediklerimizi bir yazı biçimine dönüştürürüz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char buf[1024];
    int a = 10, b = 20;
    int result;

    result = sprintf(buf, "a = %d, b = %d", a, b);
    
    printf("buf = %s, result = %d\n", buf, result);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Üç basamaklı sayıyı ekrana (stdout dosyasına) yazdıran yukarıdaki örneği char türden bir dizi içerisine yazı olarak yerleştiren örnek biçiminde değiştirebiliriz. 
    Bu tür kodlarda bir yazının sona sürekli olarak başka yazıların eklenmesi gerekebilmektedir. Programcılar genellikle bu tür eklemeleri düz mantık ile 
    strcat fonksiyonunu kullnarak yapma eğilimindedir. Halbuki sprintf fonksiyonun geri dönüş değerinden hareketle bu işlemler daha pratik yapılabilmektedir. 
    Örneğin:

    index = 0;
    char buf[1024];
    ...

    index += sprintf(buf, "a = %d, b = %d", a, b);
    index += sprintf(buf, "-this is a test-");
    
    Burada sonraki yazı ilk yazının sonuna eklenecektir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <math.h>

void num2text(int number, char *buf)
{
    char *ones[] = {"", "bir", "iki", "uc", "dort", "bes", "alti", "yedi", "sekiz", "dokuz"};
    char *tens[] = {"", "on", "yirmi", "otuz", "kirk", "elli", "altmis", "yetmis", "seksen", "doksan"};
    int one, ten, hundred;
    int index = 0;

    if (number == 0) {
        strcpy(buf, "sifir");
        return;
    }

    one = number % 10;
    ten = number / 10 % 10;
    hundred = number / 100;

    if (hundred > 0) {
        if (hundred != 1)
            index += sprintf(buf + index, "%s ", ones[hundred]);
        index += sprintf(buf + index, "yuz");
    }
    if (ten > 0) {
        if (hundred > 0)
            buf[index++] = ' ';
        index += sprintf(buf + index, "%s", tens[ten]);
    }
    if (one > 0) {
        if (number > 10)
            buf[index++] = ' ';
        index += sprintf(buf + index, "%s", ones[one]);
    }
}

int main(void)
{
    int number;
    char str[1024];

    for (;;) {
        printf("En fazla 3 basamakli bir sayi giriniz:");
        scanf("%d", &number);
        if (number == -1)
            break;
        if ((int)log10(number) + 1 > 3) {
            printf("sayi 3 basamaktan buyuk!\n");
            continue;
        }

        num2text(number, str);
        printf(":%s:\n", str);
    }

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------
                                                    47 Ders - 22/11/2022 - Sali
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizinin ismi dizinin başlangıç adresini belirtiyordu. Ve bu adres dizinin ilişkin olduğu tür türündendi. Başka bir deyişle bir dizinin ismi
    dizinin ilk elemanın adresiydi. Pekiyi bir göstericisinin ismi ne belirtmektedir? Bir gösterici dizisinin ismini bir göstericiye atayacaksak 
    göstericinin hangi türden olması gerekir? Örneğin:

    int *a[10];

    Burada a dizisi int * türünden elemanları tutmaktadır. Bir dizinin ismi dizinin ilk elemanın adresi anlamına geleceğine göre ve bu dizinin de ilk elemanı int türden 
    bir gösterici olduğuna göre int türden bir göstericinin adresi nasıl bir göstericiye atanmalıdır? İşte C'de bir göstericinin adresi ** ile temsil edilen 
    göstericiyi gösteren bir göstericiye atanabilmektedir. Çrneğin:

    int *a[10];
    int **ppi;

    ppi = a;

    Bu konu ileride ayrı bir başlık altında ele alınacaktır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Rastgele sayı üretimi programlamada pek çok alanda kullanılmaktadır. Örneğin Tetris gibi bir oyunda düşen şekillerin rastgele olması için rastgele sayı
    üretilmesi gerekmektedir. Bilgisayarda rastgele sayı üretme işlemi artimetik yolla yapılmaktadır. Bu biçimde rastgele sayı üretilmesine "pseudo random number generation"
    denilmektedir. Rastgele sayı üretiminde belli bir tohum (seed) değer alınır. O değer üzerinde bir işlem uygulanır, rastgele bir değer elde edilir. 
    Elde edilen rastgele değer yeniden bir işleme sokulur ondan da rastgele değer elde edilir. Böylece bir dizi rastgele değer elde edilmiş olur. 
    İlk tohum değer aynı olursa hep aynı rastgele sayılar elde edilecektir. Bir sayının bir sayıya bölümünden elde edilen kalan rastegelik içermektedir. 
    Benzer biçimde sayının ortadaki n basamağının karesi de rastgele bir değer oluşturmaktadır. Yani rastegele sayı elde etmek için buna benzer çeşitli yöntemler kullanılmaktadır. 

    C'de rastgele sayı üretimi ile ilgili iki standart C fonksiyonu vardır: rand ve srand. Bu fonksiyonların prototipleri <stdlib.h> içerisinde bildirilmiştir. 
    rand fonksiyonu her çağrıldığında 0 ile RAND_MAX arasında rastgele int ütrdne bir tamsayı değeri üretir. RAND_MAX <stdlib.h> içerisinde bildirilmiş olan bir sembolik sabittir. 
    Microsoft C derleyicilerinde 32767, gcc derleyicilerinde 2147438647 olarak define edilmiş durumdadır. Standartlara göre kaç olarak define edileceği derleyicileri 
    yazanların isteğine bırakılmıştır. Standartlar RAND_MAX için minimum değerin 32767 olableceğini de belirtmektedir. Yani derleyicilerimiz RAND_MAZ için bu değerden daha küçük bir değer
    define edemez. rand fonksiyonun prototipi şöyledir:

    int rand(void);

    Tabii eğer biz daha dar bir aralıkta rassal sayı elde etmek istiyorsak bu durumda rand fonksiyonundan elde edilen değere mod işlemi uygularız. 
    rand fonksiyonunun kullandığı tohum değer program her çalıştırıldığında hep aynı değerden başlatıldığı için programın her çalışmasında aynı rassal sayılar 
    elde edilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int val;

    for (int i = 0; i < 20; ++i) {
        val = rand() % 10;
        printf("%d ", val);
    }
    printf("\n");

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------
    Rassal sayı üretiminde kullanılabilecek tohum değer srand fonksiyonuyla değiştirilebilmektedir. srand fonksiyonun prototipi şöyledir:

    void srand(unsigned int seed);

    Pekiyi programın her çalışmasında farklı bir rassal diziliminin elde edilmesi için ne yapılabilir? srand fonksiyonu ile işin başında bir tohum değer belirlemekle
    bu işlem yapılamaz. Örneğin:

    #include <stdio.h>
    #include <stdlib.h>

    int main(void)
    {
        int val;

        srand(123456);

        for (int i = 0; i < 20; ++i) {
            val = rand() % 10;
            printf("%d ", val);
        }
        printf("\n");

        return 0;
    }
 
    Burada üretilen 20 değer bir önceki programdaki 20 değerden farklı olacaktır. Ancak yine program her çalıştığında tohum değer aynı olacağı için aynı değerler
    elde edilecektir. Tabii srand çağırmasını dönügünün içerisine yerleştirirsek bu defa hep aynı sayıyı elde ederiz:
    
    #include <stdio.h>
    #include <stdlib.h>

    int main(void)
    {
        int val;

        for (int i = 0; i < 20; ++i) {
            srand(123456);
            val = rand() % 10;
            printf("%d ", val);
        }
        printf("\n");

        return 0;
    }
 
    Programın her çalışmasında farklı bir rastgele sayı kümesinin elde edilmesi için programın her çalışmasında tohum değerin farklı bir değerle başlatılması gerekir. 
    İşte bunun için time isimli standart C fonksiyonunda faydalanılmaktadır. Bu fonksiyon ileride açıklanacaktır. Ancak fonksiyon her çağrıldığında belli bir noktadan 
    itibaren çağrılma zamanına kadarki saniye sayısını vermektedir. Genellikle bu belli nokta 01/01/1970 olarak alınmaktadır. time fonksiyonunun prototipi <time.h> 
    dosyası içerisindedir. O halde problem şöyle çözülebilir:

    #include <stdio.h>
    #include <stdlib.h>
    #include <time.h>

    int main(void)
    {
        int val;

        srand(time(NULL));

        for (int i = 0; i < 20; ++i) {
            val = rand() % 10;
            printf("%d ", val);
        }
        printf("\n");

        return 0;
    }
 
    Bu konuda en sık yapılan hatalardan biri de srand çağırmasını döngü içerisine yerleştirmektir. Örneğin:

    for (int i = 0; i < 20; ++i) {
        srand(time(NULL));
        val = rand() % 10;
        printf("%d ", val);
    }

    Burada time fonksiyonu 01/01/1970'ten çağrım zamanına kadar geçen saniye sayısını vereceğine göre aslında tohum değer hep aynı değerle başlatılacaktır. srand(time(NULL))
    çağırması programın başında yalnızca bir kez yapılmalıdır. 

    time fonksiyonunun 01/01/1970'ten geçen saniye sayısını vermesi zorunlu değildir. Ancak geleneksel olarak bu fonksiyon bu biçimde davranmaktadır. UNIX türevi sistemlerde
    ise time fonksiyonunun 01/01/1970'ten geçen saniye sayısını vereceği POSIX standartlarında garanti edilmektedir. 

    Dennis Ritchie ve Brian Kernigan tarafından yazılmış olan "The C Programming Language" kitabının ikinci baskısında (edition) rand ve srand fonksiyonlarının olası 
    gerçekleştirimi aşağıdaki gibi verilmiştir:

    unsigned long int next = 1;
    
    int rand(void)
    {
        next = next * 1103515245 + 12345;

        return (unsigned int)(next/65536) % 32768;
    }
        
    void srand(unsigned int seed)
    {
        next = seed;
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte 5 kişi arasında rastgele kişiler seçilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NPERSONS	5

int main(void)
{
    int index;
    char *names[] = {"ali", "veli", "selami", "ayse", "fatma"};

    srand(time(NULL));

    for (int i = 0; i < 10; ++i) {
        index = rand() % NPERSONS;
        printf("%s\n", names[index]);
    }

    return 0;
}
 
/*----------------------------------------------------------------------------------------------------------------------
    1 ile 49 arasında birbirinden farklı 6 sayı üreten bir fonksiyon aşağıdaki gibi yazılabilir. Fonksiyonun prototipi şöyledir:

    int *get_lotto_column(int *col);

    Fonksiyon 6 elemanlı bir int dizinin adresini parametre olarak alır ve aldığı adrese geri döner. Fonksiyon rastegele ürettiği sayıları bu diziye yerleştirmektedir. 

    Asında aşağıdaki yöntem her zaman uygun bir yöntem değildir. Örneğin 100 tane sayıdan birbirinin aynısı olmayan 95 sayı elde edeceğimiz zaman bu yöntem 
    çok fazla zamana mal olabilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int *get_lotto_column(int *col);

int main(void)
{
    int a[6];

    srand(time(NULL));

    get_lotto_column(a);

    for (int i = 0; i < 6; ++i)
        printf("%d ", a[i]);
    printf("\n");

    return 0;
}

void get_lotto_column(int *col)
{
    int val;
    int flag;

    for (int i = 0; i < 6; ++i) {		
        do {
            flag = 0;
            val = rand() % 49 + 1;
            for (int k = 0; k < i; ++k) {
                if (col[k] == val) {
                    flag = 1;
                    break;
                }
            }
        } while (flag);

        col[i] = val;
    }
    return col;
}
 
/*----------------------------------------------------------------------------------------------------------------------
                                                    48 Ders - 24/11/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/


/*----------------------------------------------------------------------------------------------------------------------
    Olasılığın en yaygın tanımı "göreli sıklık (relative frequencey)" tanımıdır. Olasılık bu tanıma göre bir limit durumudur. Örneğin bir paranın atılmasında 
    yazı gelme olsalığının 0.5 olması demek atım artırıldığında sayıda tekrarlanırsa yazı gelme sayısının atım sayısına oranının 0.5'e yakınsaması demektir. 
    Buna istatistikte "büyük sayılar yasası (law of large numbers)" da denilmektedir. Aşağıda yazı tura atma işleminde deneme sayısının artırılmasıyla oranın gitgide 
    0.5'e yakınmaması gösterilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define TRYNUM		100000000

int main(void)
{
    unsigned long long head, tail;
    int val;
    double head_ratio, tail_ratio;

    srand(time(NULL));

    head = tail = 0;
    for (unsigned long long i = 0; i < TRYNUM; ++i) {
        val = rand() % 2;
        if (val == 0)			/* Head */
            ++head;
        else
            ++tail;				/* tail */
    }

    head_ratio = (double)head / TRYNUM;
    tail_ratio = (double)tail / TRYNUM;

    printf("head: %.8f, tail: %.8f\n", head_ratio, tail_ratio);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Pek çok programlama dilinin standart kütüphanesinde 0 ile 1 arasında rasgele gerçek sayı veren fonksiyonlar bulunmaktadır. Ancak C'de böyle bir fonksiyon 
    yoktur. C'de bunu sağlamanın en prik yolu aşağıdaki gibidir:

    result = (double)rand() / RAND_MAX;

    Diğer kütüphanelerde de aslında işlemler buradaki yöntemle yapılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
    double result;

    srand(time(NULL));

    for (int i = 0; i < 10; ++i) {
        result = (double)rand() / RAND_MAX;
        printf("%f\n", result);
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Şimdi de rastgele sayı üreterek PI sayısını bulan ilginç bir örnek yapalım. Birim çemberin dörtte birini düşünelim. Bu dörtte birlik bölgedeki kare
    nin alanı 1'dir. Bu dörtte birlik daire diliminin alanı ise pi / 4'tür. Biz 0 ile 1 arasında N tane rastgele nokta üretirsek bu nokta karenin içerisinde olacaktır. 
    Ancak çemberin içerisinde olmayabilir. KArenin içerisinde olanların çemberin içerisinde olanlara oranı bunların alanlarının oranı kadar olmalıdır. Bu duurmda
    N toplam elde edilen nokta sayısını, k ise bunoktaların dörtte birlik çember içerisinde kalanlarının sayısını belirtiyor olsun. Bu durumda

    1 / (pi /4) = N / k olmalıdır. İçler dışlar çarpımıyla pi'yi çekersek şu eşitliği elde ederiz:

    pi = 4 * k / n

    Aşağıda örnek verilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <math.h>
#include <time.h>

#define N		10000000

int main(void)
{
    double x, y;
    unsigned long long k;
    double pi;

    srand(time(NULL));

    k = 0;
    for (unsigned long long i = 0; i < N; ++i) {
        x = (double)rand() / RAND_MAX;
        y = (double)rand() / RAND_MAX;
        if (sqrt(pow(x, 2) + pow(y, 2)) < 1)
            ++k;
    }

    pi = 4. * k / N;
    printf("%f\n", pi);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında rassal sayı üreticilerinin kaliteleri diye bir kavram vardır. Ritchie Kernigan kitabında belirtildiği gibi üretilen rassal sayılar çabuk üretilirler 
    ancak düşük kaliteye sahiplerdir. Kalite döngüye girme olasılığı ve yansızlık ile ölçülmektedir. Eğer tohum değer güncellenirken eski tohum değerlerden biri elde edilirse
    rassal sayı üreticisi döngüye girer. İşte bu döngüye girme ne kadar geç olursa kalite o kadar yükselmektedir. Kaliteli rassal sayı üeticileri rassal sayıları daha 
    uzun zamanda ürettiği için programlama dillerinin kütüphanelerinde genellikle kullanılmazlar. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Eskiden beri kullanılan klasik siyak ekrana "console" ekranı denilmektedir. Console ekranı text bir ekrandır Text ekran demek yalnızca karakter basılan 
    ancak pixel basılamayan ekran demektir. Yani text ekranın en küçük birimi bir karakterdir. Halbuki grafik ekranın en küçük birimi bir pixel'dir. 
    Console ekranında imleçtan bağımsız herhangi bir yere bir karakter basmak için hazır bir standart C fonksiyonu yoktur. Benzer biçimde console 
    ekranında renkli yazı yazmak için de standart C fonksiyonları yoktur. Bu tür işlemler iki biçimde yapılabilmektedir:

    1) Terminal aygıt sürücüleri genellikle ANSI escape komutlarını desteklemektedir. Dolayısıyla ekrana sanki yazı yazılıyormuş gibi bazı escape 
    karakterleri gönderilirse bu işlemler yapılabilmektedir. 

    2) Bu işlemler için üçüncü parti kütüphaneler bulunmaktadır. Örneğin Windows sistemlerinde "Console API'leri" denilen özel fonksiyonlar bu işlemleri 
    yapmaktadır. Benzer biçimde UNIX/Linux ve macOS sistemlerinde bu tür işlemler "curses" gibi kütüphanelerle daha kolay yapılabilmektedir. 

    Aşağıdaki örnekte Windows'un console API'leri kullanılarak writec, hide_cursor ve get_console_size fonksiyonları yazılmıştır. Sonra da write fonksiyonu kullanılarak 
    bir '*' hareket ettirilmiştir. Buradaki _getch fonksiyonu yuşa basar basmaz alan Microsoft derleyicilerinde bulunan ek bir fonksiyondur. Aslında 
    bu fonksiyon da Console API'leri kullanılarak yazılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <conio.h>

void get_console_size(int *height, int *width)
{
    CONSOLE_SCREEN_BUFFER_INFO csbi;

    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
    *width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
    *height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
}

void hide_cursor(void)
{
    CONSOLE_CURSOR_INFO cinfo;

    cinfo.dwSize = 100;
    cinfo.bVisible = FALSE;

    SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cinfo);
}

void writec(int row, int col, char ch)
{
    COORD coord = {col, row};
    DWORD dw;

    WriteConsoleOutputCharacterA(GetStdHandle(STD_OUTPUT_HANDLE), &ch, 1, coord, &dw);
}

int main(void)
{
    int rowsize, colsize;
    int row, col;
    int ch;

    get_console_size(&rowsize, &colsize);
    hide_cursor();

    row = 10, col = 10;

    for (;;) {
        
        writec(row, col, '*');

        ch = _getch();
        
        writec(row, col, ' ');

        switch (ch) {
            case 'w':
                if (row == 0)
                    row = rowsize - 1;
                else
                    --row;
                break;
            case 's':
                if (col == colsize - 1)
                    col= 0;
                else
                    ++col;
                break;
            case 'z':
                if (row == rowsize - 1)
                    row = 0;
                else
                    ++row;
                break;
            case 'a':
                if (col == 0)
                    col = colsize - 1;
                else
                    --col;
                break;
        }
    }
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Hem bir tuşa basılana kadar programımın beklememesi hem de tuş okuması nasıl yapabiliriz? Bu işlemi yapabilen standart C fonksiyonları yoktur. 
    Microsoft'un Windows C kütüphanesinde _kbhit isimli bir fonksiyon bulunmaktadır. Bu fonksiyonun benzeri Curses kütüphanesinde de vardır. 
    Bu fonksiyon o anda klavyede tuşa basılı olup olmadığı bilgisini verir. O halde tuşa basılmışsa tuşu okursak istediğimizi yapabiliriz. Örneğin:

    if (_kbhit()) {
        ch = _getch();
        ...
    }

    Aşağıdaki örnekte bir '*' belli yönde hareket etmekte 'w', 's', 'z', 'a' tuşları ile onn yönü değiştirilmektedir. Kod içerisinde Sleep isimli bir 
    Windows API fonksiyonu daha kullanımıştır. Bu fonksiyon programın akışını parametresiyle belirtilen milisaniye kadar bekletmektedir. Bu fonksiyonun UNIX/Linux ve
    macOS sistemlerinde de benzerleri vardır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <conio.h>

#define UP			0
#define RIGHT		1
#define DOWN		2
#define LEFT		3

void get_console_size(int *height, int *width)
{
    CONSOLE_SCREEN_BUFFER_INFO csbi;

    GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
    *width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
    *height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
}

void hide_cursor(void)
{
    CONSOLE_CURSOR_INFO cinfo;

    cinfo.dwSize = 100;
    cinfo.bVisible = FALSE;

    SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cinfo);
}

void writec(int row, int col, char ch)
{
    COORD coord = {col, row};
    DWORD dw;

    WriteConsoleOutputCharacterA(GetStdHandle(STD_OUTPUT_HANDLE), &ch, 1, coord, &dw);
}

int main(void)
{
    int rowsize, colsize;
    int row, col;
    int direction;
    int ch;

    get_console_size(&rowsize, &colsize);
    hide_cursor();

    row = 10, col = 10;
    direction = RIGHT;

    for (;;) {
        writec(row, col, '*');

        Sleep(100);


        if (_kbhit()) {
            ch = _getch();
            switch (ch) {
                case 'w':
                    direction = UP;
                    break;
                case 's':
                    direction = RIGHT;
                    break;
                case 'z':
                    direction = DOWN;
                    break;
                case 'a':
                    direction = LEFT;
                    break;
                case 'p':
                      _getch();
                    break;
                case 'q':
                    goto EXIT;
            }
        }

        writec(row, col, ' ');

        switch (direction) {
            case UP:
                if (row == 0)
                    row = rowsize - 1;
                else
                    --row;
                break;
            case RIGHT:
                if (col == colsize - 1)
                    col = 0;
                else
                    ++col;
                break;
            case DOWN:
                if (row == rowsize - 1)
                    row = 0;
                else
                    ++row;
                break;
            case LEFT:
                if (col == 0)
                    col = colsize - 1;
                else
                    --col;
                break;
        }
    }
EXIT:
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                        49. Ders - 29/11/2022 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Normal olarak bir program akış main fonksiyonunu bitirdiğinde ya da main fonksiyonunda return deyimini gördüğünde sonlanmaktadır. Ancak bir program 
    her hangi bir fonksiyonun içerisinde de istenilen noktada sonlandırılabilmektedir. Bunun exit isimli standar C fonksiyonu kullanılmaktadır. exit 
    fonksiyonunun prototipi <stdlib.h> dosyası içerisindedir ve aşağıdaki gibidir:

    void exit(int status);

    Programın akışı exit fonksiynunu gördüğünde proses sonlandırılmaktadır. 

    exit fonksiyonu parametre olarak "exit kod (exit code)" denilen int türden bir değer alır. İşletim sistemlerinde sonlanan programlar (çalışmakta olan programlara "proses"
    de denilmektedir) işletim sistemine sonlanma bilgşisi olarak "exit kod (exit code)" denilen bir bilgi de gönderirler. İşletim sistemi bu exit kodla ilgili bir şey yapmaz. 
    Dolayısıyla işletim sistemi için bu exit kodun kaç olduğunun bir önemi yoktur. Ancak işletim sistemi bu exit kodu bir proses (özellikle üst proses) 
    talep ederse ona verebilmektedir. Böylece bir programın başka bir programı çalıştırdığı durumlarda çalıştırılan program başarısızlık durumunda belli exit kodlarla
    sonlandırılırsa onu çalıştıran programlar o programın başarı ya da başarısızlığını exit koduna bakarak anlayabilmektedir. Pekiyi biz programımızı sonlandırırken 
    exit kod olarak exit fonksiyonunun içerisine gangi değeri yazmalıyız? İşte genellikle ve geleneksel olarak C'de başarılı sonlanmalar için 0 değeri, başarısız 
    sonlanmalar için sıfır dışı değerler tercih edilmektedir. Okunabilirliği artırmak için <stdlib.h> dosyası içerisinde 
    aşağıdaki iki sembolik sabit bildirilmiştir:

    #define EXIT_SUCCESS			0
    #define EXIT_FAILURE			1

    Standartlarda EXIT_SUCCESS ve EXIT_FAILURE değerlerinin 0 ve 1 olduğu belirtilmemiştir. Bu durum bu sembolik sabitlerin değerlerinin derleyicileri yazanların
    isteğine bağlı olarak değişebileceği anlamına gelmektedir. Ancak hemen her zaman derleyiciler bu sembolik sabitleri 0 ve 1 olarak define etmektedir. main fonksiyonunda
    return işlemi uygulanmazsa sanki 0 değeriyle geri dönülmüş gibi işlem uygulandığını anımsayınız. 

    Pekiyi programımızı neden erken sonlandırmak isteyebiliriz? Bunun iki nedeni olabilir: Birincisi program zaten hedeflediği şeyi yapmış durumdadır. Dolayısıyla
    programcı o noktada artık mutlı bir biçimde exit(EXIT_SUCCESS) çağrısıyla programını sonlandırabilir. Programın exit fonksiyonuyla sonlandırılmasının ikinci 
    nedeni de bazı başarısızlıklar olabilmektedir. Örneğin program bir dosyayı açamamış olabilir. Bu durumda programa devam edilmesinin bir anlamı kalmaya bilir. 
    Böylesi bir durumda programcı exit(EXIT_FAILURE) çağrısı ile programını sonlandırabilir. exit fonksiyonuna negatif bir değer de argüman olarak geçilebilir. 
    Ancak işletim sistemlerinin çoğu negatif exit kodlarını kabul etmemektedir. Yani işletim sistemlerinin çoğu exit kodu olarak verilen değeri işaretsiz tamsayı 
    türlerine dönüştürmektedir.

    Programın başarısz bir biçimde sonlandırılması sırasında programcının exit fonksiyonunu çağırmadan önce oluşan problemli durum hakkında ekrana bir yazı bastırması
    iyi bir tekniktir. Böylece kullanıcı başarısızlığın endenini anlayabilir ve onu düzeltme yoluna gidebilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

void bar(void)
{
    printf("bar begins...\n");

    exit(EXIT_SUCCESS);

    printf("bar ends...\n");
}

void foo(void)
{
    printf("foo begins...\n");
    bar();
    printf("foo ends...\n");

}

int main(void)
{
    printf("main begins...\n");

    foo();
   
    printf("main ends...\n");


    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    main fonksiyonu bittiğinde program bitiyordu. Peki bu durumda programın exit kodu ne olacaktır? İşte bir C programı eğer exit fonksiyonuyla sonlandırılmamışsa 
    programın main fonksiyonunu bitirmesiyle sonlandırılır. main fonksiyonun geri dönüş değeri (yani main fonksiyonunda return ettiğimiz değer) bu durumda
    programın exit kodu olacaktır. C standartları main fonksiyonu bittiğinde onun geri dönüş değeri ile exit fonksiyonun derleyici tarafından çağrılmasını gerektiğini 
    belirtmektedir. Başka bir deyişle standartlara göre main fonksiyonun çağrılması exit(main()) biçiminde olmaktadır. Ayrıca C standaratları main fonksiyonun aözgü olarak 
    main fonksiyonunda hiç return kullanılmazsa sanki main fonksiyonunun sonuna return 0 yazılmış gibi bir etki oluşacağını da belirtmektedir. 
    Yani biz main fonksiyonunda hiç return deyimini kullanmasak bile sanki 0 ile return etmişiz gibi bir durum oluşmaktadır. Tabii bu durum main fonksiyonuna 
    özgüdür. Başka bir fonksiyonda açıkça return kullanmazsak geri dönüş değeri olarak çöp değer elde edilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    UNIX/Linux ve macOS sistemlerinde komut satırında son çalıştırılmış olan programın exit kodunu aşağıdaki komtla görüntüleyebiliriz:

    echo $?

    Aynı işlem Windows'un komut satırında şu komutla yapılmaktadır:

    echo %errorlevel%
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Program hatalı bir biçimde exit ile sonlandırılmadan önce hata mesajının stdout dosyasına değil stderr dosyasına yazdırılması iyi bir tekniktir. 
    Biz henüz stderr dosyasının ne anlama gedliğini görmedik. Ancak stderr dosyasına yazdırılan mesajlar default durumda yine ekrana basılmaktadır. 
    stderr dosyasına yazdırma yapmak için printf yerine fprintf fonksiyonu kullanılmaktadır. fprintf fonksiyonunun birinci parametresine stderr 
    yazmalısınız. fprintf fonksiyonun diğer parametreleri tamamen printf fonksiyonu ile aynıdır. Örneğin:

    fprintf(stderr, "cannot allocate memory!..\n");
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi C90'a kadar C'de dizilerin uzunlukları tanomlama sırasında sabit ifadeleriyle belirtilmek zorundaydı. C99 ile birlikte yerel 
    dizilerin uzunluklarınıjn sabit ifdeleri ile belirtilme zorunluluğu ortadan kaldırılmıştı. Ancak bu değişekn uzunluktaki diziler (variable length array) konusu
    C++'a dahil edilmemişti. Ayrıca bir diziyi tanımladıktan sonra onun boyutlarını değiştiremiyorduk. 

    Ancak bazen bir dizinin eleman uzunluğu programın çalışma zamanı sırasında ve anacak birtakım olaylar sonucunda belirlenebilmektedir. Bazen de dizilerin 
    büyütülüp küçültülmesi gerekebilmektedir. İşte bunların sağlanması için programın çalışma zamanı sırasında güvenli bir biçimde ardışıl byte'ların tahsis edilmesi gerekmektedir.
    C'de programın çalışma zamanı sırasın ardışıl byte tansis edebilmeye yönelik mekanizmalara "dinamik bellek yöetimi (dynamic memory management)" 
    denilmektedir. C'de dinamik bellek yönetimi ismine "dinamik bellek fonksiyonları" denilen standart C fonksiyonlarıyla yapılmaktadır. 

    Dinamik bellek fonksiyonlarının rototipleri <stdlib.h> içerisindedir. C'de 4 dinamik bellek fonksiyonu vardır: malloc, calloc, realloc ve free.

    Dinamik bellek fonksiyonlarıyla tahsis edime potansiyelinde olan alanlara "heap" denilmektedir. Heap alanının ne büyüklükte olduğu ve prosesin bellek alanında
    nerede oluşturulduğu sistem sisteme değişebilmektedir. Windows, UNIX/Linux ve macOS işletim sistemlerinde heap alanı prosese özgüdür. 
    Yani bu sistemlerde her prosesin heap alanı diğerlerinden farklıdır ve o proses için oluşturulmuştur. Bu sistemlerde bir program bellek yüklendiğinde
    onun için bir heap alanı oluşturulur. Program bittiğinde programla birlikte onun heap alanı da sisteme iade edilir. Böylece bu sistemlerde bir 
    program içerisinde yapılan dinamik tahsisatların başka programların heap alanlarını daraltıcı bir etkisi olmaz. Ancak C standartlarında heap alanının
    proses özgü olup olmadığı konusunda bir şey söylenmemiştir. Bazı sistemlerde heap alanı tüm prosesler için ortak bir alan biçiminde oluşturulabilmektedir.

    Genel olarak çalışmakta olan bir program için en büyük alan data/bss ve heap alanlarıdır. En küçük alan yerel değişkenlerin ve parametre değişkenlerinin 
    yaratıldığı stack alanıdır. Heap alanı genel olarak büyük bir alan olma eğilimindedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    en sık kullanılan dinamik bellek fonksiyonu malloc fonksiyonudur. Fonksiyonun prototipi şöyledir:

    void *malloc(size_t size);

    Fonksiyn parametresiyle belirtilen miktarda ardışıl alanı heap'te tahsis eder. Tahsis ettiği alanın başlangıç daresiyle geri döner. Artık o alan 
    bizim kullanımımız için güvenli bir biçimde tahsis edilmiş olur. malloc fonksiyonu başarısızlık durumunda NULL adresle geri dönmektedir. eğer heap alanı doluysa
    ve istenilen miktarda ardışıl alan bulunamadıysa malloc başarısız olabilmektedir. Bugün kullanıdğımız 64 bit Windows, UNIX/Linux ve macOS sistemlerinde 
    proseslerin heap alanları oludkç geniştir. Ancak ne olursa olsun programcı malloc ile yaptığı tahsisatın başarısını kontrol etmelidir. Genellikle böylesi 
    bir başarısızlıkta programcılar programlarını exit fonksiyonuyla sonlandırırlar. malloc fonksiyonu ile tahsis edilen alanda çöp değerler vardır. 

    Programcı tipik olarak malloc ile tahsis edilen alanın adresiniş bir göstericiye atar ve o alanı artık bir dizi gibi kullanır. Zaten diziyi dizi yapan
    unsur elemanların aynı türden olması ve bellekte ardışıl bir biçimde bulunmasıdır. 

    malloc fonksiyuonun void bir adresle geri dönmesi oldukça anlamlıdır. Çünkü malloc fonksiyonu tahsis etmiş olduğu alanın programcı tarafından 
    ne niyetle (örneğin hangü türden dizi olarak) kullanılacğını bilmez. void adresler genel adres olarak da kullanılmaktadır.

    Aşağıdaki örnekte malloc fonksiyonu ile klavyeden girilen miktar kadar int bir dizi tahsis edilmiştir. Sonra o diziye scanf fonksiyonu ile 
    değerler okunmuş ve okunan değerler yazdırılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *pi;
    int size;

    printf("Dizi ununlugu:");
    scanf("%d", &size);

    pi = (int *)malloc(size * sizeof(int));
    if (pi == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }
    for (int i = 0; i < size; ++i) {
        printf("%d. elemani giriniz:", i + 1);
        scanf("%d", &pi[i]);
    }

    for (int i = 0; i < size; ++i)
        printf("%d ", pi[i]);
    printf("\n");

    return 0;
}
                                                      
/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte malloc fonksiyonu ile heap'te 100 byte tahsis edilmiştir. Tahsis edilen alanın adresi char türden bir göstericiye atanmıştır ve o alan
    char türden bir dizi gibi kullanılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *str;

    if ((str = (char *)malloc(100)) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }
    printf("Bir yazi giriniz:");
    gets(str);

    puts(str);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte 5 elemanlı char türden bir gösterici dizisinin her elemanı için malloc ile 64 byte alan tahsis edilmiş ve o alana gets 
    ile okuma yapılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *names[5];

    for (int i = 0; i < 5; ++i) {
        if ((names[i] = (char *)malloc(64)) == NULL) {
            fprintf(stderr, "cannot allocate memory!..\n");
            exit(EXIT_FAILURE);
        }
        printf("Bir isim giriniz:");
        gets(names[i]);
    }

    for (int i = 0; i < 5; ++i)
        puts(names[i]);

    return 0;
}
                                                      
/*----------------------------------------------------------------------------------------------------------------------
    Dinamik bellek fonksiyonlarıyla tahsis ettiğimiz alanlar kullanıldıktan sonra ne olacaktır? Yukarıda a belirtitğimiz gibi Windows, UNIX/Linux ve macOS 
    sistemlerinde heap alanı prosese özgüdür. Yani bu sistemlerde program bitince zaten programınb tüm bellek alanı heap alanı da dahil olmak üzere sisteme 
    iade edilmektedir. O halde bu sistemlerde en kötü olasılıkla program sonlandığında tahsis edilmiş olan dinamik alanlar free hale getirilecektir. 
    Ancak C standartlarında heap alanın prosese özgü olup olmadığı konusunda bir belirlemede bulunulmamıştır. Gerçekten de bazı nadir sistemlerde proses bittiği 
    halde prosesin yaptığı tahsisatlar otomatik boşaltılmayabilmektedir. Ayrıca programcının tahsis ettiği dinamik alanı kullandıktan sonra artık 
    o alanla bir işlemi kalmamışsa o alanı serbest bırakması iyi bir tekniktir. İşte daha önce tahsis edilmiş olan dinamik alanı serbest bırakmak için
    frtee isimli standart C fonksiyonu kullanılmaktadır. free fonksiyonun prototipi şöyledir:

    void free(void *ptr);

    free fonksiyonu parametre olarak daha önce dinamik bellek fonksiyonlarıyla tahsis edilmiş olan bloğun başlangıç adresini alır. Tüm bloğu serbest bırakır. 
    Bloğun bir kısmının serbest bırakılması biçiminde bir olanak yoktur. Dinamik bellek fonksiyonları heap alanının tahsisatı için ortak bir tahsisat tablosu 
    oluşturmaktadır. Bu tahsşisat tablosunu tapu kadastro dairesine benzetebiliriz. Bu tahsisat tablosunda tahsis edilmiş olan alanlar bşalngıç adresleri ve 
    uzunluklarıyla kaydedilmektedir. Dolayısıyla bizim free fonksiyonuna daha önce tahsis edilmiş olan bloğun başlangıç adresini vermemiz gerekir. 
    Aksi takdirde fonksiyon tanımsız davranışa yol açmaktadır ve bu hatalı kullanımn programın çökmesine neden olabilmektedir. 

    free fonksiyonun geri dönüş değeri yoktur. Yani biz bu fonksiyonun başarılı olup olmadığını anlayamayız. Bize düşen görev fonksiyona daha önce tahsis edilmiş 
    olan bloğun başlangıç adresinidüzgün bir biçimde geçirmektedir. VBu durumda fonksiyon başarısz olmayacaktır.

    Aşağıdaki örnekte önce malloc fonksiyonu ile dinamik bir alan tahsis edilmiş bu alan kullanıldıktan sonra da free fonksiyonu ile serbest bırakılmıştır.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *str;

    if ((str = (char *)malloc(100)) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }
    printf("Bir yazi giriniz:");
    gets(str);

    puts(str);

    free(str);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte 5 elemanlı char türden göstericisinin her elemanı için 64 byte'lık alanlar dinamik olarak tehsis edilmiştir. Kullanım bittikten sonra 
    bu beş ayrı alan tek tek free hale getirilmiştir. Yukarıda da belirttiğimiz gibi aslında Windows, Linux ve macOS gibi 
    sistemlerde her prosesin ayrı heap alanı vardır. Bir proses sonlandığında zaten onun heap alanı da sisteme iade edilmektedir. 
    Yani aslında bu sistemlerde program sonlanırken daha önce tahsis etmiş olduğumuz alanların free hale getirilmesi gerekmez. 
    Ancak C genelinde böyle bir zorunluluğun olmadığını tekrar belirtmek istiyoruz. Bu nedenle aşağıdaki örnekte bir tahsisat 
    başarısızsa daha önce yapılan tahsisatlar da free hale getirilerek programdan çıkılmıştır:

    if ((names[i] = (char *)malloc(64)) == NULL) {
        for (int k = 0; k < i; ++k)
            free(names[k]);
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    char *names[5];

    for (int i = 0; i < 5; ++i) {
        if ((names[i] = (char *)malloc(64)) == NULL) {
            for (int k = 0; k < i; ++k)
                free(names[k]);
            fprintf(stderr, "cannot allocate memory!..\n");
            exit(EXIT_FAILURE);
        }
        printf("Bir isim giriniz:");
        gets(names[i]);
    }

    for (int i = 0; i < 5; ++i)
        puts(names[i]);

    for (int i = 0; i < 5; ++i)
        free(names[i]);

    return 0;
}
      
/*----------------------------------------------------------------------------------------------------------------------
                                                
/*----------------------------------------------------------------------------------------------------------------------
                                                        50. Ders - 01/12/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Dinamik bellek fonksiyonlarıyla tahsis edilen alanlar free hale getirilene kadar yaşamaya devam eder. Tabii yukarıda 
    da belirttiğimiz gibi Windows, UNIX/Linux ve macOS gibi işletim sistemlerinde proses bittiğinde prosesin heap alanı 
    da boşaltılacağı için dinamik alanlar en kötü olasılıkla program bittiğinde serbest bırakılırlar. Bir fonksiyon içerisinde 
    dinamik tahsisat yaptığımızda fonksiyon sonlansa bile tahsis edilen tahsis alan edilmiş olarak kalmaktadır. Örneğin:

    char *foo()
    {
        char *str;

        if ((str = (char *)malloc(64)) == NULL) {
            fprintf(stderr, "cannot allocate memory!..\n");
            exit(EXIT_FAILURE);
        }

        return str;
    }

    Burada str göstericisi malloc ile tahsis edilmiş bloğu göstermektedir. Fonksiyon da str içerisindeki adresle geri dönmüştür. Fonksiyon bittiğinde 
    str yerel değişkeni yok edilecektir. Ancak onun içerisindeki adreste bulunan alan tahsis edilmiş olarak kalacaktır. 
    Tabii durada bu alanın free hale geitirilmesi artık fonksiyonu çağıranın sorumluluğundadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi hiçbir fonksiyon yerel değişkenin ya da dizinin adresiyle geri dönmemeliydi. Örneğin:

    char *getname(void)
    {
        char name[1024];

        printf("Adi soyadi:");
        gets(name);

        return name;
    }

    Burada getname fonksiyonu yerel name dizisinin adresiyle geri dönmüştür. Ancak fonksiyon bittiğinde fonksiyonun yerel değişkenleri yok edileceğinden
    namme dizisi de yok edilecektir. O halde geri döndürülen adres aslında artık tahsis edilmiş olan bir adres olmayacaktır. 

    Ancak bir fonksiyon dinamik bir biçimde tahsis ettiği bir alanın adresiyle geri dönebilir. Örneğin:

    char *getname(void)
    {
        char name[1024];
        char *str;

        printf("Adi soyadi:");
        gets(name);

        if ((str = (char *)malloc(strlen(name) + 1)) == NULL)
            return NULL;

        strcpy(str, name);

        return str;   
    }

    Burada bir sorun yoktur. Çünkü getname fonksiyonu dinamik tahsis edilmiş alanın başlanıç adresiyle geri dönmektedir. Fonksiyon bittiğinde
    bu dinamik alan yaşamaya devam edecektir. Burada yerel dizi kullanılmasının nedeni malloc fonksiyonuyla tam gereken miktarda byte tahsis etmek içindir. 
    name dizisi nasıl olsa fonksiyon bittiğinde yok edilecektir. Ancak dinamik alan tahsis edilmiş olarak kalacaktır. Tabii burada getname fonksiyonun geri döndürdüğü
    dinamik alanın free hale getirilmesi getname fonksiyonunu çağıranın sorumluluğundadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *getname(void)
{
    char name[1024];
    char *str;

    printf("Adi soyadi:");
    gets(name);

    if ((str = (char *)malloc(strlen(name) + 1)) == NULL)
        return NULL;

    strcpy(str, name);

    return str;   
}

int main(void)
{
    char *name;

    if ((name = getname()) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        eit(EXIT_FAILURE);
    }
    puts(name);

    free(name);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki örnekte olduğu gibi char türden bir yerel dizinin içeriğini dinamik olarak tahsis edilmiş bir alana kopyalamak
    için strdup isimli standart bir C fonksiyonu da bulundurulmuştur. Fonksiyonun prototipi şöyledir:

     #include <string.h>

     char *strdup(const char *s);

     Fonksiyon parametresiyle belirtilen adresteki yazı kadar (null karakter de dahil olmak üzere) alanı malloc ile tahsis 
     eder. Bu yazıyı tahsis ettiği alana kopyalar ve tahsis edilen alanın başlangıç adresine geri döner. Fonksiyon 
     başarısızlık durumunda NULL adrese geri dönmektedir. Örneğin:

     char *getname(void)
     {
        char name[1024];
        
        printf("Adi soyadi:");
        gets(name);

        return strdup(name);
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *getname(void)
{
    char name[1024];
        
    printf("Adi soyadi:");
    gets(name);

    return strdup(name);
}

int main(void)
{
    char *name;

    if ((name = getname()) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        eit(EXIT_FAILURE);
    }
    puts(name);

    free(name);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Dinamik olarak tahsis edilmiş bir alanın free fonksiyonuyla serbest bırakılması unutulursa ne olur? Böylesi durumlara programlamada "bellek sızıntısı (memory leak)"
    denilmektedir. Bellek sızıntısı eğer önemli bir boyutta değilse bir sorun olarak kendini göstermeyebilir. Nasıl olsa Windows, UNIX/Linux ve macOS sistemlerinde
    en kötü olasılıkla program bittiğinde bu alanlar serbest bırakılacaktır. Ancak C standartlarında daha önceden de belirtildiği gibi program bittiğinde 
    dinamik alanların serbest bırakılması garanti edilmemiştir. Bunun yanı sıra Windows, UNIX/Linux ve macOS sistemlerinde program bittiğinde tüm dinamik alanlar
    sisteme iade ediliyor olsa bile bellek sızıntısı önemli bir sorun haline gelebilmektedir. Çok uzun süre (yıllarca) çalışan programlar vardır. Bu programlarda 
    küçük sızıntılar bile zamanla çok büyük miktarda alanın tahsis edilmiş olmasına yol açabilmektedir. Bu tür sızıntılarda heap alanı dolabilmekte ya da 
    sistemin sanal bellek alanı yetersiz kalabilmektedir. Bunlardan biri olmasa bile kronik bir sızıntı sistem performansını olumsuz etkileyebilir. 
    Bu nedenle C'de bellek sızıntısı önemli bir problem olarak değerlendirilmektedir. Programcının yaptığı dinamik tahsisatları işi bittiğinde serbest bırakması gerekir. 

    Aşağıdaki örnekte getname fonksiyonu her çağrıldığında dinamik alanın adresiyle geri dönmektedir. Ancakl programcı bu alanları free hale getirmemiştir. 
    Bu biçimdeki bellek sızıntılarına dikkat ediniz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

char *getname(void)
{
    char name[1024];
    char *str;

    printf("Adi soyadi:");
    gets(name);

    if ((str = (char *)malloc(strlen(name) + 1)) == NULL)
        return NULL;

    strcpy(str, name);

    return str;   
}

int main(void)
{
    char *name;

    for (;;) {
        if ((name = getname()) == NULL) {           /* dikkat! bellek sızıntısı (memory leak */
            fprintf(stderr, "cannot allocate memory!..\n");
            exit(EXIT_FAILURE);
        }
        if (!strcmp(name, "quit"))
            break;
        puts(name);
    }

    return 0;
}
                                                      
/*----------------------------------------------------------------------------------------------------------------------
    Java, C# gibi dillerde tahsis edilen dinamik alanlar o dillerin framework'leri tarafından otomatik olarak yok edilmektedir. Bu mekanizmaya 
    "çöp toplayıcı (garbage collector)" denilmektedir. C'de böyle bir çöp toplayıcı mekanizma yoktur. C++'ta da yoktur. Dolayısıyla C'de kullanılmayan
    dinamik alanların serbest bırakılması programcının sorumluluğundadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    calloc (count of allocation) fonksiyonu aslında taban (base) bir fonksiyon değildir. Bir sarma (wrapper) fonksiyon biçimindedir. calloc fonksiyonunun prototipi 
    şöyledir:

    void *calloc(size_t nelem, size_t size);

    calloc fonksiyonu iki parametresini çarpımı kadar ardışıl byte tahsis etmektedir. Geleneksel olarak ilk parametre tahsis edilecek dizinin eleman sayısını,
    ikinci parametre dizinin bir elemanının byte uzunluğunu belirtir. Örneğin 10 elemanlı int bir diziyi dinamik olarak tahsis etmek için calloc 
    fonksiyonu şöyle kullanılır:

    pi = (int *)calloc(10, sizeof(int));

    calloc fonksiyonu yine başarı durumunda tahsis edilen alanıon başlangıç adresiyle, başarısızlık durumunda NULL adresle geri dönmektedir. Ancak calloc fonksiyonun malloc 
    fonksiyonundan asıl farkı calloc fonksiyonun tahsis ettiği alanı sıfırlamasıdır. Halbuki malloc fonksiyonu ile tahsis edilen alan içerisinde çöp değerler bulunmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int *pi;

    if ((pi = (int *)calloc(10, sizeof(int))) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < 10; ++i)
        printf("%d ", pi[i]);
    printf("\n");

    free(pi);

    return 0;
}
                                                      
/*--------------------------------------------------------------------------------------------------------------------------------------------------
    calloc fonksiyonu malloc fonksiyonunu kullanarak aşağıdaki gibi yazılabilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

void *mycalloc(size_t nelems, size_t size)
{
    void *ptr;

    if ((ptr = malloc(nelems * size)) == NULL)
        return NULL;

    return memset(ptr, 0, nelems * size);
}

int main(void)
{
    int *pi;

    if ((pi = (int *)mycalloc(10, sizeof(int))) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < 10; ++i)
        printf("%d ", pi[i]);
    printf("\n");

    free(pi);

    return 0;
}
                                                      
/*----------------------------------------------------------------------------------------------------------------------
    realloc fonksiyonu daha önce tahsis edilmiş olan dinamik alanı büyütmek ya da küçültmek çin kullanılmaktadır. Prototipi şöyledir:

    void *realloc(void *ptr, size_t newsize);

    Fonksiyonun birinci parametresi daha önce tahsis edilmiş olan dinamik alanın başlangıç adresini, ikinci parametresi ise bloğun arzu edilen 
    toplam yeni uzunluğunu belirtmektedir. ralloc fonksiyonu tipik olarak şöyle çalışmaktadır (ancak fonksiyonun bu biçimde çalışması standartda garanti 
    edilmemiştir): Eğer blok büyütülmek istenmişse fonksiyon daha önce tahasi edilmiş olan bloğun hemen altında toplam yeni uzunluk için yeteri kadar boş alanın 
    olup olmadığına bakar. Eğer daha önce tahsis edilmiş olan bloğun hemen altında toplam yeni uzunluk için yeterli alan varsa orayı da tahsis eder. Eğer eski 
    bloğun hemen altında toplam yeni uzunluğu karşılayacak kadar yeterli boş alan yoksa realloc bu sefer heap'in başka bir yerinde toplam yeni 
    uzunluk kadar alan tahsis etmeye çalışır. Eğer böyle bir alanı tahsis edebilirse eski alandaki bilgileri yeni alana kopyalar ve eski alanı free hale getirir. 
    realloc toplam yeni uzunluk kadar olan alanın başlangıç adresiyle geri dönmektedir. Programcı eski bloğun yer değiştirmiş olabileceğini göz önüne 
    almak zorundadır. Bu nedenle programcı her zaman realloc fonksiyonun geri döndürdüğü değeri dikkate almalıdır. Örneğin:

    p = malloc(10)
    //..
    realloc(p, 20);

    Bu kullanım hatalıdır.  Çünkü p göstericisinin gösterdiği yerdeki blok yer değiştirmiş olabilir ve eğer blok yer değiştirdiyse programcı bunu dikkate almalıdır:
    
    p = malloc(10);
    //...
    pnew = realloc(p, 20);

    Eğer realloc fonksiyonu eski bloğun altında toplam yeni uzunluğu karşılayacak kadar yer bulamazsa ve heap'in başka bir yerinde de toplam yeni uzunluk kadar 
    yer bulamazsa başarısız olur ve NULL adresle geri döner. Tabii bu durumda eski blok free hale getirilmemektedir. 

    realloc fonksiyonu ile daha önce tahsis etmiş olduğumuz bloğu küçültmek de isteyebiliriz. Örneğin:

    p = malloc(100);
    //...
    pnew = realloc(p, 50);

    Burada 100 byte'lık blok 50 byte'a küçültülmüştür. realloc fonksiyonu bloğu küçültürken de bloğun yerini yerini değiştirebilmektedir. Dolayısıyla programcının 
    yine realloc fonklsiyonunun geri dönüş değerini dikkate alması gerekir. 

    Biz yukarıda realloc fonksiyonu ile daha önce tahsis edilmiş olan bloğu büyütürken realloc fonksiyonun tipik olarak önce eski bloğun altında 
    toplam yeni uzunluğu karşılayacak kadar yer var mı diye baktığını, eğer yer varsa hemen orayı da tahsis ettiğini söylemiştik. C Standartları
    realloc fonksiyonun önce eski bloğun altına bakacağı yönünde bir ifade kullanmamıştır. Dolayısıyla biz burada tipik gerçekleştirimin bu biçimde 
    olduğunu belirttik. Bir derleyicideki reealloc fonksiyonu eski bloğun aşağısına bakmadan heap'in başka bir yerinde toplam yeni uzunluk kadar 
    yer araştırabilir. 

    realloc fonksiyonun birinci parametresi NULL adres geçilirse realloc tamamen malloc gibi davranmaktadır. Yani realloc(NULL, n) çağrısı ile malloc(n) 
    çağrısı tamamen eşdeğerdir. 

    realloc fonksiyonu ile bloğu büyüttüğümüzde büyütülen kısımda çöp değerler bulunmaktadır. Yani büyütülen kısım sıfırlanmamaktadır.

    realloc ile normal diziler büyütülüp küçültülemezler. Ancak dinamik biçimde tahsis edilmiş (yani malloc ya da calloc ile ya da realloc ile tahsis edilmiş) alanlar 
    büyütülüp küçültülebilirler.

    realloc fonksiyonunu kullanırken fonksiyonun başarısız olduğu durumda eski bloğu kaybetmemek için geri dönüş değerini önce başka bir göstericye atayıp 
    sonra asıl gösteriye atayabilirsiniz. Örneğin:

    for (n = 0;; ++n) {
        //...
        if ((pnew = realloc(pi, (n + 1) * sizeof(int))) == NULL) {
            fprintf(stderr,i "cannot realloc memory!..\n);
            break;
        }
        pi = pnew;
        //...
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    realloc fonksiyonu dibnamik biçimde büyütülen dizilerde sıkça kullanılmaktadır. Dinamik olarak büyütülen dizi demekle gerektiği zaman büyütülen dizi 
    anlaşılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    int val;
    int *pi, *pnew;
    int n;

    pi = NULL;
    for (n = 0;; ++n) {
        printf("Bir deger giriniz:");
        scanf("%d", &val);
        if (val == 0)
            break;
        if ((pnew = (int *)realloc(pi, (n + 1) * sizeof(int))) == NULL) {
            fprintf(stderr, "cannot realloc memory!..\n");
            break;
        }
        pi = pnew;
        pi[n] = val;
    }

    for (int i = 0; i < n; ++i)
        printf("%d ", pi[i]);
    printf("\n");

    free(pi);

    return 0;
}
                                                      
/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki gibi dinamik büyütülen dizilerde aslında büyütme bi,rer birer yapılmamaktadır. Çünkü bir eleman için yeniden realloc fonksiyonun çağrılması
    oldukça maliyetlidir. Şimdi yukarıdaki örneği büyütmeyi birer birer değil CHUNK_SIZE kadar yapacak biçimde değiştirelim.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

#define CHUNK_SIZE      10

int main(void)
{
    int val;
    int *pi, *pnew;
    int n;

    pi = NULL;
    for (n = 0;; ++n) {
        printf("Bir deger giriniz:");
        scanf("%d", &val);
        if (val == 0)
            break;
        if (n % CHUNK_SIZE == 0) {
            if ((pnew = (int *)realloc(pi, (n + CHUNK_SIZE) * sizeof(int))) == NULL) {
                fprintf(stderr, "cannot realloc memory!..\n");
                break;
            }
            pi = pnew;
        }
        pi[n] = val;
    }

    for (int i = 0; i < n; ++i)
        printf("%d ", pi[i]);
    printf("\n");

    free(pi);

    return 0;
}
                                                      
/*----------------------------------------------------------------------------------------------------------------------
    Aslında dinamik büyütülen dizilerde büyütme genellikle eskisinin iki katı olacak biçimde (yani geometrik biçimde) yapılmaktadır. Bunun nedenini 
    burada açıklamayacağız. Ancak eğer dizi 1'den başlatılacaksa tipik olarak 1, 2, 4, 8, 16, 32, 64, 128, 256 biçiminde büyütme uygulanmaktadır. 
    Bu biçimde dinamik büyütme gerçekleştiriminde tipik olarak programcı iki uzunlşuğu tutar: Toplam tahsis edilmiş eleman uzunluğu ve dolu eleman uzunluğu.
    Genellikle toplam tahsis edilmişl eleman uzunluğuna "capacity", dolu eleman uzunlupuna da "size" denilmektedir. İşin başında size = 0 biçimindedir. 
    Her adımda size değeri bir artırılır. size değeri capacity değerine eriştiğinde eskisinin iki katı olacak biçimde yeniden tahsisat yapılır.

    Aşağıdaki örnekte dinamik dizi için işin başında DEF_CAPACITY kadar yer ayrılmıştır. Sonra size değeri capacity değerine eriştiğinde capacity iki 
    kat artırılarak blok eskisinin iki katı büyüklükte olacak biçimde büyütülmüştür.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

#define DEF_CAPACITY        4

int main(void)
{
    int val;
    int *pi, *pnew;
    size_t size, capacity;

    size = 0;
    capacity = DEF_CAPACITY;

    if ((pi = (int *)malloc(DEF_CAPACITY * sizeof(int))) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }
    
    for (;;) {
        printf("Bir deger giriniz:");
        scanf("%d", &val);
        if (val == 0)
            break;
        if (size == capacity) {
            if ((pnew = (int *)realloc(pi, (capacity * 2) * sizeof(int))) == NULL) {
                fprintf(stderr, "cannot reallocate memory!..\n");
                break;
            }
            pi = pnew;
            capacity *= 2;
        }
        pi[size] = val;
        ++size;  
    }

    for (int i = 0; i < size; ++i)
        printf("%d ", pi[i]);
    printf("\n");

    free(pi);

    return 0;
}
                                                      
/*----------------------------------------------------------------------------------------------------------------------
                                                            51. Ders - 08/12/2022 Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'nin dil tarafından desteklenen veri yapılarından biri de yapıladır. Elemanları bellekte ardışıl bir biçimde bulunan fakat farklı türlerden olabilen 
    veri yapılarına "yapı (structure)" denilmektedir. Yapılarla diziler birbirlerine oldukça benzemektedir. Hem dizilerin hem de yapıların elemanları bellekte 
    ardışıl bir biçimde bulunmaktadır. Ancak dizi elemanlarının hepsi aynı türden iken yapı elemanları farklı türlerden olabilmektedir. Derleyiciler
    yapı elemanlarının arasında "padding" denilen kontrolü boşluklar bırakabilmektedir. Bu olguya "hizalama (alignment)" denilmektedir. Hizalama 
    yapı elemanlarının ardışıllığını bozmaz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapıyla çalışmadan önce yapı elemanlarının türleri ve isimleri derleyiciye bildirilir. Buna "yapı bildirimi (structure declaration)" denilmektedir. 
    Yapı bildiriminin genel biçimi şöyledir:

    struct <yapı_ismi> {
        <yapı eleman bildirimleri>
    };

    Örneğin:

    struct SAMPLE {
        int a;
        long b;
        double c;
    };

    Örneğin:

    struct point {
        int x, y;
    };

    struct Person {
        char name[32];
        int no;
    };

    Yapı isimlerinin Kernighan & Ritchie stilinde genellikle tüm karakterleri büyük harflerden oluşturulmaktadır. Ancak bazı programcılar yapı isimleribib
    tüm karakterlerini küçük harflerle oluşturabildiği gibi bazı programcılar da yapı isimlerinin yalnızca ilk karakterlerini (ve sonraki sözcüklerin ilk karakterlerini)
    büyük harflerden diğerlerini küçük harflerden oluşturabilmektedir. Biz kursumuzda daha çok yapı isimlerini büyük harflerle oluşturacağız. Ancak bazı örneklerde
    de diğer harflendirme biçimlerini kullanacağız. 

    C'de yapı bildiriminin içi boş olamaz. En az bir yapı eelemanının bildirilmesi gerekmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapı bildirildiği zaman aynı zamanda bir tür de oluşurulmuş olur. Yapı bildirimi ile oluşturulan türün ismi struct anahtar sçzüğü ile yapı isminden 
    oluşmaktadır. Örneğin:

    struct POINT {
        int x, y;
    };

    Burada oluşturulan bu türün ismi "POINT" değil "struct POINT" biçimindedir. Örmeğin:

    struct date {
        int day;
        int month;
        int year;  
    };
    
    Burada oluşturulmuş olan türün ismi ise "struct date" biçimindedir. 

    Bir yapı bildirildikten sonra oluşturulan tür ismi kullanılarak o yapı türünden nesneler yaratılabilir. Örneğin:

    struct SAMPLE {
        int a;
        long b;
        double c;
    };

    struct SAMPLE s, k;

    Burada s ve k nesneleri struct SAMPLE türündendir. 

    Bir yapı bildirimi tanımlama değildir. Dolayısıyla biz bir yapı bildirdiğimizde yalnızca derleyiciye bilgi vermiş oluruz. Yapı bildirimini gören derleyici
    bellekte bizim için bir yer ayırmaz. Bellekte yer ayırma işlemi o yapı türünden nesler tanımlandığında yapılmaktadır. Örneğin:

    struct DATE {			/* bildirim, bellekte yer ayrılmaz */
        int day, month, year;
    };

    struct DATE d;		/* tanımlama, bellekte yer ayrılır */

    Yapı nesneleri "bileşik (compound)" nesnelerdir. Yani parçalardan oluşmaktadır. Yapı bildiriminde belirtilen elemanlar o yapı türünden nesneler 
    tanımlandığında onların parçalarını belirtmektedir. Yapı bildiriminde ilk yazılan eleman düşük adreste olacak biçimde ardışıl bir yerleşim uygulanmaktadır. 
    (Ancak yukarıda da belirtildiği gibi kontrollü padding denilen boşluklar bırakılabilmektedir.) Örneğin:

    struct SAMPLE {
        int a;
        long b;
        double c;
    };

    struct SAMPLE s;

    Burada s üç parçadan oluşmaktadır. Parçaların isimleri a , b ve c'dir. s'in a parçası en düşük adreste bulunur. Onu b parçası onu da c parçası izler. 
    Bir yapı nesnesi yoluyla yapının belli bir elemanına "nokta operatörü" ile erişilir. Nokta operatörü iki operand'lı araek bir operatördür. Nokta operatörünün 
    sol tarafındaki operand bir yapı türünden nesneyi, sağ tarafındaki operand ise o yapının bir elemanını belirtmek zorundadır. Örneğin s.a ifadesinde 
    s bir yapı nesnesidir, a ise o yapının bir elemanıdır. Nokta opetratörü öncelik tablosunun en üst grubunda bulunmaktadır:

    () [] .							Soldan-Sağa
    + - ++ -- !	& *	sizeof (tür)	Sağdan-Sola
    * / %							Soldan-Sağa
    + -								Soldan-Sağa
    <  > <= >=						Soldan-Sağa
    != ==							Soldan-Sağa
    &&								Soldan-Sağa
    ||								Soldan-Sağa
    ?:								Sağdan-Sola
    =, +=, /=, *=,...				Sağdan-Sola
    ,								Soldan-Sağa	 

    Örneğin:

    struct SAMPLE {
        int a;
        long b;
        double c;
    };

    struct SAMPLE s;

    Burada s "strct SAMPLE" türündendir. Ancak s.a int tüdendir. Benzer biçimde s.b long türden, s.c ise double türdendir. Biz yapının elemanlarını bağımsız nesneler 
    gibi kullanabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct SAMPLE {
    int a;
    long b;
    double c;
};

int main(void)
{
    struct SAMPLE s;

    s.a = 10;
    s.b = 20;
    s.c = 30.2;

    printf("%d, %ld, %f\n", s.a, s.b, s.c);
    
    return 0;
} 

/*----------------------------------------------------------------------------------------------------------------------
    Yapı elemanları bildirimde ilk yazılan eleman düşük adreste olacak biçimde ardışıldır. Aşağıdaki örnekte yapı nesnesi içerisindeki elemanların 
    adresleri yazdırılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day, month, year;
};

int main(void)
{
    struct DATE d;

    printf("%p\n", &d.day);
    printf("%p\n", &d.month);
    printf("%p\n", &d.year);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapı bildirmi global düzeyde de yapılabilir, yerel düzeyde de yapılabilir. Eğer yapı bildirimi global düzeyde yapılırsa o yapının ismi her yerde 
    kullanılabilir. Dolyısıyla biz her fonksiyon da istersek o yapı türünden nesneler yaratabiliriz. Örneğin:

    #include <stdio.h>

    struct DATE {
        int day, month, year;
    };

    int main(void)
    {
        struct DATE d;		/* geçerli */
        
        /* ...*/

        return 0;
    }

    void foo(void)
    {
        struct DATE x;		/* geçerli */

        /* ... */
    }

    void bar(void)
    {
        struct DATE m;		/* geçerli */

        /* ... */
    }

    Ancak yapı bildirimleri yerel düzeyde yapılırsa bildirimle oluşturulan tür ismi ancak o blokta kullanılabilir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        struct DATE {
            int day, month, year;
        };

        struct DATE d;		/* geçerli */
        
        /* ...*/

        return 0;
    }

    void foo(void)
    {
        struct DATE x;		/* geçersiz! */

        /* ... */
    }

    void bar(void)
    {
        struct DATE m;		/* geçersiz! */

        /* ... */
    }

    C90'da tüm yerel bildirimler blokların başında yapılmak zorunda olduğu için yerel yapı bildirimleri de blokların başlarında yapılmak zorundadır. 
    Anımsayacağınız gibi C99 ve sonrasında yerel değişkenlerin bildirimleri blokların herhangi bir yerinde yapılabilir hale getirilmiştir. 

    Hemen her zaman programcılar yapı bildirimlerini global düzeyde yaparlar. Bazen yapı bildirimleri başlık dosyalarının içerisine yerleştirilir. 
    Onlar include edildiği zaman derleyici o bildirimleri görmüş olur. 

    Aynı faaliyet alanında yapı ismiyle aynı isimli başka türden bir değişken bildirimi de yapılabilir. Bu durumda bu değişken ismi kullanıldığında başka türden olan ismin 
    kullanıldığı kabul edilir. Ancak struct anahtar sözcüğü ile yapı ismi kullanıdığında artık yapıya ilişkin tür ismi kullanılmış olur. Örneğin:

    struct point {
        int x, y;
    };

    int point;		/* geçerli */

    /* ... */

    point = 10;				/* int olan point kullnımış */
    struct point pt;		/* yapı ismi olan point kullanılmış */

    Tabii bir yapı ile aynı isimde o yapı türünden bir nesne de tanımlayabiliriz. Örneğin:

    struct point {
        int x, y;
    };

    struct point point;			/* geçerli */

    point.x = 10;				/* geçerli */
    point.y = 20;				/* geçerli */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapı bildiriminde yapı elemanlarına ilkdeğer verme anlamsız geçersiz bir işlemdir. Örneğin:

    struct SAMPLE {
        int a = 10;		/* geçersiz! */
        long b = 20;	/* geçersiz! */
    };

    Yukarıdaki işlem bir bildirim işlemidir. Bu bildirim işlemiyle a ve b için bir yer ayrılmaz. Dolayısıyla oraya değer atamak da anlamsız ve geçersizdir.
    Ancak bir yapı nesnesinin parçalarına bildirim sırasında tıpkı dizilerde olduğu gibi küme parantezleri içerisinde değer atayabiliriz. Örneğin:

    struct POINT {
        int x, y;
    };

    struct POINT pt = {10, 20};

    Bu durumda küme parantezlerinin içerisindeki değerler sırasıyla yapı nesnesinin parçalarına atanmaktadır. Yani yukarıdaki örnekte 10 değeri pt.x elemanına, 
    20 değeri de pt.y elemanına atanacaktır. 

    Yapı nesnelerine küme parantezleri ile ilkdeğer verilirken eksik sayıda elemana ilkdeğer verebiliriz. Yine dizilerde olduğu gibi geri kalan elemanlara 
    yapı nesnesi global da olsa yerel de olsa 0 yerleştirilmektedir. Yapı nesnesine küme parantezleri içerisinde ilkdeğer verilirken küme parantezlerinin 
    içi boş bırakılamaz. Örneğin:

    struct POINT pt = {};			// geçersiz!
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct SAMPLE {
    int a;
    long b;
    double c;
};

int main(void)
{
    struct SAMPLE s = {10};

    printf("%d, %ld, %f\n", s.a, s.b, s.c);		/* 10, 0, 0.000000 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte "designated initializer" denilen sentaks yapılar için de oluşturulmuştur. Bu sayede C99 ve ötesinde yapının yalnızca belirli elemanlarına 
    ilkdeğerler verilebilmektedir. Yapılar için designated initializer sentaksı ".<eleman_ismi> = <değer>" biçimindedir. Yine bu sentakstan sonraki değerler
    bu sentaksta belirtilen elemandan sonraki elemanlara sırasıyla atanmaktadır. Örneğin:

    #include <stdio.h>

    struct SAMPLE {
        int a, b, c, d, e, f, g, h;
    };

    int main(void)
    {
        struct SAMPLE s = {1, 2, .e = 10, 4, .g = 20, 30};

        printf("%d %d %d %d %d %d %d %d\n", s.a, s.b, s.c, s.d, s.e, s.f, s.g, s.h);		/* 1 2 0 0 10 4 20 30 */

        return 0;
    }

    Yine aynı elemana birden fazla kez değer atama geçerli kabul edilmektedir. Örneğin:

    #include <stdio.h>

    struct SAMPLE {
        int a, b, c, d, e, f, g, h;
    };

    int main(void)
    {
        struct SAMPLE s = {1, 2, 3, 4, .b = 10, 20};

        printf("%d %d %d %d %d %d %d %d\n", s.a, s.b, s.c, s.d, s.e, s.f, s.g, s.h);		/* 1 10 20 4 0 0 0 0 */

        return 0;
    }

    Burada yapının b ve c elemanlarına iki kez değer atanmıştır. Tabii son atanan değerler bu elemanlarda kalmaktadır. Yine değer atanmayan elemanlarda 
    0 değerinin bulunduğuna dikkat ediniz.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İlkdeğer verilmemiş yapı nesnelerinin elemanlarında nesne yerel ise çöp değerler, nesne global ise 0 değerleri bulunmaktadır. Örneğin:

    struct DATE {
        int day, month, year;
    };

    struct DATE x;		/* x'in elemanlarında 0 değerleri var */

    void foo(void)
    {
        struct DATE d;		/* d'nin elemanlarında çöp değerler var */

        /* ... */
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapı nesneleri bütünsel olarak artimetik işlemlere sokulamaz, karşılaştırma işlemlerine de sokulamaz. Örneğin:

    struct DATE {
        int day, month, year;
    };

    struct DATE x = {10, 12, 2006};
    struct DATE y = {11, 10, 2013};

    if (x > y) {		/* geçersiz! */
        /* ... */
    }

    Yapı nesnelerinin parçalarına erişerek parçaları üzerinde işlemler yapabiliriz. 
/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/
    Ancak aynı türden olmak koşuluyla iki yapı nesnesi bütünsel olarak birbirlerine atanabilmektedir. Örneğin:

    struct DATE {
        int day, month, year;
    };

    struct DATE x = {10, 12, 2006};
    struct DATE y;

    y = x;		/* geçerli */ 

    Bu durumda yapının karşılıklı elemanları birbirine atanmaktadır. (Ancak derleyici elemanlar arasında hizalama amacıyla "padding" boşlukları bırakmışsa 
    bu boşlukların atanması konusunda bir garanti verilmemiştir. Hizalama (alignment) ve padding konusu izleyen paragraflarda ele alınmaktadır.)
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day;
    int month;
    int year;
};

int main(void)
{
    struct DATE d = {10, 12, 1993};
    struct DATE k;

    k = d;

    printf("%d/%d/%d\n", k.day, k.month, k.year);		/* 10/12/1993 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İki yapının içeriği aynı olsa bile bu iki yapı aynı türden değildir. Dolayısıyla bu iki yapı türünden nesne biribirine atanamaz. Örneğin:

    struct DATE {
        int day, month, year;
    };

    struct MATE {
        int day, month, year;
    };

    struct DATE d = {10, 12, 2001};
    struct MATE m;

    m = d;		/* geçersiz! yapılar farklı türlerden */

    Burada iki struct DATE nesnesi biribirine atanabilir. İki struct MATE nesnesi de biribirine atanabilir. Ancak struct DATE ile struct MATE nesneleri birbirine 
    atanamazlar. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapının elemanı bir dizi olabilir. Örneğin:

    struct PERSON {
        char name[32];
        int no;
    };

    Burada yapının name elemanı 32 elemanlık char türden bir dizidir. Bu yapı türünden bir nesne yaratalım:

    struct PERSON per;

    Bir yapı nesnesi yoluyla nokta operatörü kullanılarak yapının dizi elemanına erişilirse yine bu dizi ismi bir nesne belirtmez. Yapı içerisindeki dizinin
    tüm bellekteki başlangıç adresini belirtir. Örneğin per.name ifadesi per yapı nesnesi içerisindeki name dizisinin tüm bellekteki başlangıç adresini 
    belirtmektedir. per.name ifadesi char * türündendir. Yani char türden bir adres belirtmektedir. per.no ifadesi ise int türdendir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

struct PERSON {
    char name[32];
    int no;
};

int main(void)
{
    struct PERSON per;

    strcpy(per.name, "Ali Serce");
    per.no = 123;

    printf("%s, %d\n", per.name, per.no);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                    52. Ders - 13/12/2022 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapının elemanı bir dizi ise yapı nesnesi yoluyla dizinin elemanlarına erişirken nokta ve [] operatörleri aynı ifade içerisinde kullanılmaktadır. 
    Örneğin:

    struct SAMPLE {
        int a[5];
        double b;
    };

    struct SAMPLE s;

    s.a[3] = 10;

    Burada önce nokta operatör üsonra [] operatörü, en sonunda da atama operatörü yapılır:

    İ1: s.a		(buradan int türdne bir adres elde edilecek)
    İ2: İ1[3]	(yapı nesnesi içerisindeki dizinin 3'üncü indisli elemanına erişildi)
    İ3: İ2 = 10

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapı nesnesine ilkdeğer verilirken yapının elemanı dizi ise diziye de ilkdeğer verilebilir. Bu tür durumlarda aslında iç içe küme parantezlerini 
    kullanmak zorunlu değildir. Ancak iyi bir tekniktir. Örneğin:

    struct SAMPLE {
        int a[5];
        double b;
    };

    struct SAMPLE s = { 1, 2, 3, 4, 5, 3.14 };

    Burada iç küme parantezi kullanılmamıştır. Bu durumda derleyici küme parantezlerinin içerisindeki değerleri yapı nesnesinin içerisindeki de diziye yerleştirir. 
    Dizi bittikten sonra geri kalan elemanları dizi elemanından sonraki elemanlara yerleştirecektir. Bu örnekte b elemanına 3.14 değeri yerleştirilecektir:

    s.a[0] => 1
    s.a[1] => 2
    s.a[2] => 3
    s.a[3] => 4
    s.a[4] => 5
    s.c => 3.14
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct SAMPLE {
    int a[5];
    double b;
};

int main(void)
{
    struct SAMPLE s = {1, 2, 3, 4, 5, 3.14};

    for (int i = 0; i < 5; ++i)
        printf("%d ", s.a[i]);
    printf("\n%f\n", s.b);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yapının elemanı bir dizi ise ilkdeğer verme sırasında bu dizi için de küme parantezleri bulundurulabilir. Bu tür durumlarda iç içe küme parantezleri 
    okunabilirliği artırmktadır. Örneğin:

    #include <stdio.h>

    struct SAMPLE {
        int a[5];
        double b;
    };

    struct SAMPLE s = {{1, 2, 3, 4, 5}, 3.14};

    İç içe küme parantezi kullanmaının şöyle bir faydası vardır: Dizi için yanlışlıkla az eleman girilse bile sonraki elemanların yerleri değişmez. Örneğin:

    struct SAMPLE s = {1, 2, 3, 4, 3.14};		/* geçerli ancak muhtemelen programcının istediği bu değil */

    Burada programcı muhtemelen yanlışlıkla yapı içerisindeki dizinin son elemanını girmeyi unutmuştur. 3.14 değeri bu dizinin son elemanı gibi ele alınacaktır. 
    Dolayısıyla yapının b elemanı 0 değerinde kalacaktır:
    
    s.a[0] => 1
    s.a[1] => 2
    s.a[2] => 3
    s.a[3] => 4
    s.a[4] => 3
    s.c => 0
    
    Halbuki biz iç küme parantezini de kullansaydık bu durumda a dizinin son elemanı 0 olacak ancak b elemanı 3.14 değerini 
    alacaktı. Bu durum diğerinden daha anlamlıdır:

    struct SAMPLE s = {{1, 2, 3, 4}, 3.14};

    s.a[0] => 1
    s.a[1] => 2
    s.a[2] => 3
    s.a[3] => 4
    s.a[4] => 0
    s.c => 3.14
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct SAMPLE {
    int a[5];
    double b;
};

int main(void)
{
    struct SAMPLE s = {{1, 2, 3, 4}, 3.14};

    for (int i = 0; i < 5; ++i)
        printf("%d ", s.a[i]);		/* 1 2 3 4 0 */
    printf("\n%f\n", s.b);			/* 3.140000 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    char türden, unsigned char türden ve signed char türden dizilere iki tırnak ile ilkdeğer verebiliyorduk. Ancak bu istisna durumda iki tırnak bir adres
    belirtmiyordu. Bu sentaks "iki tırnağın içerisindeki karakterleri tek tek diziye yerleştir" anlamına geliyordu. O halde aynı anlam yapının içrisinde 
    de geçerlidir. Örneğin:

    struct PERSON {
        char name[32];
        int no;
    };

    struct PERSON per = {"Ali Serce", 123};		/* geçerli */

    Burada "ALi Serce" yazısı bir string belirtmez. Yazının karakterleri per nesnesinin içerisindeki name dizisinin elemanlarına ,
    yerleştirilecektir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct PERSON {
    char name[32];
    int no;
};

int main(void)
{
    struct PERSON per = {"Ali Serce", 123};

    printf("%s %d\n", per.name, per.no);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yapının içeisindeki dizi isminin de nesne belirtmediğine dikkat ediniz. Örneğin:

    struct PERSON {
        char name[32];
        int no;
    };

    struct PERSON per;

    per.name = "Ali Serce";		/* geçersiz! */

    Burada dizi ismine bir adres atanmak istenmiştir. Bu durum geçersizdir. Böylesi bir şeyi ilkdeğer verme dışında yine strcpy fonksiyonu ile yapmalıyız. 
    Örneğin:

    struct PERSON per;

    strcpy(per.name, "Ali Serce");
    per.no = 123;
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapının elemanı gösterici de olabilir. Ancak tabii yapı nesnesi tanımlandığında bu gösterici içerisinde rastgele bir adres vardır. Örneğin:

    struct PERSON {
        char *name;
        int no;
    };

    struct PERSON per;	

    Burada per nesnesi yerel ise name göstericisinin içerisinde rastgele bir adres, no içerisinde de rastgele bir değer bulunur. Ancak per global ise 
    name göstericisinin içerisinde NULL adres, no içerisinde de 0 bulunacaktır. (Anımsanacağı gibi global bir göstericiye değer atanmadıysa içeisinde NULL adres bulunacağı 
    garanti edilmiştir.) Örneğin:

    struct PERSON per;

    strcpy(per.name, "Ali Serce");		/* dikkat yazı rastgele bir adrese atanıyor */

    Burada "Ali Serce" yazısı per.name adresinden itibaren bellekte rastgele bir yere yerleştirilecektir. Dolayısıyla tanımsız davranış söz konusudur. 
    Ancak örneğin:

    per.name = "Ali Serce";		/* tamamen normal */

    Burada "Ali Serce" yazısı derleyici tarafından char türden statik ömürlü bir diziye yerleştirilip o alanın adresi per.name göstericisine atanmıştır. 
    Dolayısıyla per.name göstericisi güvenli (yani tahsis edilmiş) bir yeri göstermektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct PERSON {
    char *name;
    int no;
};

int main(void)
{
    struct PERSON per;

    per.name = "Ali Serce";				/* tamamen normal, name göstericisine güvenli bir yazının adresi atanıyor */
    per.no = 123;
    printf("%s %d\n", per.name, per.no);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii yapının elemanı char türden bir gösterici ise biz yine iki tırnak içerisinde bu elemana ilkdeğer verebiliriz. Şüphesiz bu durumda verilen ilkdeğer 
    aslında bir adrestir ve gösteriye bu adres atanmış olmaktadır. Örneğin:

    struct PERSON {
        char *name;
        int no;
    };

    struct PERSON per = {"Ali Serce", 123};
    
    Burada artık per.name göstericisinin içerisinde "Ali Serce" yazısının başlangıç adresi bulunacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct PERSON {
    char *name;
    int no;
};

int main(void)
{
    struct PERSON per = {"Ali Serce", 123};
    
    printf("%s %d\n", per.name, per.no);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yapı türünden nesneler bileşik nesnelerdir. Ancak biz bir yapı nesnesinin adresini & operatörü ile alabiliriz. Bu durumda elde edilen adresin tür bileşeni 
    ilgi yapı türünden, sayısal bileşeni de bellekteki yapı nesnenin başlagıcına ilişkin doğrusal adresten oluşur. Bir yapı nesnesinin adresi ancak aynı türden bir 
    yapı göstericisine artanabilir. Örneğin:

    struct SAMPLE {
        int a;
        long b;
        double c;
    };

    struct SAMPLE s;
    struct SAMPLE *ps;

    ps = &s;

    Burada artık ps göstericisi s nesnesini göstermektedir. Bir yapı tründen adresi biz * (indirection) operatörü ile kullanırsak o yapı göstericisinin gösterdiği yerdeki 
    yapı nesnesinin tamamına erişmiş oluruz. Yukarıdaki örnekte *ps ifadesi struct SAMPLE türündendir ve nesnenin bütününü temsil etmektedir. Başka bir deyişle *ps 
    ile s tamamen aynı nesneyi belirtmektedir. 

    ps bir yapı türünden adres a da ilgili yapının bir elemanı olmak üzere ps adresinin gösterdiği yerdeki yapı nesnesinin a elemanına erişmek için (*ps).a 
    ifadesi kullanılır. Burada *ps'nin parantez içerine alındığına dikkat ediniz. Nokta operatörünün sol tarafındaki operand bir yapı nesnesinin bütünü olmak zorundadır. 
    Eğer burada parantezler kullanılmazsa *ps.a ifadesi geçersiz olur. Çünkü nokta operatörü * operatöründen daha önceliklidir. Dolayısıyla burada önce 
    nokta operatörü sonra buna * operatörü uygulanacaktır. Nokta operatörünün sol tarafındaki operand bir yapı adresi olamaz. Yapı nesnesi olmak zorundadır. 
    Dolayısıyla eleman erişimde * operatörünün paranteze alınması gerekmektedir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct SAMPLE {
    int a;
    long b;
    double c;
};

int main(void)
{
    struct SAMPLE s = {10, 12, 14.25};
    struct SAMPLE *ps;

    ps = &s;

    printf("%d\n", (*ps).a);
    printf("%ld\n", (*ps).b);
    printf("%f\n", (*ps).c);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yapı türündne göstericilerle yapı elemanlarına erişirken * operatörünü paranteze almayı unutmayınız. Nokta operatörünnü solunda bir yapı nesnesi bulunmalıdır. 
    Nokta operatör bir yapı nesnesi ile yapının elemanına erişmekte kullanılır. Eğer elimizde bir yapı türünden adres varsa önce * ile o nesneyi elde edip 
    sonra nokta operatörünü uygulamalıyız. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct SAMPLE {
    int a;
    long b;
    double c;
};

int main(void)
{
    struct SAMPLE s = {10, 12, 14.25};
    struct SAMPLE *ps;

    ps = &s;

    printf("%d\n", (*ps).a);
    printf("%ld\n", (*ps).b);
    printf("%f\n", (*ps).c);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Standartlara göre yapı elemanları bildirimde ilk belirtilen eleman düşük adreste olacak biçimde ardışıl yerleşim uygulanmaktadır.
    Yapının ilk elemanı en düşük adreste bulunacağına göre bir yapı nesnesinin adresinin sayısal bileşeni ile onun ilk elemanının adresinin sayısal bileşeni aynıdır. 
    Ancak bunların tür bileşenleri farklıdır. Örneğin:

    struct DATE {
        int day, month, year;
    };

    struct DATE d;

    Burada &d ifadesi struct DATE * türündendir. Halbuki &d.a ifadesi int * türündendir. Ancak bunların sayısal bileşenleri aynıdır. Çünkü yapı nesnesinin 
    hemen başında yapının ilk elemanı bulunmak zorundadır.  Bir yapı elemanın adresi alınırken parantez gerekmediğine dikkat ediniz. Örneğin &d.day ifadesinde 
    önce d.day işlemi yapılacak ve yapının day elemanına erişilecek sonra da bu elemanın adresi alınacaktır.Nokta operatörünün & operatöründen daha yüksek 
    öncelikli olduğunu anımsayınız.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapı göstericisi ile yapı elemanına çok sık erişilmektedir. Yani ps bir yapı türünden adres  a da bu yapının bir elemanı olmak üzere (*ps).a gibi ifadeler
    çok sık kullanılmaktadır. İşte bu tür ifadeleri daha kolay yazabilmek için "Ok operatörü (arrow operator)" düşünülmüştür. Ok operatörü -> karakterleriyle 
    temsil edilmektedir. -> operatörünün solundaki operand bir yapı türünden adres sağındaki operand ise o yapının bir elemanı olmak zorundadır. (*ps).a ifadesi 
    ile ps->a ifadesi tamamen eşdeğerdir. Böylece biz (*ps).a gibi bir ifade yerine ps->a ifadesini yazım bakımındanm tercih ederiz. 

    Ok operatörü iki operand'lı araek bir adres operatörüdür. Ok operatörünün solunda bir yapı türünden adres sağında ise yapının bir elemanı bulunmak zorundadır. 
    Operatör o adresin gösterdiği yerdeki yapı nesnesinin ilgili elemanına erişmek için kullanılmaktadır. Örneğin:

    struct SAMPLE s;
    struct SAMPLE *ps;

    ps = &s;

    Burada biz s nesnesinin a parçasına s.a ifadesiyle ya da (*ps).a ifadesi ile  erişebiliriz. İşte ps göstericisinin gösterdiği yerdeki nesnenin 
    a parçasına	ps->a ifadesi ile de erişebiliriz. (*ps).a ifadesi ile ps->a ifadesi tamamen aynı anlama gelmektedir. 

    Nokta ve ok operatörlerini birbirine karıştırmayınız. Her iki operatör de yapının elemanına erişmekte kullanılır. Yani bunların sağ tarafındaki operand'lar
    yapının elemanını belirtir. Ancak nokta operatörünün solundaki operand yapı nesnesinin bütünüyken, ok operatörünün solundaki operand yapı nesnesinin 
    adresi olmak zorundadır. 

    Ok operatörü öncelik tablosunun en yüksek düzeyinde soldan-sağa grupta bulunmaktadır:

    () [] .	->						Soldan-Sağa
    + - ++ -- !	& *	sizeof (tür)	Sağdan-Sola
    * / %							Soldan-Sağa
    + -								Soldan-Sağa
    <  > <= >=						Soldan-Sağa
    != ==							Soldan-Sağa
    &&								Soldan-Sağa
    ||								Soldan-Sağa
    ?:								Sağdan-Sola
    =, +=, /=, *=,...				Sağdan-Sola
    ,								Soldan-Sağa	 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day, month, year;
};

int main(void)
{
    struct DATE d = {10, 12, 2009};
    struct DATE *pd;

    pd = &d;

    printf("%d/%d/%d\n", pd->day, pd->month, pd->year);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki gibi bir yapı olsun:

    struct SAMPLE {
        int a;
        double b;
    };

    Bu yapı türünden s isminde bir nesne tanımlayalım:

    struct SAMPLE s;

    &s->a ifadesi geçersizdir. Çünkü burada -> operatörünün solundaki operand &s değil s'tir. -> operatörü & operatöründen daha yüksek önceliklidir. 
    -> operatörünün solunda yapı nesnesi bulunamaz. Bir yapı nesnesinin adresi bulunmak zorundadır. Ancak (&s)->a ifadesi geçerlidir. Artık parantezden dolayı
    önce &s işlemi yapılacak ve buradan bir yapı adresi elde edilecek sonra -> operatöryle o adreste bulunan nesnenin a parçasına erişilecektir. Tabii (&s)->a
    ifadesine gerek yoktur zaten s.a ifadesi aynı işi yapmaktadır.

    Aşağıdaki gib bir yapı olsun:

    struct CITY {
        char name[32];
        int plate;
    };

    Bu yapı türünden bir nesne bir de gösterici tanımlayalım:

    struct CITY c = {"Ankara", 6};
    struct CITY *pc = &c;

    Burada pc->name ifadesi char * türündendir. Ancak pc->name bir nesne belirtmez. pc adresinde bulunan yapı nesnesinin içerisindeki dizinin başlangıç adresini 
    belirtir. pc->name[i]  gibi bir ifadede önce ok operatör sonra [] operatörü yapılacaktır. Bu durumda bu ifade char türdendir. Bu ifade pc göstericisinin 
    gösterdiği yerdeki yapı nesnesinin içerisindeki name dizisinin başlangıç adresinden itibaren i ilerideki char nesneyi belirtmektedir. &pc->plate ifadesi 
    int * türündedir. Yani pc göstericisinin gösterdiği yerdeki yapı nesnesinin plate elemanın adresini belirtir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct CITY {
    char name[32];
    int plate;
};

int main(void)
{
    struct CITY c = {"Ankara", 6};
    struct CITY *pc = &c;

    putchar(pc->name[2]);		/* k */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yapılar türünden diziler de söz konusu olabilir. Bunlara yapı dizileri (array of structure) denilmektedir. Yapı dizilerinin her elemanı bir yapı 
    nesnesidir. Örneğin:

    struct CITY {
        char name[32];
        int plate;
    };

    struct CITY cities[5];

    Burada cities her elemanı struct CITY türünden olan 5 elemanlı bir dizidir. Yapı elemanları ardışıldır, dizi elemanları da ardışıldır. O zaman 
    buradaki dizinin tüm elemanları bellekte ardşıl bir biçimde tutulacaktır. 

    Bir yapı dizisine ilkdeğer verilirken yine küme parantezleri kullanılmalıdır. Dizinin her elemanı bir yapı olduğuna göre elemanlar için de ayrıca küme parantezleri 
    kullanılabilir. Ancak C'de elemanlar için küme parantezlerinin kullanılması zorunlu tutulmamıştır. Fakat yukarıda daha önce açıkladığımız gerekçelerden dolayı 
    elemanlar için de ayrıca küme parantezi kullanmak iyi bir tekniktir. Örneğin:

    struct CITY cities[5] = {"Ankara", 6, "Izmir", 35, "Eskisehir", 26, "Kutahya", 43, "Istanbul", 34};		/* geçerli ama kötü teknik */

    Burada bir sorun oluşmayacaktır. Elemanlar sırasıyla dizi içerisindeki yapı elemanlarına atanacaktır. Tabii bu iyi bir teknik değildir. Dizinin elemanı olan 
    yapıların da ayrıca küme parantezlerine alınması iyi bir tekniktir:

    struct CITY cities[5] = {{"Ankara", 6}, {"Izmir", 35}, {"Eskisehir", 26}, {"Kutahya", 43}, {"Istanbul", 34}};		/* iyi teknik */

    Bir yapı dizisinin belli bir elemanına [] operatörü ile erişebiliriz. O elemanın da elemanına nokta operatörü ile erişebilir. Bu durumda örneğin
    cities[2].name[3] ifadesinde üç operatör vardır. Bunların hepsi soldan sağa operatörlerdir:

    İ1: cities[2]
    İ2: İ1.name
    İ3: İ2[3]

    Burada cities[2] ifadesi struct CITY türünden, cities[2].name ifadesi char * türünden ve cities[2].name[3] ifadesi de char türdendir. 
    O halde cities[2].name[3] ifadesi "Eskisehir" yazısının 'i' karakterini belirtir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct CITY {
    char name[32];
    int plate;
};

int main(void)
{
    struct CITY cities[5] = {
        {"Ankara", 6}, 
        {"Izmir", 35}, 
        {"Eskisehir", 26}, 
        {"Kutahya", 43}, 
        {"Istanbul", 34}
    };
    
    for (int i = 0; i < 5; ++i)
        printf("%s, %d\n", cities[i].name, cities[i].plate);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizinin ismi ifade içerisinde kullanıldığında dizinin ilk elemanın adresini belirtiyordu. O zaman biz bir yapı dizsinin ismini aynı türden bir 
    yapı göstericisine atayabiliriz. Örneğin:

    struct CITY cities[5] = {
        {"Ankara", 6}, 
        {"Izmir", 35}, 
        {"Eskisehir", 26}, 
        {"Kutahya", 43}, 
        {"Istanbul", 34}
    };
    struct CITY *pc;

    pc = cities;	

    Burada artık pc göstericisi cities dizinin ilk elemanını göstermektedir. Biz bu ilk elemanı şöyle yazdırabiliriz:

    printf("%s, %d\n", pc->name, pc->plate);

    Bir adresi 1 artırdığımızda adresin sayısal bileşeni adresin türünün uzunluğu kadar artıyordu. Bu durumda biz bir yapı göstericisini 1 artırırsak 
    gösterici içerisindeki adres göstericinin türünün uznluğu kadar artacaktır. O halde örneğin:

    ++pc;

    Artık burada pc göstericisi "İzmir" ile ilgili yapı nesnesini göstermektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct CITY {
    char name[32];
    int plate;
};

int main(void)
{
    struct CITY cities[5] = {
        {"Ankara", 6}, 
        {"Izmir", 35}, 
        {"Eskisehir", 26}, 
        {"Kutahya", 43}, 
        {"Istanbul", 34}
    };
    struct CITY *pc;

    pc = cities;

    printf("%s, %d\n", pc->name, pc->plate);
    
    ++pc;

    printf("%s, %d\n", pc->name, pc->plate);

    ++pc;

    printf("%s, %d\n", pc->name, pc->plate);

    ++pc;

    printf("%s, %d\n", pc->name, pc->plate);

    ++pc;

    printf("%s, %d\n", pc->name, pc->plate);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Dizi isimlerinin adres belirttiğini ancak nesne belirtmediğini anımsayınız. Bir yapı dizisinin ismi o yapının başlangıç adresini yani ilk elemanın adresini 
    belirtir. Ancak biz bu ismi ++ operatörüyle artıramayız. Örneğin:

    struct CITY cities[5] = {
        {"Ankara", 6}, 
        {"Izmir", 35}, 
        {"Eskisehir", 26}, 
        {"Kutahya", 43}, 
        {"Istanbul", 34}
    };
    
    printf("%s, %d\n", cities->name, cities->plate);		/* geçerli */

    ++cities;		/* geçersiz! */

    return 0;
}

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct CITY {
    char name[32];
    int plate;
};

int main(void)
{
    struct CITY cities[5] = {
        {"Ankara", 6}, 
        {"Izmir", 35}, 
        {"Eskisehir", 26}, 
        {"Kutahya", 43}, 
        {"Istanbul", 34}
    };
    
    printf("%s, %d\n", cities->name, cities->plate);
    printf("%s, %d\n", (cities + 1)->name, (cities + 1)->plate);
    printf("%s, %d\n", (cities + 2)->name, (cities + 2)->plate);
    printf("%s, %d\n", (cities + 3)->name, (cities + 3)->plate);
    printf("%s, %d\n", (cities + 4)->name, (cities + 4)->plate);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Ritchie & Kernighan yazım tarzında genel olarak iki operand'lı operatörlerle operand'lar arasında birer boşluk karakteri kullanılmaktadır. Örneğin:

    a = b + c;

    Ancak nokta ve ok operatörleri iki operand'lı olmasına karşın bu operatörler ile operand'ları arasında boşluklar kullanılmamaktadır. 
    Örneğin s.a ve ps->a gibi. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        53. Ders - 15/12/2022 Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki gibi bir yapı söz konusu olsun:

    struct SAMPLE {
        int a;
        int b;
        double c;
    };

    Bu yapı türünden bir gösteriye sahip olalım:

    struct SAMPLE s;
    struct SAMPLE *ps = &s;

    Biz bir yapı göstericisi ile yapının bir elemanına -> operatöryle eriştiğimizde derleyici elemanın yerini nasıl belirlemektedir? Örneğin 
    ps göstericisinin içerisindeki adresin sayısal bileşeni 0x1FC10 olsun. ps->c gibi bir ifadede derleyici yapının c elemanına nasıl erişecektir? 
    İşte yapının bir elemanına derleyicinin bu biçimde erişebilmesi için yapı elemanlarının ardışıl olması gerekmektedir. Derleyici yapı bildirimine baktığında
    hemen 0x1FC10 adresinden itibaren 4 byte'ın a elemanı olduğunu, sonraki 4 byte'ın b elemanı olduğunu anlar (int türünün 4 byte oluduğunu varsayıyoruz.) 
    O halde derleyici ps->c ifadesinde c'nin ps adresinden itibaren 8 byte ileride bulunduğunu ve 8 byte uzunlukta olduğunu bilecektir. Yapı elemanları bellekte 
    ardışıl bir biçimde peşi sıra yerleştirilmeseydi derleyici bu elemanın yerini tespit edemezdi. 

    Özetle bir yapı adresinden hareketle yapı elemanlarının yerlerinin bilinebilmesi için derleycinin yapı bildirimini görmesi ve elemanların ardışıl olduğunu
    kabul etmesi gerekir. Aşağıdaki örnekte yapı elemanlarıın adresleri yazdırılmıştır. Tabii buradaki adresler programın o çalışmasına özgü adreslerdir. 
    Ancak biz fikir vermesi için adreslerin değerlerini de yazdık.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct SAMPLE {
    int a;
    int b;
    double c;
};

int main(void)
{
    struct SAMPLE s;
    struct SAMPLE *ps = &s;

    printf("%p\n", ps);				/* 0000002B7395FD70 */
    printf("%p\n", &ps->a);			/* 0000002B7395FD70 */
    printf("%p\n", &ps->b);			/* 0000002B7395FD74 */
    printf("%p\n", &ps->c);			/* 0000002B7395FD78 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapı nesnesini bir fonksiyona parametre yoluyla aktarabilmek için iki yöntem kullanılmaktadır. Bunlardan biri kötü teknik, diğeri iyi tekniktir: 

    1) Yapı nesnesinin kopyasının fonksiyona aktarılması yöntemi (kötü teknik)
    2) Yapı nesnesinin adres yoluyla fonksiyona aktarılması yöntemi (iyi teknik)

    Birinci tekniğe "değer yoluyla aktarım (call by value)", ikinci tekniğe "adres yoluyla aktarım (call by reference)" 
    da denilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    1) Yapı nesnesinin kopyasının fonksiyona aktarılması yöntemi: Bu yöntemde fonksiyonun parametre değişkeni bir yapı türünden yapı nesnesi olur. Fonksiyon da 
    aynı türden bir yapı nesnesi ile çağrılır. Aynı türden iki yapı nesnesi birbirlerine atanabildiğine göre bu durum geçerlidir. Fonksiyon içerisinde yapı elemanlarına nokta 
    operatörüyle erişilir. Örneğin:

    #include <stdio.h>

    struct SAMPLE {
        int a;
        int b;fd
        int c;
    };

    void foo(struct SAMPLE k)		/* k = s */
    {
        printf("%d, %d, %d\n", k.a, k.b, k.c);
    }

    int main(void)
    {
        struct SAMPLE s = {10, 20, 30};

        foo(s);			/* kötü teknik */

        return 0;
    }

    Burada aslında k = s gibi bir işlem yapılmaktadır. Bu durumda s'in her elemanı k'ya atanacaktır. Bu tekniğin iki dezavantajı vardır: Birincisi yapı
    nesnesinin her elemanının diğer yapı nesnesine kopyalanmasıdır. Örneğin 100 elemanlı bir yapı olsa bu durum karşılıklı olarak 100 elemanın kopyalanacağı 
    anlamına gelir. Bu ise göreli bir zaman kaybı oluşturmaktadır. Ayrıca bu yöntemde fonksiyon içerisinde parametre olarak aktarılan nesnede bir değişiklik 
    yapılamamaktadır. Bu teknik genel olarak kötü bir tekniktir. Ancak tabii yapılar küçükse bu teknik uygulanabilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct SAMPLE {
    int a;
    int b;
    int c;
};

void foo(struct SAMPLE k)		/* k = s */
{
    printf("%d, %d, %d\n", k.a, k.b, k.c);
}

int main(void)
{
    struct SAMPLE s = {10, 20, 30};

    foo(s);			/* kötü teknik */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    2) Yapı nesnesinin adres yoluyla fonksiyona aktarılması yöntemi: Bu yöntemde fonksiyonun parametre değişkeni bir yapı türünden gösterici olur. Fonksiyon da aynı 
    yapı türünden bir yapı nesnesinin adresiyle çağrılır. Fonksiyonun içersinde yapı elemanlarına ok operatöryle erişilebilir. Örneğin:

    #include <stdio.h>

    struct SAMPLE {
        int a;
        int b;
        int c;
    };

    void foo(struct SAMPLE *ps)				/* ps = &s */
    {
        printf("%d, %d, %d\n", ps->a, ps->b, ps->c);
    }

    int main(void)
    {
        struct SAMPLE s = {10, 20, 30};

        foo(&s);			/* iyi teknik */

        return 0;
    }

    Bu yöntemde yapı ne kadar büyük olursa olsun aktarılan yalnızca bir adres bilgisidir. Bu yöntemde aynı zamanda fonksiyon içerisinde asıl nesnenin 
    elemanlarını değiştirme olanağı da vardır. Örneğin:

    #include <stdio.h>

    struct SAMPLE {
        int a;
        int b;
        int c;
    };

    void set(struct SAMPLE *ps)				/* ps = &s */
    {
        ps->a = 100;
        ps->b = 200;
        ps->c = 300;
    }

    void disp(struct SAMPLE *ps)			/* ps = &s */
    {
        printf("%d, %d, %d\n", ps->a, ps->b, ps->c);
    }

    int main(void)
    {
        struct SAMPLE s = {10, 20, 30};

        disp(&s);
        set(&s);
        disp(&s);

        return 0;
    }

    Tabii aslında böyle bir aktarımın mümkün olabilmesi için yapı elemanlarının ardıllığının garanti edilmiş olması gerekir. Başka bir deyişle eğer yapı elemanları 
    ardışıl olmasydı yapı nesnesinin adresini alan fonksiyon elemanların yerlerini tespit edemezdi. 

    Uygulamada hemen her zaman biz fonksiyonun parametre değişkeninin bir yapı göstericisi olduğunu görürüz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day;
    int month;
    int year;
};

void disp_date(struct DATE *pd)
{
    printf("%d/%d/%d\n", pd->day, pd->month, pd->year);
}

void get_date(struct DATE *pd)
{
    printf("Day: ");
    scanf("%d", &pd->day);

    printf("Month: ");
    scanf("%d", &pd->month);

    printf("Year: ");
    scanf("%d", &pd->year);
}

int main(void)
{
    struct DATE date;

    get_date(&date);
    disp_date(&date);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Örneğin ekranda bir pixel'in koordinatlarını belirten aşağıdaki gibi bir yapı olsun:

    struct POINT {
        int x, y;
    };

    İki noktayla verilen bir doğruyu çizen draw_line isimli fonksiyonun parametrik yapısı şöyle olabilir:

    void draw_line(struct POINT *pt1, struct POINT *pt2);

    Biz de bu fonksiyonu şöyle çağırabiliriz:

    struct POINT pt1 = {10, 2}, pt2 = {20, 4};

    draw_line(&pt1, &pt2);
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi yapılar neden kullanılmaktadır? Yapılar üç nedenden dolayı programcılar tarafından kullanılır:

    1) Birbirleriyle ilişkili olgular bir yapı olarak ifade edilirse kavramsal kolaylık sağlanır. Örneğin bir tarih kavramı, bir nokta kavramı, bir insanın
    kimlik bilgileri birbirinden kopuk bilgiler değildir. Bunların ayrı nesneler içerisinde tutulması hem anlamlandırmayı zorlaştırır hem de programcıya ek 
    bir yük getirir. 

    2) Fonksiyonların çok fazla parametreye sahip olması iyi bir teknik değildir. Örneğin bir insanın kimlik bilgileri farklı türlere ilişkin yirminin üzerinde
    bileşene sahiptir. Bu bilgilerin farklı parametreler yoluyla fonksiyona geçirilmesi hem çok zahmetlidir hem de zaman kaybına yol açar. Eğer bir fonksiyona 
    çok sayıda bilgi parametre yoluyla aktarılacaksa burada yapılması gereken şey aktarılacak bilgileri bir yapı olarak organize edip yapı nesnesini adres yoluyla 
    fonksiyona geçirmektir. 

    3) Bir fonksiyonun tek bir geri dönüş değeri vardır. Ancak fonksiyonlar bazen birden fazla değeri iletmek isteyebilirler. İşte bu tür durumlarda yapılması gereken şey
    fonksiyonun ileteceği bilgileri bir yapı olarak bildirmeki fonksiyona bu yapı türünden bir yapı nesnesinin adresini geçmektir. Fonksiyon da bu nesnenin içini 
    dolduracaktır. C'de bir fonksiyonun birden fazla değeri çağıran fonksiyona iletmesi söz konusu olduğununda hemen aklımıza yapılar gelmelidir. Örneğin 
    bir fonksiyonun sistemin o anki tarhini alarak bize verdğiğini düşünelim. Tarih üç bileşene sahiptir. O halde fonksiyon bize üç değer vermelidir. İşte bu 
    tür durumlarda aklımıza hemen yapı kullanımı gelmelidir. Örneğin:

    struct DATE {
        int day, month, year;
    };

    void get_current_date(struct DATE *pd);
    ...

    struct DATE date;

    get_current_date(&date);

    Örneğin bir fonksiyon o anki sistem hakkında birtakım bilgileri elde etmek istesin. Örneğin kullanılan CPU'yu, hard disklerin boyutlarını, işletim sisteminin 
    versiyonunu vs. Bunun için get_sysinfo isimli bir fonksiyonun bulunduğunu düşünelim. Burada iletilecek bilgiler çok fazladır. O zaman muhtemelen fonksiyonun 
    parametrik yapısı şöyle olacaktır:

    struct SYSINFO {
        /* gerekli bilgiler */
    };

    void get_sysinfo(struct SYSINFO *si);
    ...

    struct SYSINFO si;

    get_sysinfo(&si);

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun geri dönüş değeri de bir yapı nesnesi olabilir. Örneğin:

    struct COMPLEX {
        double real;
        double imag;
    };

    struct COMPLEX add_complex(struct COMPLEX *pz1, struct COMPLEX *pz2);

    Burada add_complex isimli fonksiyon struct COMPLEX türünden bir yapı nesnesi vermektedir. Yani fonksiyonun geri dönüş değeri bileşik bir nesnedir. Tabii 
    bu tür durumlarda return ifadesinin de aynı türden bir yapı nesnesi olması gerekir. Böylesi fonksiyonlarının geri dönüş değerlerinib atanacağı nesnesin de 
    aynı türden bir yapı nesnesi olması gerekir. Örneğin:

    struct COMPLEX z1 = {3, 2}, z2 = {5, 6}, z3;

    z3 = add_complex(&z1, &z2)

    Fonksiyonun geri dönüş değerinin bir yapı nesnesi olması durumuyla C'de seyrek karşılaşılmaktadır. Çünkü bunun iki önemli dezavantajı vardır: Birincisi 
    fonksiyonu yazarken fonksiyonun içerisinde return işleminde geçici nesneye atama yapılırken yapının karşılıklı elemanları birbirine atanacaktır. 
    Yapı büyükse bu bir zaman kaybına yol açar. İkincisi ise böylesi fonksiyonların geri dönüş değerlerinin de aynı türden bir yapıya atanması zorunluluğudur. 
    Bu da bir zaman kaybına yol açmaktadır. Ancak eğer yapılar küçükse bu teknik de uygulanabilir. Fakat genel olarak programcılar C'de böyle bir teknik kullanmazlar. 

    C derleycileri yukarıddaki gibi durumlarda bazı optimizasyonlarla kodun daha etkin çalışmasını sağlayabilmektedir. 
    Örneğin:

    z3 = add_complex(&z1, &z2)

    Burada C derleyicisi aslında gizlice z3 nesnesini adresini de fonksiyona gönderebilir. Hiç bu atamaları yapmadan 
    sonucun doğrudan z3 nesnesine yerleştirilmesini sağlayabilir. Yani derleyici adeta bizim yapmamız gereken doğru tekniği 
    kendisi gizlice uygulayabilir. Tabii derleyicilerin optimizasyonlarına güvenmek ancak spesifik bir derleyici için 
    uygun olabilir. Bir derleyicinin yaptığı optimizasyonu diğer bir derleyici yapamayabilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct COMPLEX {
    double real;
    double imag;
};

struct COMPLEX add_complex(struct COMPLEX *pz1, struct COMPLEX *pz2)
{
    struct COMPLEX result;

    result.real = pz1->real + pz2->real;
    result.imag = pz1->imag + pz2->imag;

    return result;
}

int main(void)
{
    struct COMPLEX z1 = {3, 4}, z2 = {1, 6}, z3;

    z3 = add_complex(&z1, &z2);
    printf("%.0f+%.0fi\n", z3.real, z3.imag);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii hiçbir fonksiyon C'de yerel bir nesnenin (static olmayan) adresiyle geri dönmemelidir. Aşağıdaki gibi bir fonksiyon hatalı bir tasarımdır:

    struct COMPLEX {
        double real;
        double imag;
    };

    struct COMPLEX *add_complex(struct COMPLEX *pz1, struct COMPLEX *pz2)
    {
        struct COMPLEX result;

        result.real = pz1->real + pz2->real;
        result.imag = pz1->imag + pz2->imag;

        return &result;
    }

    Burada fonksiyon bir yapı nesnesinin adresiyle geri dönmektedir. Ancak fonksiyonun çağrısı bitince bu yerel değişken stack'ten boşaltılacağı için bu durum
    tanımsız davranışa yol açacaktır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapı elemanları bellekte ardışıl yer kapladığına göre biz bir yapı nesnesini malloc, calloc fonksiyonlarıyla heap'te dinamik bir biçimde tahsis edebiliriz. 
    Örneğin:

    struct PERSON {
        char name[32];
        int no;
    };

    struct PERSON *per;

    if ((per = (struct PERSON *)malloc(sizeof(struct PERSON))) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }

    Burada malloc fonksiyonu ile heap'te sizeof(struct PERSON) kadar byte tahsis edilmiştir. sizeof operatörüne bir tür ismi, verildiğinde bu operatör
    o türün o sistemde kaç byte yer kapladığını bize veriyordu. O halde aslında sizeof(struct PERSON) ifadesinde bir struct PERSON nesnesinin ilgili sistemde
    kaç byte yer kapladığı bilgisi elde edilmektedir. Tabii kullanım bittikten sonra bu alanın free hale getirilmesi gerekir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

struct PERSON {
    char name[32];
    int no;
};

int main(void)
{
    struct PERSON *per;

    if ((per = (struct PERSON *)malloc(sizeof(struct PERSON))) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }

    strcpy(per->name, "Ali Serce");
    per->no = 123;

    printf("%s, %d\n", per->name, per->no);

    free(per);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun içerisinde bir yapı nesnesi dinamik olarak tahsis edilip fonksiyon onun başlangıç adresiyle geri dönebilir. Bu durumda fonksiyonu çağıran 
    kişi dinamik olarak tahsis edilmiş olan yapı nesnesinin adresini elde edecektir. Bilindiği gibi bir fonksiyonun içerisinde dinamik tahsisat yapılırsa 
    fonksiyondan çıkılsa bile o alan tahsis edilmiş bir biçimde kalmaya devam etmektedir. Ta ki free işlemi uygulanana kadar. 

    Aşağıdaki örnekte get_person fonksiyonu bir kişinin adını, soyadını ve numarasını klavyeden (stdin dosyasından) okuyarak dinamik biçimde tahsis edilen bir yapı nesnesinin 
    içerisine yerleştirmektedir. Fonksiyonu çağıran kişi bu adresteki nesneye ulaşmış, onu kullanmış ve nihayet onu free hale getirmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

struct PERSON {
    char name[32];
    int no;
};

struct PERSON *get_person(void)
{
    struct PERSON *per;

    if ((per = (struct PERSON *)malloc(sizeof(struct PERSON))) == NULL)
        return NULL;

    printf("Adi soyadi:");
    gets(per->name);
    printf("No:");
    scanf("%d", &per->no);

    while (getchar() != '\n')
        ;

    return per;
}

int main(void)
{
    struct PERSON *per;

    if ((per = get_person()) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }

    printf("%s, %d\n", per->name, per->no);

    free(per);

    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
    time isimli standart C fonksiyonu <time.h> dosyası içerisinde aşağıdaki gibi bildirilmiştir:

    time_t time(time_t *pt);

    Buradaki time_t türü ileride göreceğimiz typedef işlemi ile oluşturulmuştur. Standartlara time_t derleyicileri yazanların 
    tamsayı ya da gerçek sayı türlerinden bir tanesi olarak typedef edecekleri bir türü belirtir. Programcının bu türün ne olarak typedef edildiğini 
    bilmesine gerek yoktur. Fonksiyon bilgisayarın saatine bakarak belli bir orijinden geçen saniye sayısını bize verir. Bu orijine "epoch" denilmektedir. 
    Eğer biz time_t türünden bir nesnenin adresini fonksiyona geçersek fonksiyon bu değeri adresini geçtiğimiz nesneye yerleştirir ve aynı değerle geri döner. 
    Ancak parametre olarak NULL adres geçebiliriz. Bu durumda fonksiyon bu değeri parametresiyle belirtilen adrese yerleştirmez ancak saniye sayısını 
    yine geri dönüş değeri olarak verir. Programcılar bu fonksiyonu genellikle şöyle çağırırlar:

    time_t t;

    t = time(NULL);

    Ya da şöyle kullanırlar:

    time_t t;

    time(&t);

    Ancak bu saniye sayısını her iki yöntemle de elde etmenin bir anlamı yoktur. Örneğin:

    time_t t;

    t = time(&t);		/* geçerli ama gereksiz */

    time fonksiyonun epoch orijini C standartlarında belirtilmemiştir. Ancak geleneksel olarak 01/01/1970 alınmaktadır. 
    
    Aşağıda time fonksiyonundan elde edilen değer ekrana (stdout dosyasına) yazdırılmıştır. Burada mademki programcı time_t türünün aslında hangi tür olduğunu bilmemektedir. O halde 
    en kötü olasılıkla onu double türüne dönüştürerek yazdırdık. printf fonksiyonunda long double türünü yazdırmak için %Lf formak karakterleri kullanılmaktadır. 
    Normalde printf float, double ve long double türleri için noktadan sonra 6 basamak yazdırır. Ancak %.0f ya da %.0Lf ""noktadan sonra 0 basamak yazdır" anlamına gelmektedir.
    
    Aslında C standartlarında time fonksiyonun bir saniye sayısı vereceği de belirtilmemiştir. Ancak geleneksel bu fonksiyonlar hep 01/01/1970'ten geçen saniye 
    sayısını vermektedir. UNIX/Linux sistemlerinde bu durum garanti edilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t t;

    t = time(NULL);

    printf("%.0Lf\n", (long double)t);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                        54.Ders - 20/12/2022 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapı bildirilirken kapanış küme parantezinden sonra ';' konulmayıp bir değişken listesi yazılırsa hem yapı bildirilmiş olur hem de o yapı türünden 
    nesneler tanımlanmış olur. Örneğin:

    struct SAMPLE {
        int a;
        double b;
    } x, y;

    Bu işlem aşağıdakki ile eşdeğerdir:

    struct SAMPLE {
        int a;
        double b;
    };

    struct SAMPLE x, y;

    Örneğin:

    struct POINT {
        int x, y;
    } pt, *ppt;

    Bu işlem de aşağıdaki ile eşdeğerdir:

    struct POINT {
        int x, y;
    };

    struct POINT pt, *ppt;

    Tabii burada tanımlanan değişkenler yapı global olarak bildirildiyse (genellikle böyledir) global değişken, yapı yerel olarak bildirildiyse (çok seyrek durum)
    yerel değişkenlerdir. Burada tanımlanan değişkenlere ilkdeğer de verilebilir. Örneğin:

    struct POINT {
        int x, y;
    } pt = {10, 12}, *pt2;

    Bir yapı bildirilirken yapıya isim verilmek zorundadır. Örneğin:

    struct {				/* geçersiz */
        int x, y;
    };

    Bu bildirim geçersizdir. Çünkü bu yapının bir ismi olmadığına göre daha sonra bu yapıyı kullanmanın bir yolu yoktur. 
    Ancak C standartlarına göre eğer ';' den mnce birtakım nesne tanımalaması yapılmışsa yapıya isim verilmeyebilir. 
    Örneğin:

    struct {				/* geçerli */
        int day, month, year;
    } x, y;

    Bu bildirim geçerlidir. Çünkü x ve y program içerisinde kullanılabilir. Pekiyi bu durumda x ve y'nin türü nedir? 
    İşte C'de bu nesnelerin artık türünü bilmenin açık bir faydası yoktur. Dolayısıyla x ve y'nin derleyici tarafından 
    isimlendirilmiş bir yapı türünden olduğunu varsayabiliriz. Tabii burada x ve y aynı türden olduğu için birbirine 
    atanabilir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day, month, year;
} date = {10, 12, 2009}, *pdate = &date;

int main(void)
{
    struct POINT {
        int x, y;
    } pt = {10, 20};

    printf("%d/%d/%d\n", date.day, date.month, date.year);
    printf("%d/%d/%d\n", pdate->day, pdate->month, pdate->year);

    printf("%d, %d\n", pt.x, pt.y);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapının bir elemanı başka bir yapı türünden yapı nenesi olabilir. Örneğin:

    struct DATE {
        int day, month, year;
    };

    struct PERSON {
        char name[32];
        int no;
        struct DATE bdate;
    };

    Burada PERSON yapısının bdate (birth date) elemanı struct DATE türündendir. Yani bu eleman da aslında bileşik bir türdür. Örneğin:

    struct PERSON per;

    Burada per.name char türden bir adres belirtir. per.no ise int türdendir. per.bdate ifadesi struct DATE türündendir. O halde biz per yapı nesnesinin 
    içerisindek, bdate elemanın elemanlarına erişmek için birden fazla nokta operatörü kullanırız. Örneğin per.bdate.day ifadesi per nesnesinin 
    içerisindeki bdate nesnesinin day elemanını belirtmektedir:

    struct PERSON per;

    strcpy(per.name, "Sacit Mutlu");
    per.no = 123;
    per.bdate.day = 12;
    per.bdate.month = 7;
    per.bdate.year= 1978;
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

struct DATE {
    int day, month, year;
};

struct PERSON {
    char name[32];
    int no;
    struct DATE bdate;
};

int main(void)
{
    struct PERSON per;

    strcpy(per.name, "Sacit Mutlu");
    per.no = 123;
    per.bdate.day = 12;
    per.bdate.month = 7;
    per.bdate.year= 1978;

    printf("%s, %d, %d/%d/%d\n", per.name, per.no, per.bdate.day, per.bdate.month, per.bdate.year);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İç içe yapılarda yapı nesnesine ilkdeğer verilirken iç yapı nesnesi için ayrıca küme parantezi kullanmak zorunlu değildir. Ancak iç yapı nesnesi için
    ayrıca küme parantezinin kullanılması iyi bir tekniktir. Örneğin:

    struct DATE {
        int day, month, year;
    };

    struct PERSON {
        char name[32];
        int no;
        struct DATE bdate;
    };

    struct PERSON per = {"Sacit Mutlu", 123, 12, 7, 1978};		/* geçerli fakat kötü teknik */

    İç yapı nesnesinin ayrıca küme parantezlerine alınması iyi bir tekniktir:

    struct PERSON per = {"Sacit Mutlu", 123, {12, 7, 1978}};	/* iyi teknik */
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day, month, year;
};

struct PERSON {
    char name[32];
    int no;
    struct DATE bdate;
};

int main(void)
{
    struct PERSON per = {"Sacit Mutlu", 123, {12, 7, 1978}};

    printf("%s, %d, %d/%d/%d\n", per.name, per.no, per.bdate.day, per.bdate.month, per.bdate.year);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yine aşağıdaki gibi iç içe yapılar söz konusu olsun:

    struct DATE {
        int day, month, year;
    };

    struct PERSON {
        char name[32];
        int no;
        struct DATE bdate;
    };

    Elimizde de struct PERSON türünden bir gösterici olsun:

    struct PERSON per = {"Sacit Mutlu", 123, {12, 7, 1978}};
    struct PERSON *pper;

    pper = &per;

    pper göstericisi ile ok operatörünü kullanarak yapı elemanlarına erişebiliriz. Örneğin pper->name, pper->no gibi. Burada yine pper->bdate yapı nesnesi 
    içerisindeki yapı nesnesinin bütününü belirtmektedir. Bunun da elemanlarına erişilecekse pper->bdate.day, pper->bdate.month ve pper->bdate.year biçiminde 
    erişim yapılmalıdır. Şu ifadeye dikkat ediniz:

    pper->bdate.day

    Burada iki operatör vardır: Nokta ve ok operatörü. Bu iki operatör de soldan sağa eşit önceliklidir. Dolayısıyla önce ok operatörü sonra nokta operatörü 
    yapılır:

    İ1: pper->bdate 
    İ2: İ1.day
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day, month, year;
};

struct PERSON {
    char name[32];
    int no;
    struct DATE bdate;
};

void disp(struct PERSON *pper)
{
    printf("%s, %d, %d/%d/%d\n", pper->name, pper->no, pper->bdate.day, pper->bdate.month, pper->bdate.year);
}

int main(void)
{
    struct PERSON per = {"Sacit Mutlu", 123, {12, 7, 1978}};
    
    disp(&per);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi C99 ve sonrasında yapının belli elemanlarına "designated initializer" denilen sentaksla ilkdeğer verebiliyorduk. Bu sentaksta 
    önce bir '.' atomu sonra da yapı ismi geliyordu. İç içe yapılarda da designated initializer sentaksı benzerdir. Burada iç yapı nesnesinin 
    kendisine bütünsel olarak küme parantezleri ile ilkdeğer verilebileceği gibi iç yapının elemanlarına da birden fazla nokta
    kullanılarak ilkdeğer verilebilir. Örneğin:
    
    struct X {
        int a;
        int b;
        int c;
    };

    struct Y {
        int d;
        struct X e;
        int f;
        int g;
    };

    struct Y y = {.e = {10, 20, 30}, 100};

    Burada .e ifadesiyle biz Y yapısının e yapı elemanına ilkdeğer vermiş olduk. Dolayısıyla sonra 100 değeri yapının f
    elemanına yerleştirilecektir. Örneğin:

    struct Y y = {.e.b = 10, 20, 30};

    Burada .e.b ifadesiyle biz iç yapının b elemanına ilkdeğer vermiş olduk. Artık eleman ismi belirtmezsek yukarıdan 
    aşağıya doğry yapının sırasıyla elemanlarına ilkdeğer verilir. Yani örneğimizde 20 değeri e elemanının c parçasına, 
    30 değeri ise f elemanına yerleştirilecektir. 
    
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day, month, year;
};

struct PERSON {
    char name[32];
    int no;
    struct DATE bdate;
};

int main(void)
{
    struct PERSON per = {"Sacit Mutlu", .bdate.day = 12, .bdate.month = 7, .bdate.year = 1978, .no = 123};
        
    printf("%s, %d, %d/%d/%d\n", per.name, per.no, per.bdate.day, per.bdate.month, per.bdate.year);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İç içe yapı nesnelerinin diğer alternatif bir bildirimi de elemana ilişkin yapının elemana sahip yapının içinde bildirilmesidir. Örneğin:

    struct PERSON {
        char name[32];
        int no;
        struct DATE {
            int day, month, year;
        } bdate;
    };

    Burada PERSON yapısının içerisinde hem DATE yapısı bildirilmiştir hem de DATE yapısı türünden bdate elemanı bildirilmiştir. Bu durum tamamen aşağıdaki ile eşdeğerdir:

    struct DATE {
        int day, month, year;
    };

    struct PERSON {
        char name[32];
        int no;
        struct DATE bdate;
    };

    Yapı içinde yapı bildirirken içteki yapı yine bağımsız olarak dışarıda kullanılabilmektedir. Örneğin:

    struct PERSON {
        char name[32];
        int no;
        struct DATE {
            int day, month, year;
        } bdate;
    };

    struct PERSON per;		/* geçerli */
    struct DATE date;		/* geçerli */

    Burada her ne kadar DATE yapısı PERSON yağısının içerisinde bildirilmiş olsa da ayrı bir faaliyet alanına sahip değildir. Yani DATE yapısı sanki 
    dışarıda bildirilmiş gibi ele alınmaktadır. Aslında yapı içerisinde yapının bildirildiği ikinci alternatif oldukça seyrek kullanılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapı kendisi türünden bir elemana sahipo olamaz. Örneğin:

    struct SAMPLE {
        int a;
        int b;
        struct SAMPLE c;		/* geçersiz! */
    };

    Eğer böyle bir şey olsaydı bu durumda bu türden bir yapı nesnesi için ne kadar yer ayrılacağı bilinemezdi. Ancak bir yapı kendisi türünden bir gösterici 
    elemana sahip olabilir:

    struct SAMPLE {
        int a;
        int b;
        struct SAMPLE *c;		/* geçerli */
    };

    Çünkü göstericilerin türü ne olursa olsun onların kapladığı alan derleme sırasında bilinmektedir. Örneğin tipik olarak 32 bit sistemlerde göstericiler
    4 byte, 64 bit sistemlerde 8 byte yer kaplamaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yapının bir elemanın kendi türünden bir gösterici olması durumu "bağlı listeler (linked lists)" gibi veri yapılarının gerçekleştirilmesinde sıkça 
    kullanılmaktadır. Bağlı listeler C Programalam Dili tarafından doğrudan desteklenmeyen, programcının kendisinin oluşturması gereken bir veri yapısıdır. 
    Veri yapıları ve algoritmalar konusu "Sistem Programala ve İleri C Uygulamaları - 1" kursunda ele alınmaktadır. Ancak biz burada ayrıntıya girmeden 
    uygulama amaçlı bağlı listelerden biraz bahsedeceğiz. 

    Veri yapıları dünyasında elemanlar arasında öncelik-sonralık ilişkisi olan veri yapılarına genel olarak "liste (list)" denilmektedir. Önceki elemanının
    sonraki elemanı gösteren veri yapılarına ise "bağlı listeler (linked lists)" denilmektedir. Bağlı listelerin elemanlarına daha çok "düğüm (node)" denilmektedir. 
    Bağlı listelerde her düğüm sonraki düğümün yerini tutar. Programcı da ilk düğümün yerini tutmaktadır. Böylece programcı her elemanı erişebilmektedir. 
    Bağlı listelerin her düğümü genellikle (her zaman değil) heap'te malloc fonksiyonuyla tahsis edilmektedir. Böylece düğümler aslında herhangi bir yerde bulunabilir. 
    Ancak önceki eleman sonrki elemanı gösterdiği için bir organizasyon oluşturulmuş olur. Bağlı liste gerçekleştirimlerinde programcılar genellikle son elemanın da 
    yerini tutarlar. Böylece sona hızlı bir biçimde eleman ekleyebilirler. Bağlı listelerin dizilere göre avantajları ve dezavantajları şunlardır:

    - Eleman erişim dizilerde çk hızlı bir biçimde "rastgele" yapılabilmektedir. Yani p bir dizinin başlangıç adresi olmak üzere p[n] ile dizinin n'inci elemanına
    erişim çok hızlıdır. Bir dizinin 0'ıncı lemanına erişimle 1000'inci elemanına erişim arasında bir zaman farkı yoktur. Ancak bağlı listelerde bir elemana 
    erişebilmek için önceki eemanların üzerinden geçmek gerekir. 

    - Dizilerde araya eleman eklemek ve aradan eleman silmek dizi kaydırmasına yol açtığından yavaş bir işlemdir. Oysa bağlı listelerde bu işlem 
    çok hızlı yapılabilmektedir.

    - Dizilerin elemanları ardışıl olmak zorundadır. Oysa bazen ardışıl bellek yetersiz olabilir. Bu durumda dizi yerine bağlı listelerden faydalanılır. 

    - Dizilerin belli bir uzunlukları vardır. Her ne kadar diziler dinamik olarak yaratılıp realloc fonksiyonuyla genişletilebilseler de aslında bu işlemler
    oldukça yavaştır. Halbuki bağlı listelerde bir uzunluk kısıtı yoktur. Uzunluk heap alanı kadar olabilir. Dolayısıyla eleman sayısının bilinmediği 
    durumlarda bağlı listeler tercih edilebilmektedir. 

    - Diziler toplamda bağlı listelerden daha az yer kaplamaktadır. 

    Sonuç olarak eğer bir sistemde çok fazla insert, delete işlemi yapılıyor ancak elemana erişim fazla yapılmıyorsa bağlı listeler daha iyi bir seçenek oluşturabilmektedir. 
    Sınırı baştan bilinmeyen durumlarda da dizi yerine bağlı listeler tercih edilebilmektedir. 

    Bir bağlı listede önceki eleman sonraki elemanı gösterirken sonraki eleman da önceki elemanı gösteriyorsa bu tür bağlı listelere "çift bağlı listeler (doubly linkes lists)"
    denilmektedir. Eğer yalnızca önceki eleman sonraki elemanı gösteriyorsa bu tür bağlı listelere "tek bağlı listeler (single linked lists)" denir. 

    Düğümler bağlı listelerde yapılarla temsil edilir. Elemanlar da heap'te tahsis edilir. Tek bağlı listenin düğümü aşağıdaki gibi bir yapıyla temsil edilebilir:

    struct NODE {
        int val;		/* tutulan değer */
        struct NODE *next;
    };

    Çift bağlı listenin de bir düğümü şöyle bir yapıyla temsil edilebilir:

    struct NODE {
        int val;
        struct NODE *next;
        struct NODE *prev;
    };

    Genellikle bağlı listelerin ilk ve son düğümü ayrıca da içindeki eleman sayısı programcı tarafından tutulmaktadır. Yine genellikle bu bilgiler de bir 
    yapıyla temsil edilmektedir:

    struct LLIST {
        struct NODE *head;
        struct NODE *tail
        size_t count;
    };

    Aşağıda içerisinde int değerleri tutan tek bağlı liste veri yapısı oluşturulmuştur. Konunun ayrıntıları "Sistem Programalama ve İleri C Uygulamaları-1"
    kursunda ele alınmaktadır. Her ne kadar birden fazla kaynak dopsyayla çalışmayı ileride ele alacaksak da şimdiden buna yönelik de bir örnek 
    vermek istiyoruz. Aşağıdaki örnekte üç dosya bulunmaktadır. "llist.h" dosyası diğer iki dosyadan include edilmiştir. 
    Eğer Visual Studio IDE'sinde çalışıyorsanız bu örnekteki "llist.c" ve "sample.c" dosyaları proje eklenmiş olmalıdır. 
    UNIX/Linux ve macOS sistemlerinde çalışıyorsanız komut satırından derlemeyi şöyle yapabilirsiniz:

    $ gcc -o sample sample.c llist.c
----------------------------------------------------------------------------------------------------------------------*/

/* llist.h */

#ifndef LLIST_H_
#define LLIST_H_

#include <stddef.h>

/* Type Declarations */

struct NODE {
    int val;
    struct NODE *next;
    struct NODE *prev;
};

struct LLIST {
    struct NODE *head;
    struct NODE *tail;
    size_t count;
};

/* Function Prototypes */

struct LLIST *create_llist(void);
struct NODE *add_tail(struct LLIST *llist, int val);
struct NODE *add_head(struct LLIST *llist, int val);
void walk_llist(struct LLIST *llist);
void walk_llist_rev(struct LLIST *llist);
struct NODE *insert_next(struct LLIST *llist, struct NODE *node, int val);
struct NODE *insert_prev(struct LLIST *llist, struct NODE *node, int val);
void remove_node(struct LLIST *llist, struct NODE *node);
void clear_llist(struct LLIST *llist);
void destroy_llist(struct LLIST *llist);

#endif

/* llist.c */

#include <stdio.h>
#include <stdlib.h>
#include "llist.h"

struct LLIST *create_llist(void)
{
    struct LLIST *llist;

    if ((llist = (struct LLIST *)malloc(sizeof(struct LLIST))) == NULL)
        return NULL;
    llist->head = llist->tail = NULL;
    llist->count = 0;

    return llist;
}

struct NODE *add_tail(struct LLIST *llist, int val)
{
    struct NODE *new_node;

    if ((new_node = (struct NODE *)malloc(sizeof(struct NODE))) == NULL)
        return NULL;
    new_node->val = val;
    new_node->next = NULL;

    if (llist->head == NULL)		/* is list empty? */
        llist->head = new_node;
    else
        llist->tail->next = new_node;

    new_node->prev = llist->tail;
    llist->tail = new_node;
    ++llist->count;

    return new_node;
}

struct NODE *add_head(struct LLIST *llist, int val)
{
    struct NODE *new_node;

    if ((new_node = (struct NODE *)malloc(sizeof(struct NODE))) == NULL)
        return NULL;
    new_node->val = val;
    new_node->prev = NULL;

    if (llist->head == NULL)
        llist->tail = new_node;
    else
        llist->head->prev = new_node;

    new_node->next = llist->head;
    llist->head = new_node;
    ++llist->count;

    return new_node;
}

void walk_llist(struct LLIST *llist)
{
    struct NODE *node;
    
    for (node = llist->head; node != NULL; node = node->next) 
        printf("%d ", node->val);

    putchar('\n');
}

void walk_llist_rev(struct LLIST *llist)
{
    struct NODE *node;

    for (node = llist->tail; node != NULL; node = node->prev)
        printf("%d ", node->val);

    putchar('\n');
}

struct NODE *insert_next(struct LLIST *llist, struct NODE *node, int val)
{
    struct NODE *new_node;

    if ((new_node = (struct NODE *)malloc(sizeof(struct NODE))) == NULL)
        return NULL;
    new_node->val = val;

    new_node->prev = node;
    if (node->next != NULL)
        node->next->prev = new_node;
    else
        llist->tail = new_node;
    new_node->next = node->next;
    node->next = new_node;

    ++llist->count;

    return new_node;
}

struct NODE *insert_prev(struct LLIST *llist, struct NODE *node, int val)
{
    struct NODE *new_node;

    if ((new_node = (struct NODE *)malloc(sizeof(struct NODE))) == NULL)
        return NULL;
    new_node->val = val;

    if (node->prev != NULL)
        node->prev->next = new_node;
    else
        llist->head = new_node;
    new_node->prev = node->prev;
    new_node->next = node;
    node->prev = new_node;

    ++llist->count;

    return new_node;
}

void remove_node(struct LLIST *llist, struct NODE *node)
{
    if (node->prev == NULL)
        llist->head = node->next;
    else
        node->prev->next = node->next;
    if (node->next == NULL)
        llist->tail = node->prev;
    else
        node->next->prev = node->prev;

    --llist->count;
}

void clear_llist(struct LLIST *llist)
{
    struct NODE *node, *temp_node;

    node = llist->head;
    while (node != NULL) {
        temp_node = node->next;
        free(node);
        node = temp_node;
    }

    llist->head = llist->tail = NULL;
    llist->count = 0;
}

void destroy_llist(struct LLIST *llist)
{
    struct NODE *node, *temp_node;

    node = llist->head;
    while (node != NULL) {
        temp_node = node->next;
        free(node);
        node = temp_node;
    }

    free(llist);
}

/* sample.c */

#include <stdio.h>
#include <stdlib.h>
#include "llist.h"

int main(void)
{
    struct LLIST *llist;
    struct NODE *node;
    struct NODE *insert_pos;

    if ((llist = create_llist()) == NULL) {
        fprintf(stderr, "cannot create linked list!..\n");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < 100; ++i) {
        if ((node = add_tail(llist, i)) == NULL) {
            fprintf(stderr, "cannot add tail!..\n");
            exit(EXIT_FAILURE);
        }
        if (i == 0)
            insert_pos = node;
    }

    if (insert_prev(llist, insert_pos, 1000) == NULL) {
        fprintf(stderr, "cannot insert item!..\n");
        exit(EXIT_FAILURE);
    }

    walk_llist(llist);

    destroy_llist(llist);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                            55.Ders - 22/12/2022 Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapılar konusunda önemli bir kavram da "yapı elemanlarının hizalanması (structure member alignment)" denilen kavramdır. Bu konunun anlaşılması için 32
    bit ve 64 bit işlemcilerin bellek bağlantısı hakkında bazı ayrıntıların bilinmesi gerek,r. 32 bit işlemcilerin bellek bağlantılarında işlemci 
    bellekten bir hamlede 4 byte çekmektedir. Yani işlemci bellekten 2 byte bile okuyacak olsa önce 4 byte'lık bir bilgi çeker. Kendi içerisinde okunacak 2 byte'ı 
    elde eder. 32 bit işlemcilere bağlanan bellekler aslında dört byte dört byte yuvalanmaktadır:

    XXXX
    XXXX
    XXXX
    XXXX
    ....
    XXXX
    XXXX
    XXXX
    XXXX

    Böyle bir bellek organizasyonunda her satırın adres bakımından dördün katlarından başladığına dikkat ediniz. Böyle bir bellekte 4 byte 
    uzunluğunda bir nesneye (örneğin int bir nesneye) işlemci erişirken o dört byte uzunluktaki nesnenin dört byte'ın neresinden başladığı önemli olmaktadır.
    Örneğin:
    
    XXXX
    XXXX
    XXXX
    XXXX
    ....
    XXXX
    XXYY
    YYXX
    XXXX

    Burada işlemcinin YYYY ile belirtilen 4 byte'a erişmek istediğini düşünelim. Bu durumda işlemci iki hareketle bu 4 byte'ı elde etmektedir. Önce
    XXYY dört byte'ını çeker, sonra YYXX dört byte'ını çeker ve bunları içeride birleştir. Halbuki bu 4 byte dördün katlarında olsaydı erişim tek hamlede yapılabilirdi:
    
    XXXX
    XXXX
    XXXX
    XXXX
    ....
    XXXX
    YYYY
    XXXX
    XXXX

    Bu durumda 32 bit bir mikroişlemcide 4 byte'lık bilgiler eğer dördün katlarında olursa işlemci onlara daha hızlı erişmektedir. 
    32 bit işlemcilerde eğer erişilecek bilgi 1 byte ise onun dört byte'ın neresinde olduğunun bir önemi yoktur. 2 byte'lık bilgilerin de 
    (örneğin short nesneler) aynı dört byte içerisinde olması hızlı erişime yol açmaktadır. Örneğin aşağıdaki 2 byte'lık Y nesnesine 
    erişim de yine yavaş olacaktır:

    XXXX
    XXXX
    XXXX
    XXXX
    ....
    XXXY
    YXXX
    XXXX
    XXXX
        
    Aynı durum 64 bit mikroişlemciler için de geçerlidir. Bu işlemciler de bellekten tek hamlede 8 byte çekmek üzere tasarlanmışlardır:

    XXXXXXXX
    XXXXXXXX
    XXXXXXXX
    XXXXXXXX
    ........
    XXXXXXXX
    XXXXYYYY
    YYYYXXXX
    XXXXXXXX

    Bu işlemcilerde 8 byte'lık bilgiler (örneğin long long türünden ya da double türden nesneler) 8'in katlarında ise erişim daha hızlı olmaktadır.
    İşte 32 bit ve 64 bit derleyiciler bu durumu bildikleri için 4 byte'lık ve 8 byte'lık nesneleri ve yapı elemanlarını her zaman dördün ve sekizin 
    katlarında tutmaktadır. Programcı genellikle bunu fark etmemektedir. Örneğin:

    void foo(void)
    {
        int a;
        short b;
        int c;
        ...
    }

    Burada a ve c nesneleri aslında derleyici tarafından adres olarak dördün katlarında tutulmaktadır. Fakat programcılar ancak yapılar söz konusu olduğunda 
    derleyicilerin uyguladıkları bu hizalamayı fark edebilmektedir. Örneğin:

    #include <stdio.h>

    struct SAMPLE {
        char a;
        int b;
        char c;
        int d;
    };

    int main(void)
    {
        struct SAMPLE s;

        printf("%zd\n", sizeof s);			/* 16 */

        return 0;
    }

    Burada int türünün 4 byte olduğu yaygın sistemlerde programcı bu yapı türünden nesnenin kapladığı alanın 10 byte olması gerektiğini sanabilir. 
    Ancak bu yapı nesnesi 16 byte yer kaplayacaktır. Çünkü derleyici yapının int elemanlarını işlemci hızlı erişsin diye dördün katlarında tutacaktır. 
    Tüm nesneyi de yine dördün katlarına yerleştirecektir. Böylece derleyici hız kazancı sağlansın diye aşağıdaki gibi bir organizasyon yapacaktır:

    a---
    bbbb
    c---
    dddd

    Burada yapının b ve c elemanlarının dördün katında tutulması için a ve c'den sonra üçer byte boşluk bırakılmıştır. Tabii programcı yapı elemanlarını aşağıdaki gibi 
    organize etseydi bu yapı türünden nesneler daha az yer kaplardı:

    struct SAMPLE {
        char a;
        char b;
        int c;
        int d;
    };
    
    Bu yapı nesnesi artık 12 byte yer kaplacaktır. Çünkü 1 byte'lık nesnelerin dört byte'ın neresinde olduğunun hız bakımından bir önemi yoktur. 
    Derleyici yerleşimi şöyle yapacaktır:

    ab--
    cccc
    dddd

    #include <stdio.h>

    struct SAMPLE {
        char a;
        char b;
        int c;
        int d;
    };

    int main(void)
    {
        struct SAMPLE s;

        printf("%zd\n", sizeof s);		/* 12 */

        return 0;
    }

    Derleyiciler nesnenin sonunda da nesneyi dördün katlarına tamamlamak için boşluk bırakmaktadır. Örneğin:

    struct SAMPLE {
        int a;
        char b;
        int c;
        char d;
    };

    Burada derleyici yerleşemi şöyle yapacaktır:

    aaaa
    b---
    cccc
    d---

    32 bit derleyiciler nesnenin tamamını her zaman dördün katlarına tamamlamaktadır. 

    #include <stdio.h>

    struct SAMPLE {
        int a;
        char b;
        int c;
        char d;
    };

    int main(void)
    {
        struct SAMPLE s;

        printf("%zd\n", sizeof s);		/* 16 */

        return 0;
    }

    Derleyicilerin sonraki 4 byte'lık eleman dördün (ya da sekizin) katlarına gelsin diye yapı nesnesi içerisinde bıraktığı boşluklara "padding" denilmektedir. 
    Bu işlemin kendisine ise "hizalama (alignment)" denilmektedir. Hizalamada 2 byte'lık nesnelerin de 2'nin katlarında bulunması (yani aynı 4 byte ya da 8 byte'ıın
    içinde bulunması) uygun olur. Böylece en hızlı erişim için her nesnenin kendi uzunluunun katlarına yerleştirilmesi gerekir. Ynai örneğin 32 bit bir işlemcide
    int nesneler 4'ün katlarına, short nesneler 2'nin katlarına ve char nesneler 1'in katlarına yrleştirilirse en hızlı erişim sağlanır. 
    Burada önemli bir noktaya dikkatinizi çekmek istiyoruz: Hizalanmamış nesnelere erişim yine tek bir makine komutuyla yapılmaktadır. Örneğin:

    MOV EAX, [adres]

    Bu makine komutu Intel işlemcilerinde belirtilen adresten itibaren 4 byte'ı işlemcinin içerisindeki EAX isimli yaznaca çeker. Burada bellek adresi 4'e 
    hizalanmış olsa da olmasa da bu işlem tek bir makine komutuyla yapılmaktadır. Buradaki sorun bu makine komutunun hizalanmamış nesnelerde daha yavaş 
    hizalanmış nesnelerde daha hızlı çalışmasıdır. 

    Hizalama Intel işlemcilerinde "isteğe bağlı (optional)" bir durumdur. Yani örneğin 32 bit Intel işlemcileri hizalanmamış bir 4 byte'lık  
    bilgiye erişebilmektedir ancak nano saniye mertebesinde yavaş erişmektedir. Halbuki bazı işlemciler (örneğin ARM işlemcileri) hizalanmamış bilgilere
    hiç erişememektedir. İşlemcinine hizalanmamış bir bilgiye erişmesi istendiğinde işlemci bir kesme oluşturmakta ve işletim sistemi de prosesi sonlandırmaktadır. 

    Derleyiciler yapı nesnelerini hizalarken nesnenin bütününü hizalamay uygun biçimde yerleştirip nesnenin toplamının bu hizalamanın katı uzunluğunda olmasını
    sağlamaktadır. Tabii bu özellik standart bir özellik değildir. 

    Biz daha önce "yapı elemanlarının ilk yazılan eleman düşük adreste olacak biçimde ardışıl yerleştirildiğini" belirtmiştik. Hizalama bu durumu ihlal etmekte midir?
    Aslında hizalama derleyici tarafından zaten kontrollü biçimde yapıldığı için (yani derleyici hangi elemanların arasına padding byte'ları eklediğini bildiği için)
    ardışıllık bu anlamda bozulmamaktadır. (Yani siz padding byte'larında aslında yapıda kullanılmayan elemanların bulunduğunu varsayabilirsiniz.)

    C'de aynı türden iki yapı nesnesi birbirine atandığında elemanlar arasındaki padding byte'larının da biribirine atanacağının bir garantisi yoktur. 
    Yani örneğin programcı padding byte'larına bir şey yerleştirmişse (iyi bir teknik değildir) bu yapı nesnesini başka bir nesneye atadığında bu yereştirdiği 
    bilgiler hedef nesneye atanmayabilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bazen programcı hizalama üzerinde kontrol sağlamak isteyebilir. Fakat hizalama konusu C standartlarında her platformda 
    söz konusu olabilecek standart bir konu değildir. Bu nedenle hizalama üzerinde kontrol sağlamak derleyicinin eklenti 
    özellikleriyle (extensions) sağlanmaktadır. 
    
    Derleyiciler genel olarak beş farklı hizalama stratejisi izleyebilmektedir:

    1 Byte Hizalama (Byte Alignment)
    2 Byte Hizalama (Word Alignment)
    4 Byte Hizalama (Double Word Alignment)
    8 Byte Hizalama (Quad Word Alignemnet)
    16 Byte Hizalama (Double Quad Word Alignment)

    N byte hizalama şu anlama gelmektedir: "Nesnenin uzunluğu ve N değerinin hangisi küçükse nesne o değerin katlarına hizalanır". Nesnenin 
    tamamı da N'in katlarına hizalanmaktadır. Örneğin 4 byte hizalama söz konusu olsun. Bu durumda 1 byte'lık bir nesne (örneğin char bir 
    nesne) 1'in katlarına, 2 byte'lık bir nesne (örneğin short bir nesne) 2'nin katlarına, 4 byte'lık bir nesne (örneğin int bir nesne) 4'ün 
    katlarına ve 8 byte'lık bir nesne 4'ün katlarına yerleştirilir. Örneğin 8 byte (Quad Word alignment) söz konusu olsun. Bu durumda 1 byte'lık 
    nesne 1'in katlarına, 2 byte'lık bir nesne 2'nin katlarına, dört byte'lık bir nesne 4'ün katlarına, 8 byte'lık bir nesne 8'in katlarına hizalanır. 
    "1 byte hizalamanın aslında	hizalama yapmamakla" aynı anlama geldiğine dikkat ediniz. Dolayısıyla programcı hizalamayı kaldıracaksa derleyiciyi 
    1 byte hizalama ayaralayabilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Derleyicinin yaptığı default hizalamalar bazen programcının işine gelmeyebilir. Yani programcı çeşitli gerekçelerle derleyicinin hizalama yapmasını 
    istemeyebilir. Hizalamanın programcı tarafından kontrolü genellikle derleyicilerin sunduğu ek özelliklerle sağlanmaktadır. Microsoft 
    derleyicilerinde hizalama komut satırından derleme yapılırken /ZpN (buradaki N 1, 2, 4, 8, 16 olabilir) seçeneği ile ayarlanmaktadır. 
    Hizalama Visual Studio IDE'sinde proje seçeneklerinden "C-C++/Code Generation/Struct Member Alignment" combo box seçneğinden ayarlanabilmektedir. 
    Eğer bu ayarlamalar yapılmazsa (default durum) 32 bit derleme için default durum /Zp8 (yani quad word alignment), 64 bit derleme için /Zp16 biçimindedir. 
    
    gcc ve clang derleyicilerinde de default durumda Microsoft'taki gibi 32 bit derleyiciler için 8 byte hizalama, 64 bit derleyiciler için 16 
    byte hizalama kullanılmaktadır. Hizalamayı değiştirmek için -fpack-struct=N komut satırı seçeneği kullanılmalıdır. Örneğin

    gcc -fpack-struct=1 -o sample sample.c

    Hizalamayı değiştirmek için hem Microsoft hem gcc hem de clang derleyicilerinde #pragma pack(N) direktifi de kullanılabilmektedir. Bu direktif 
    komut satırında belirtilen hizalama seçeneğine göre daha yüksek önceliklidir. Örneğin:

    #include <stdio.h>

    #pragma pack(1)

    struct SAMPLE {
        char a;
        int b;
        char c;
        int d;
    };

    int main(void)
    {
        struct SAMPLE s;

        printf("%zd\n", sizeof s);		/* 10 */

        return 0;
    }

    #pragma pack direktifi sonraki #pragma pack direktifine kadar etkili olmaktadır. Böylece programcı isterse programın farklı yerlerinde farklı hizalamalar kullanabilir. 
    Örneğin:

    #include <stdio.h>

    #pragma pack(1)

    struct SAMPLE {
        char a;
        int b;
        char c;
        int d;
    };

    #pragma pack(8)

    struct MAMPLE {
        char a;
        int b;
        char c;
        int d;
    };

    int main(void)
    {
        struct SAMPLE s;
        struct MAMPLE m;

        printf("%zd\n", sizeof s);		/* 10 */
        printf("%zd\n", sizeof m);		/* 16 */

        return 0;
    }

    Bir #pragma pack uygulandıktan sonra default duruma geri dönmek için direktif parametresiz bir biçimde kullanılır. 
    Örneğin:

    #include <stdio.h>

    #pragma pack(1)

    struct SAMPLE {
        char a;
        int b;
        char c;
        int d;
    };

    #pragma pack()

    struct MAMPLE {
        char a;
        int b;
        char c;
        int d;
    };

    int main(void)
    {
        struct SAMPLE s;
        struct MAMPLE m;

        printf("%zd\n", sizeof s);		/* 10 */
        printf("%zd\n", sizeof m);		/* 16 */

        return 0;
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C11 ile birlikte bir nesnenin (bir yapının elemanı için de söz konusu olabilir) hizalaması _Alignas(N) belirleyicisi ile değiştirilebilmektedir. 
    _Alignas(N) belirleyicisi yapı ilgili nesnenin ya da yapı elemanının N'nin katlarına yerleştirileceği anlamına gelir. Örneğin:

    #include <stdio.h>

    struct SAMPLE {
         int a;
        _Alignas(8) int b;
    };

    int main(void)
    {
        struct SAMPLE s;

        printf("%zd\n", sizeof s);		/* 16 */

        return 0;
    }

    Burada yapının b elemanının önüne _Alignas(8) belirleyicisi getirilmiştir. Bu belirleyici aslında dördün katlarına yerleştirilecek int nesnesnin 8'in katlarına 
    yerleştirilmesini sağlamaktadır. Ancak C11 standartlarına göre _Alignas(N) belirleyicisi ile yüksek bir hizalama gereksinimi düşük bir hizalamaya çevrilemez. Örneğin:

    struct SAMPLE {
         int a;
        _Alignas(1) int b;		/* geçersiz! */
    };

    _Alignas ile belli nesnelerin ya da belli yapı elemanlarının hizalama gereksinimlerinin değiştirilebildiğine dikkat ediniz. 

    Genellikle derleyiciler tüm yapı nesnesini yapının en büyük elemanının hizalama gereksinimine ilişkin değerin katlarına tamamlamaktadır. Ancak
    bu özellik standart bir özellik değildir. Örneğin:

    struct SAMPLE {
        short a;
        int b;
        _Alignas(32) char c;
        int d;
    };

    Burada bu yapının sizeof değerinin 64 çıktığını görürseniz şaşırmayınız. Burada yapının c elemanı 32'in katlarına hizalanacaktır. Ancak tüm nesne 
    için 32'nin katı olacak biçimde yer ayrılacak ve yapı nesnesnin sonunda da buna uygun padding bulundurulacaktır. 

    Ayrıca C11 ile birlikte _Alignof(tür_ismi) isminde bir operatör de dile eklenmiştir. Bu operatör o anda o tür için derleyicinin uyguladığı hizalamayı
    bize vermektedir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        printf("%zu\n", _Alignof(int));     /* 4 */

        return 0;
    }
        
    _Alignas belirleyicisi bir tür ismiyle de kullanılabilmektedir. Örneğin:

    _Alignas(int) char c;

    _Alignas(tür_ismi) aslında _Alignas(_Alignof(tür_ismi)) anlamına gelmektedir. Yani bir _Alignas(int) dediğimizde int türünün hizalama gereksinimi neyse
    o sayıyı parantez içerisine yazmış gibi oluruz. 

    <stdalign.h> dosyası içerisinde alignas makrosu _Alignas biçiminde alignof mmakrosu ise _Alignof biçiminde define 
    edilmiştir. Yani bu başlık dosyası include edilirse biz _Alignas yerina alignas, _Alignof yerine alignof makrolarını
    kullanabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de her ifadenin bir türü vardır. Bu tür de sembolik bir biçimde gösterilebilmektedir.

    - Temel türlerin isimleri ilgili anahtar sözcüklerle oluşturulmaktadır. Örneğin int, unisgned long, double gibi.

    - Bir dizi türü C'de bir tür sonra köşeli parantez içerisinde bir sabit ifadesi ile temsil edilmektedir. Örneğin:

    int a[10];

    Burada a 10 elemanlı bir dizinin bütününü temsil etmektedir. Ancak anımsanacağı dizi isimleri bir ifadede kullanıldığında o dizinin başlangıç adresi
    anlamına gelmektedir. Başka bir deyişle dizi isimleri ifadede kullanıldığında derleyici tarafından oronatik olarak dizinin başlangıç adresine 
    dönüştürülmektedir. Yukarıdaki bildirimde a 10 elemanlı bir dizidir. a'nın türü sembolik olartak int[10] ile gösterilmektedir. Ancak a ifade içerisinde 
    kullanılırsa artık int * türünden bir değer olarak işeleme sokulur. O halde bir dizinin ismini belirtip türü sorulursa biz ona T[N] demeliyiz. Ancak 
    ifade içerisindeki türü sorukursa biz ona T * demeliyiz.

    - Bir adresin ya da göstericinin türü adresin tür bileşeni T olmak züere T * biçiminde gösterilir. Örneğin:

    int *pi;

    Burada pi, int * türündendir. Pekiyi aşağıdaki dizinin türü nedir?

    int *a[10];

    Burada a'nın türü, int *[10] biçiminde temsil edilmektedir. Örneğin:

    int a, *pi, *b[10];

    Burada a,'nın türü int, pi'nin türü int * ve b'nin türü int *[10] biçiminde belirtilmektedir. 

    - Daha önceden de belirttiğimiz gibi bir yapının türü "struct" anaktar sözcüğü ile "yapı isminin" birleşimindne oluşmaktadır. Örneğin:

    struct SAMPLE {
        int a;
        int b;
    };

    struct SAMPLE s;

    Burada oluşturduğumuz tür SAMPLE biçiminde isimlendirilmez, "struct SAMPLE" biçiminde isimlendirilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir türe tamamen onun yerini tutabilecek alternatif isim verilmesine "typedef bildirimi" denilmektedir. typedef anahtar sözcüğü C'nin sentaksında
    "yer belirleyicisi (storage class specifier)" grubunda bulunmaktadır. (Bu konu ileride ele alınacaktır.) Her bildirimin önüne typedef getirilebilir. Örneğin:

    int A;

    Bu geçerli bir bildirimdir. Biz bunun önüne typedef belirleyicisini getirebiliriz:

    typedef int A;

    Örneğin:

    int A, *PA;

    Bu geçerli bir bildirimdir. Biz bunun önüne typedef belirleyicisni getirebilriz. typedef belireyicisi "bir bildirimdeki değişken ismini o değişkenin türünü 
    belirten tür ismi haline" getirmektedir. Örneğin:

    int I;

    Burada I int türden bir değişkendir. Şimdi typedef getirelim:

    typedef int I;

    Artık I int türü ile tamamen aynı anlama gelen bir tür ismidir. Yani:
    
    int a;

    ile 

    I a;

    aynı anlamdadır.
    
    Örneğin:

    int *PI;

    Burada PI int * türündendir. Başına typedf belirleyicisi getirelim:

    typedef int *PI;

    Şimdi artık PI ismi int * türünü temsil etmektedir. Yani örneğin:

    int *pi;

    ile 

    PI pi;

    aynı anlamdadır. Örneğin:

    #include <stdio.h>

    typedef char *STR;

    int main(void)
    {
        STR s = "ankara";		/* char *s = "ankara"; */

        printf("%s\n", s);		/* ankara */

        return 0;
    }

    Örneğin:

    int I, *PI;

    Burada I int türdendir, PI ise int * türündendir. Şimdi bildirimin başına typedef belirleyicisini getirelim:

    typedef int I, *PI;

    Burada artık I int türünü temsil eden, PI ise int * türünü temsil eden tür isimleri haline getirilmiştir. Örneğin:

    #include <stdio.h>

    typedef int I, *PI;

    int main(void)
    {
        I a = 10;			/* int a = 10; */
        PI pi;				/* int *pi; */

        pi = &a;

        printf("%d\n", *pi);
    
        return 0;
    }

    typedef bir tanımala oluşturmaz, bir bildirimn işlemidir. Yani derleyici typedef bildirimini gördüğünde yalnızca bilgi edinmektedir. Bir yer ayırmamaktadır. 
    Örneğin:

    int A[5];

    Burada A 5 elemanlı int türden bir dizidir. Sembolik olarak A'nın türü C'de int[5] ile gösterilmektedir. Şimdi bu bildirimin başına typedef belirleyicisini
    getirelim:

    typedef int A[5];

    Artık burada A ismi 5 elemanlı int bir dizi türünü temsil etmektedir. Yani artık A int[5] türü ile aynı anlamdadır. Örneğin:
    
    int a[5];

    ile,

    A a;

    aynı anlamdadır. 

    Yukarıda da belirtildiği gibi typedef anahtar sözcüğü C standartlarında "yer belirleyicisi (storage class specifier)" grubundadır. Bildirimde (bu konu ileride 
    ele alınacak) yer belirleyicileri ile tür belirleyicileri yer değiştirebildiği için aslında typedef anahtar sözcüğünün başta bulunması zorunlu değildir. Yani örneğin:

    typedef int I;

    Bu typedef bildirimi şöyle de yapılabilirdi:

    int typedef I;

    Tabii genel alışkanlık typedef belirleyicisni başa getirmektedir. 

----------------------------------------------------------------------------------------------------------------------*/
                                        
/*----------------------------------------------------------------------------------------------------------------------
                                                    56. Ders 27/12/2022 - Sali
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aslında typedef belirleiyicisi prototipler için de kullanılabilmektedir. Örneğin:

    void F(void);

    Burada F, geri dönüş değeri void parametresi void olan bir fonksiyon türündendir. Başına typedef getirelim:

    typedef void F(void);

    Artık burada F geri dönüş değeri void parametresi void olan bir fonksiyon türünü temsil etmektedir. Örneğin:

    F f;

    ile,

    void f(void);

    aynı anlamddır. Ancak bu biçimde prototip bildirilebilir ancak fonksiyon tanımlanamaz. Örneğin:

    f()		/* geçerli değil */
    {
        /* ... */
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    typedef bildirimi global düzeyde ya da yerel düzeyde yapılabilmektedir. Eğer typedef bildirimi global düzeyde yapılırsa typedef ismi bir tür ismi olarak 
    tüm fonksiyonlarda kullanılabilir. Örneğin:

    #include <stdio.h>

    typedef char *STR;

    int main(void)
    {
        STR s = "ankara";		/* geçerli, STR ismi buarada kullanılabilir */

        puts(s);
        
        return 0;
    }

    void foo()
    {
        STR k = "izmir";		/* geçerli, STR ismi burada da kullanılabilir */

        puts(k);
    }

    Eğer typedef bildirimi yerel düzeyde yapılırsa typedef ismi yalnızca o blokta kullanılabilir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        typedef char *STR;
    
        STR s = "ankara";		/* geçerli, STR ismi burada kullanılabilir */

        puts(s);
        
        return 0;
    }

    void foo()
    {
        STR k = "izmir";		/* geçersiz! STR ismi burada kullanılamaz */

        puts(k);
    }

    C programcıları hemen her zaman typedef bildirimini global düzeyde yani programın tepesinde ya da bir başlık dosyasının içerisinde yapmaktadır: 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapılarla birlikte typedef çok sık kullanılmaktadır. Bu sayede programcı "struct" anahtar sözcüğünü kullanmak zorunda kalmaz. Örneğin:

    struct SAMPLE {
        int a;
        double b;
    };

    struct SAMPLE SMP;

    Burada SMP "struct SAMPLE" türündendir. Bu bildirimin başına typedef getirelim:

    typedef struct SAMPLE SMP;

    Artık SMP "struct SAMPLE" türünü temsil eden tür ismi olmuştur. Yani:

    struct SAMPLE s;

    ile,

    SMP s;

    tamamen eşdeğerdir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day, month, year;
};

typedef struct DATE DT;

int main(void)
{
    DT d = {10, 12, 1990};

    printf("%d/%d/%d\n", d.day, d.month, d.year);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi yapı bildirimi ';' ile kapatılmadan bir değişken listesi de yazılırsa aynı zamanda tanımlama da yapılmış olmaktadır. Örneğin:

    struct SAMPLE {
        int a;
        double b;
    } SMP;

    Burada SMP "struct SAMPLE" türünden bir değişkendir. O halde bunun da başına typedef belirleyicisini getirebiliriz:

    typedef struct SAMPLE {
        int a;
        double b;
    } SMP;

    Burada SMP artık "struct SAMPLE" türünü temsil etmektedir. Yani:

    struct SMAPLE s;

    ile,

    SMP s;

    aynı anlamdadır. Gerçekten de yapı bildirimi yaparken aynı zamanda typedef belirleyicisi ile tür ismi de oluşturmak çok sık kullanılan bir 
    kalıptır. Programcılar genel olarak yapı ismiyle tyoedef ismini farklı oluştururlar. Örneğin Microsoft kendi kodlarında yapı isimlerinin başında "tag"
    öneki getirir. typedef isimlerinin başına bu öneki getirmez:

    typedef struct tagMSG {
        ...
    } MSG;

    Aslında daha önce de gördüğümüz gibi yapı ismiyle aynı isimli bir değişken bildirilebilir. Çünkü yapı isimleri tek başlarına değil "struct" anahtar sözcüğü 
    ile birlikte kullanılmaktadır. Örneğin:

    struct test {
        ...
    };

    int test;		/* geçerli */

    test = 10;		/* geçerli, int olan test anlaşılır */

    struct test t;	/* geçerli, tür ismi kullanılmış */

    Buradan hareketle aşağıdaki gibi bir durum da söz konusu olabilir:

    struct SAMPLE {
        int a;
        double b;
    };

    typedef struct SAMPLE SAMPLE;		/* geçerli */

    Ynai typedef ismi ile yapı ismi de aynı olabilir. Bu durumda aşağıdaki iki bildirim de geçerlidir:

    struct SAMPLE s;		/* geçerli */
    SAMPLE k;				/* geçerli */

    Tabii böylesi bir durumda artık tür ismini ""struct SAMPLE" biçiminde kullanmanın da bir anlamı kalmaz. O halde aşağıdaki 
    bildirim de geçerlidir:

    typedef struct SAMPLE {
        int a;
        double b;
    } SAMPLE;

    Benzer biçimde aşağıdaki iki bildirim de geçerlidir:

    struct SAMPLE s;
    SAMPLE k;

    Tabii programcılar genellikle yapı ismi ile typedef ismini biribirinden ayırmaktadır. Örneğin:

    typedef struct SAMPLE {
        int a;
        double b;
    } SMP;

    Anımsanacağı gibi eğer yapı bildirimi saırasında aynı zamanda o yapı türünden değişkenler bildiriliyorsa yapıya isim vermek de zorunlu değildir. 
    Örneğin:

    struct {
        int a;
        double b;
    } s, k;

    Bu bildirim geçerlidir. Burada s ve k aynı türdendir. Ama tür ismini programcı vermemiştir. O halde bundan sonra da artık bu türden yeni bir değişken tanımlayamayız. 
    Bu tür bildirimlerde tür isminin derleyici tarafından benzersiz (unique) biçimde oluşturulduğunu düşünebilirsiniz. Ancak C'de eğer yapı türünden değişken bildirilmiyorsa
    yapıya isim verilmesi zorunludur. Aşağıdaki bildirim geçerli değildir:

    struct {
        int a;
        double b;
    };

    Zaten böyle bir bildirim bir anlam ifade etmektedir. O halde typedef işlemi yapılırken yapıya isim verilmesi de zorunlu değildir. Örneğin:

    struct {
        int a;
        double b;
    } SAMPLE;

    Bu bildirim geçerlidir. Burada SAMPLE ilgili yapı türündendir. Şimdi bildirimin başına typedef getirelim:

    typedef struct {
        int a;
        double b;
    } SAMPLE;

    Burada yapının bir ismş yoktur. Ancak türün ismi vardır. Biz bu yapı türünden değişkenler bildirebiliriz:

    SAMPLE s, k;  /* geçerli */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapı türünden göstericiler sık kullanılmaktadır. Çünkü yapılar fonksiyonlara birincil yöntem olarak adres yoluyla aktarılırlar. Bu nedenle
    yapı türünden adres türlerinin de typef edilmesi ile sık karşılaşılmaktadır. Örneğin:

    typedef struct tagDATE {
        int day, month, year;
    } DATE;

    DATE *PDATE;

    Burada PDATE "struct tagDATE *" türünden bir değişkendir. Başına typedef getirelim:

    typedef DATE *PDATE;

    Artık PDATE "struct tagDATE *" türünü temsil etmektedir. Örneğin:

    void disp_date (PDATE pdate)
    {
        /* ... */
    }

    Bu fonksiyon aşağıdakiyle eşdeğerdir:

    void disp_date (struct tagDATE *pdate)
    {
        /* ... */
    }

    Bazı programcılar yapı için typedef bildirmi yaparken aynı zamanda bu yapı türünden adres için de typedef bildirimi yaparlar. Örneğin:

    struct tagDATE {
        int day, month, year;
    } DATE, *PDATE;

    Burada DATE "struct tagDATE" tründendir. Benzer biçimde PDATE de "struct tagDATE *" türündendir. O halde bildirimin başına typedf getirelim:

    typedef struct tagDATE {
        int day, month, year;
    } DATE, *PDATE;

    Artık burada DATE "struct tagDATE" türünü, PDATE ise "struct tagDATE *" türünü temsil etmektedir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi typedef bildiriminin faydası nedir? Bunları şöyle özetleyebiliriz:

    1) typedef bildirmi tür isimlerini kısaltmak amacıyla kullanılabilmektedir. Örneğin:

    char *names[10];

    yerine:

    typedef char ASTR[10];

    ASTR names;

    gibi. Benzer biçimde typedef bildirimi yapılar söz konusu olduğunda struct anahtar sözcüğünü elimine edebilmektedir. Örneğin:

    typedef struct tagDATE {
        int day, month, year;
    } DATE;

    DATE s;	

    Burada struct tagDATE yerine yalnızca DATE diyebilmekteyiz. Henüz görmemiş olsak da typedef bildirimi fonksiyon göstericileri söz konusu olduğunda 
    önemli yazım kolaylığı sağlamaktadır. 

    2) typedef bildirimi taşınabilirliği (portability) sağlamak için de kullanılmaktadır. Örneğin biz bir kütüphane yazmış olalım. Orada bir handle türü bazı sistemlerde 
    int, bazı sistemlerde long olsun. Programcının bundan etkilenmemesini sağlamak istiyorsak programcının doğrudan tür ismi yerine typedef ismini kullanmasını 
    teşvik ederiz. Örneğin:
    
    #include "xlib.h"
    ...
    HANDLE h;

    gibi. Eğer o sistemde HANDLE int ise ve programcı bunu int olarak kullanırsa kodunu HANDLE türünün long olduğu başka sisteme götürdüğünde 
    sorun oluşur. Örneğin kütüphanede şöyle bir fonksiyon olsun:

    HANDLE create_handle(void);

    Biz de fonksiyonu şöyle çağıralım:

    HANDLE h;

    h = create_handle();

    İlgili sistemde HANDLE türünün int olarak typedef edildiğini biliyor olalım. O zaman bu kodun ilgili sistemde aşağıdakinden bir farkı kalmaz:

    int h;

    h = create_handle();

    Ancak bu kodu bu biçimde HANDLE türünün long olduğu bir sisteme götürürsek sorun oluşacaktır. İşet biz hep HANDLE türünü kullanmalıyız:

    #include "xlib.h"

    HANDLE h;

    h = create_handle();

    Bu sayede biz kodumuzun iki sistemde de geçerli olmasını sağlamış olmaktayız. Çünkü kütüphaneyi yazanlar zaten bu "xlib.h" içerisini değiştirip 
    HANDLE türünü o sisteme uygun biçimde typedef etmiş olacaklarıdır. 

    3) typedef bildirimi okunabilirliği artırmak için de kullanılabilmektedir. Örneğin türlere onların kullanım amaçlarına uygun isimler verilebilir. 
    Bu isimler de kodu daha anlamlı hale getirebilir. Örneğin:

    typedef void *HBITMAP;
    typedef void *HBRUSH;
    typedef void *HPEN;

    Burada HBITMAP, HBRUSH ve HPEN aynı türdendir. O zaman neden bunlara farklı tür isimleri verilmiştir. İşte okunabilirliği artırmak için:

    HBITMAP a;
    HBRUSH b;
    HPEN c;
    
    Burada a, b, ve c değişkenlerinin hangi niyetle oluşturuldukları tür isimlerinden anlaşılmaktadır. Aslında bu bildirimlerin hepsi aynı türdendir:

    void *a;
    void *b;
    void *c;
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    typedef isimleri ile orijinal tür isimleri tamamen aynı türleri belirtmektedir. typedef bildirimi yalnızca türe alternatif isim vermek için kullanılmaktadır. 
    Örneğin:

    struct SAMPLE {
        int a;
        double b;
    };

    typedef struct SAMPLE SMP;

    struct SAMPLE s;
    SMP k;

    Burada s ile k aynı türdendir. SMP tür ismi tamamen struct SAMPLE anlamına gelmektedir. Örneğin:

    typedef int I;
    ...
    I a;
    double b = 2.3;

    a = (I)b;

    typedef isimleri orijinal tür isimleri nerede kullanılırsa orada kullanılabilir. Örneğin:

    typedef int I, *PI;

    PI pi;

    pi = (PI)malloc(sizeof(I) * 10);

    Burada I "int" türünü, PI ise "int *" türünü temsil etmektedir. Tabii PI ile "int *" eşdeğer olduğuna göre "I *" da eşdeğerdir. 

    C'de aynı typedef ikinci kez yapılırsa bu durum geçersiz kabul edilmektedir. Ancak pek çok C derleyicisi bu durumu geçerli kabul edebilmektedir. 
    Örneğin:

    typedef int I;
    typedef int I;

    Bu durum C'de geçersizdir. Ancak C++'ta geçerli kabul edilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C standartlarında sistemden sisteme değişebilecek bazı türler taşınabilirliği sağlamak için typedef edilmiştir. C'nin bu typedef tür isimlerinin
    sonuna _t getirilmiş durumdadır. Bu biçimdeki sonu _t ile biten isimleri çeşitli başlık dosyalarında typedef edilmiş durumdadır. 
    Bunlar anahtar sözcük olmadığı için eğer bu tür isimlerini kullandığımızda bu türlerin typedef edilmiş olduğu başlık dosyalarını include 
    etmezsek derleme işleminde hata oluşur. 

    C'nin typedef isimlerinin önemli bir bölümü minimal bir başlık dosyası olarak <stddef.h> içerisinde bildirilmiştir. Ancak bu typedef isimlerinin
    bazıları başka başlık dosyalarında da include edilmiş durumdadır. Biz daha önce size_t türünü kullanmıştık. Burada bu standart typedef türleri hakkında
    bilgiler vereceğiz. 

    size_t: Bu tür ilgili sistemdeki "teorik bellek büyüklüğü" ile ilişkilendirilmiştir. Dolayısıyla bellek büyüklüğü ile doğrudan ya da dolaylı bir ilgisi bulunan 
    bağlamlarda programcılar bu türü kullanmayı tercih ederler. Örneğin bir dizinin uzunluğu, bir dizi indeksi bu türle ifade edilir. Standratlara göre size_t
    işaretsiz bir tamsayı türü olmak koşuluyla derleyicileri yazanlar tarafından onların isteği doğrultusunda herhangi bir tür olarak typedef edilebilir. 
    Bu nedenle bazı sistemlerde örneğin size_t "unsigned int" olarak typedef edilmişken bazı sistemlerde ""unsigned long int" olarak typedef edilmiş olabilir. 
    Standartlarda size_t şu başlık dosyalarında typedef edilmiş olmak zorundadır:

    <stddef.h>
    <stdio.h>
    <stdlib.h>
    <string.h>
    <time.h>
    <uchar.h>	(C11 ile birlikte)
    <wchar.h>

    Ayrıca anımsayacağınız gibi C standartlarında bazı standart C fonksiyonlarının parametrik yapılarında size_t kullanılmıştır. Örneğin:

    size_t strlen(const char *str);
    void *malloc(size_t size);
    void *memcpy(void *dest, const void *source, size_t n);
    ...

    Bu durumda örneğin strlen fonksiyonunun geri dönüş değerinin türü "o sistemde size_t hangi tür olarak typedef edilmişse o türdendir". Tabii bizim de 
    taşınabilirlik bakımından strlen fonksiyonunun geri dönüş değerini size_t türünden bir nesneye atamamız uygun olur. Örneğin:

    char s[] = "ankara";
    size_t len;
    ...

    len = strlen(s);

    printf fonksiyonunda size_t türü %z (yanında d, u, ve x de gelebilir) format karakteriyle yazdırılmaktadır. Bu özellik C99 ile birlikte 
    C'ye eklenmiştir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        char s[] = "ankara";
        size_t len;

        len = strlen(s);
        printf("%zu\n", len);

        return 0;
    }

    ptrdiff_t: C'de aynı türden olmak koşuluyla iki adres birbirinden çıkartılabilir. Ancak toplanamaz. Bu durumda adresin sayısal bileşenleri birbirinden çıkarrılır. 
    Elde edilen değer adresin türünün uzunluğuna bölünür. Yani işlem soncunda her zaman aradaki eleman sayısı elde edilir. Örneğin:

    T a[10];

    Burada &a[5] - &a[0] işleminden T türü ne olursa olsun her zaman 5 değeri elde edilecektir. Tabii bu işlem ters de yapılabilir: &a[0] - &a[5] buradan ise -5 
    elde edilir. Aynı türden iki adres birbirinden çıkartıldığında elde edilen değer sistemde teorik bellek büyüklüğü ile ilgilidir. İşte bu tür ptrdiff_t 
    ile temsil edilmiştir. Biz C'de aynı türden iki adresi çıkartırsak sonuç o sistemde derleyicileri yazanlar ptrdiff_t türünü nasıl typedef etmişlerse o türden
    elde edilecektir. Standartlarda ptrdiff_t türünün derleyicileri yazanlar tarafından işaretli olmak koşuluyla herhangi bir tamsayı türü olarak typedef edilebleceği 
    belirtilmiştir. Bu tür yalnızca <stddef.h> içerisinde typedef edilmiştir. C'de aynı türden iki adresin farkı bir nesnede saklanacaksa onun ptrdiff_t türünden 
    olması en uygun durumdur. Örneğin:

    #include <stddef.h>

    ...
    int a[10];
    ptrdiff_t diff;
    ...
    diff = &a[5] - &a[0];

    printf fonksiyonunda ptrdiff_t türünü yazdırmak için %t (yanına d, u, x gelebilir ) format karakteri kullanılmaktadır. Bu format karakteri de C99 ile
    C'ye eklenmiştir. Örneğin:

    #include <stdio.h>
    #include <stddef.h>

    int main(void)
    {
        int a[10];
        ptrdiff_t diff;

        diff = &a[5] - &a[0];

        printf("%td\n", diff);

        return 0;
    }

    time_t: Bu tür time fonksiyonun verdiği belli bir orijinden geçen zamanı ifade etmek için kullanılmaktadır. Sistemlerin hemen hepsinde orijin (epoch)
    01/01/1970 00:00 kabul edilmektedir. time fonksiyonu da bu tarihten geçen saniye sayısını tamsayı olarak vermektedir. Ancak standratlarda bu türün tamsayı ya da 
    gerçek sayı türlerinden olabileceği belirtilmiştir. (Başka bir deyişle geçen bu zaman saniye cinsinden değil örneğin mili saniye cinsinden de olabilir ya da 
    saniye cinsinden ancak noktalı da olabilir.) Derleyicilerin büyük çoğunluğunda bu tür tamsayı türü olarak typedef edilmiştir.

    wchar_t: Daha önceden de belirtildiği gibi C standartlarında hangi karakter tablosunu ve encoding'i temsil ettiği belirlenmemişse de "geniş karakter (wide character)"
    diye de bir karakter türü vardır. (Geniş karakter sabitleri L'x' biçiminde, geniş karakter string'leri L"text" biçimde belirtilmektedir.) İşte geniş karakterler 
    wchar_t türündendir. wchar_t tür ismi Microsoft derleyicilerinde unsigned short int, gcc ve clang derleyicilerinde unsigned int biçiminde typedef edilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                            57. Ders 29/12/2022 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte ilgili sistemde N bit uzunluğunda tamsayı türlerini temsil etmek için <stdint.h> içerisinde yeni typedef isimleri dile eklenmiştir. 
    Örneğin 32 bitlik bir nesneye ihtiyacımız olsun. Bu 32 bitlik nesne DOS sisteminde long türüyle, Linux ve Windows sistemlerinde int türüyle temsil edilmektedir. 
    İşte kodun taşınabilirliğini sağlamak için bu N bit uzunlukta tamsayı türleri <stdint.h> içerisinde derleyicileri yazanlar tarafından typedef edilmiştir. Bu türler 
    aşağıda belirtilmiştir:

    - N (8, 16, 32, 64 olmak üzere) intN_t isimleri tam olarak N bit uzuluğunda işaretli tamsayı türlerini uintN_t isimleri de tam olarak N bit uzunluğunda
    işaretsiz tamsayı türlerini belirtir. Bu türler şunlardır:

    int8_t
    uint8_t
    int16_t
    uint16_t
    int32_t
    uint32_t
    int64_t
    uint64_t

    - intmax_t ve uintmax_t ilgili derleyicide bulunan en büyük işaretli ve işaretsiz tamsayı türleri olarak typedef edilmiş olmalıdır. Her ne kadar C99 ile birlikte
    en büyük işaretli ve işaretsiz tamsayı türleri long long ve unsigned long long ise de C standratlarında derleyicilerin daha büyük tamsayı türleri 
    bulundurabileceği (bir eklenti olarak) belirtilmiştir. printf fonksiyonunda intmax_t ve uintmax_t %j (yanına d, u, o ve x getirilebilir) format karakteriyle yazdırılabilmektedir. 
    Bu format karakteri de C99 ile birlikte C'ye eklenmiştir. Örneğin:

    #include <stdio.h>
    #include <stdint.h>

    int main(void)
    {
        intmax_t a = 12345678;

        printf("%jd\n", a);

        return 0;
    }	

    - N sembolü 8, 16, 32 ve 64 sayılarından biri olmak üzere int_leastN_t ve uint_leastN_t türleri N bite sahip en küçük türler olarak typedef edilmiştir. 
    Bu türler şunlardır:

    int_least8_t
    uint_least8_t
    int_least16_t
    uint_least16_t
    int_least32_t
    uint_least32_t
    int_least64_t
    uint_least_64_t

    Bu least türleri şu anlama gelmektedir: Bir least türü en azından N bit uzunluğundadır. Ancak ondan daha kısa bir int türü yoktur. Örneğin int_least16_t 
    türü 32 bit bir sistemde short olabilir ancak int olamaz. Örneğin int_least32_t türü 32 biti içeren en düşük tür olmak zorundadır. Fakat örneğin belli bir sistemde 
    short türü de int türü de 32 bit olsun. Bu sistemde int_least32_t türü short da olabilir int de olabilir. Çünkü her ikisi de 32 bittir ve o sistemde 16 biti kapsayan 
    32 bittten daha küçük bir tamsayı türü yoktur. Yani int_leastN_t ya da uint_leastN_t türleri N biri içeren en küçük tamsayı türleridir. Örneğin 
    int32_t türü ile int_least32_t türü arasında şöyle bir farklılık vardır: int32_t türü tam olarak 32 bittir. Ancak int_least32_t türü 32 bitten büyük olabilir, 
    ancak küçük olamaz. 32 bitten büyükse 32 bitlik tamsayı türünün olmaması gerekir. Tabii pek çok sistemde intN_t ile int_leastN_t türleri zaten aynıdır.

    - N 8, 16, 32 ve 64 sayılarını belirtmek üzere int_fastN_t ve uint_fastN_t türleri N biti kapsayan en hızlı tamsayı türleri olarak typedef edilir. 
    Bazı sistemlerde bazı uzunluktaki bilgiler üzerinde daha hızlı işlemler yapılabilmektedir. Örneğin bizim 16 bitlik bir işleme ihtiyacımız olsun. 
    Falanca sistemde iki byte short türü, 4 byte'lık int türünden daha yavaş işleme sokuluyor olabilir. Bu durumda programcı zaten 4 byte 2 byte'ı 
    kapsadığı için bu sistemde hız bakımından int türünü kullanmak isteyebilir. İşte bu fast türleri en az N bit uzunluğunda olan en hızlı işleme 
    sokulma potansiyelinde olan türleri temsil etmektedir. Intel, ARM gibi işlemcilerde düşük uzunluklu tamsayı türleriyle yüksek uzunluklu tamsayı türleri 
    işlemcinin yazmaç uzunluklarını geçmemek koşuluyla aynı zamanda işleme sokulmaktadır. Ancak çeşitli mimarilerde bu durum değişebilir. fast türleri 
    şunlardır:

    int_fast8_t
    uint_fast8_t
    int_fast16_t
    uint_fast16_t
    int_fast32_t
    uint_fast32_t
    int_fast64_t
    uint_fast_64_t

    - intptr_t ve uintptr_t türleri ilgili sistemde bir göstericinin sayısal bileşenini saklayabilecek uzunlukta tamsayı türleri olark typedef edilmektedir. 
    Anımsanacağı gibi genel olarak 32 bit sistemlerde göstericiler 32 bit, 64 bit sistemlerde ise 64'tir. Örneğin:

    char *ptr;
    ...

    Burada ptr göstericisinin içeisinde bir adres bilgisi olsun. Bu adres bilgisi üzerinde bit düzeyinde bir işlem yapmak 
    isteyelim. C'de adres türleri bir operatörleriyle işleme sokulamamaktadır. Bu durumda bizim önce bu adres bilgisini 
    onu kapsayacak uzunlukta bir tamsayı türünden nesneye aktarmamız, o nesneyle bit işlemini yapıp onu yeniden adres 
    türüne dönüştürmemzi gerekir:

    char *ptr;
    uintptr_t val;
    ...

    val = (uintptr_t)ptr;
    /* bit işlemleri yapılıyor */
    ptr = (char *)val;

    Tabii uintptr_t türü ve intptr_t türü o sistemdeki adres bilgisini ifade edebilecek tamsayı türünden daha büyük türler 
    biçiminde de typedef edilmiş olabilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi C99'a kadar C'de bool biçiminde bir tür yoktu. C99 ile birlikte C'ye _Bool isminde bir bool türü eklenmiştir. Ancak bu isim biraz
    tuhaf harflendirildiğinden dolayı <stdbool.h> içerisinde aşağıdaki makrolar da oluşturulmuştur:

    #define bool	_Bool
    #define false	0
    #define true	1

    bool ismi C'de bir typedef ismi değildir. (Zaten typedef ismi olsaydı bool_t biçiminde isimlendirilirdi.) Biz bool yazdığımız zaman önişlemci onu _Bool 
    biçimine dönüştürmektedir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İzleyen paragraflarda C'de bildirim işleminin bazı ayrıntıları el elaınacaktır. Bu bağlamda bildirimle kullanılabilen
    "yer belirleyicileri (storage class specifiers)" ve "tür niteleyicileri (type qualifiers)" ele açıklanacaktır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Biz daha önce bildirim işleminin şöyle yapıldığını görmüştük:

    <tür belirleyicisi> <dekleratör listesi>;

    Tür belirleyicisi (type specifier) tür belirten anahtar sözcükler olabildiği gibi struct anahtar sözcü ve yapı ismi de olabilir, typedef ismi de olabilir. 
    Bir bildirim işleminde tür belirleyicisinin dışındaki ve ilkdeğer ifadesinin dışındaki atom gruplarına "dekleratör (declarator)" ddenilmektedir. Örneğin:

    int a[3], *pi, b;

    Burada int tür belirleyicisidir. a[3], *pi ve b birer dekleratördür. Dekleratörü '=' ile bir ilkdeğer ifadesi izleyebilir. İlkdeğer dekleratöre dahil değildr. 
    Örneğin:

    int a[3] = {10, 20, 30} *pi, b = 20;

    Burada yine dekleratörler a[3], *pi ve b'dir. O halde bildirim işleminin biraz daha iyileştirilmiş genel biçimi şöyle olabilir:

    <tür belirleycisi> <dekleratör> [ = ilkdeğer], <dekleratör> [ = ilkdeğer], ...;

    Bu biçim de aslında gerçeği tam olarak yansıtmamaktadır. Çünkü bir bildirimde tür belirleyicisinin yanı sıra "yer belirleyici (storage class specifer)" ve
    "tür niteleyicisi (type qualifer)" de kullanılabilmektedir. O halde bildirim işleminin genel biçimi aslında şöyledir:

    [tür niteleyicileri] [yer belirleyicisi] <tür belirleyicisi> <dekleratör listesi (ilkdeğer verilmiş olabilir);

    Tür belirleyicisi, yer belirleyicisi ve tür niteleyicileri aslında bildirimde herhangi bir sırada bulunabilirler. Ancak programcıların çoğu bunları şöyle
    sıralandırmaktadır: Önce tür niteleyicileri, sonra yer belirleyicileri sonra da tür belirleyicileri. C99'a kadar eğer bir bildirimde en az bir yer belirleyicisi
    ya da tür niteleyicisi varsa tür belirleyicisi bulunmak zorunda değildi. Bu durumda tür belirleyicisi default int kabul ediliyordu. Örneğin:

    const static a = 10;		/* C90'da geçerli ancak C99 ve sonrasında ve C++'ta geçersiz */

    Burada const bir tür niteleyicisidir. static ise bir yer belirleyicisidir. İşte C90'da bu durum geçerli kabul ediliyordu ve tür belirleyicisi belirtilmediği 
    için default tür belirleyicisi int kabul ediliyordu. Ancak C99 ve sonrasında ve C++'ta bu kural kaldırılmıştır. Tür belirleyicisinin bulundurulması zorunlu 
    hale getirilmiştir. Örneğin:

    const static int a = 10;

    Yukarıda da belirtildiği gibi bildirimde tür niteleyicileri, yer belirleyicileri ve tür belirleyicileri herhangi bir sırada bulunabilir. Örneğin:

    static int const a = 10;

    Birden fazla anahtar sözcükten oluşan türlerde de anahtar sözcüklerin sırasının bir önemi yoktur. Örneğin:

    int const unsigned static a = 10;

    "unsigned int" türü "int unsigned" biçiminde de belirtilebilir. 
    
    Anımsanacağı gibi diziler, yapılar gibi bileşik türlere (aggregate types) ilkdeğer verirken küme parantezleri kullanılıyordu. Örneğin:

    int a[3] = {1, 2, 3};

    Aslında C standartlarına gör bileşik olmayan türlere de küme parantezleriyle ilkdeğer verilebilmektedir. Örneğin:

    int a = {0};		/* geçerli */

    Tabii böyle bir şeye hiç gerek yoktur. Bu nedenle C programcıları genellikle bu durumun geçerli olduğunu bile bilmeyebilirler. Tabii dizi ve yapılara küme 
    parantezi olmadan ilkdeğer verilememektedir. Örneğin:

    int a[1] = 10;		/* geçersiz! */

    Şİmdi de bildirimdeki "yer belirleyicileri (storage class specifiers)" ve "tür niteleyicileri (type qualifer)" konusu üzerinde duracağız.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bildirimde kullanılabilecek dört yer belirleyicisi anahtar sözcük vardır:

    auto
    register
    extern
    static

    İki de tür niteliyici anahtar sözcük vardır:

    const
    volatile

    Bir bildirimde iki yer belirleyicisi bir arada kullanılmaz. Ama iki tür niteleyicisi bir arada kullanılabilir. Örneğin:

    static extern int a;				/* geçersiz! */
    static const volatile b = 10;		/* geçerli */
    extern auto int c;					/* geçersiz! */

    Şimdi izleyen paragraflarda yer ve tür belirleyicilerinin işlevleri üzerinde duracağız. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    auto yer belirleyicisi gereksiz ve artık bugün için anlamsız bir belirleyicidir. Geçmişe doğru uyumu korumak için hala muhafaza edilmektedir. auto yer belirleyicisi
    global değişkenlerle ve parametre değişkenleriyle kullanılamaz. Yalnızca yerel değişkenlerle kullanılabilir. Yerel değişkenin blok bittiğinde yol edileceği 
    anlamına gelir. Zaten yerel değişkenler blok bittiğinde yok edilmektedir. O halde auto belirleyicisinin bir önemi yoktur. C++'ta C++11 ile birlikte 
    zaten anlamsız olan auto anahtar sözcüğüne "tür belirleyicisi (type specifier)" anlamı yüklenmiştir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    register yer belirleyicisi de artık gereksiz bir belirleyici haline gelmiştir. Bu belirleyicinin anlamını anlayabilmek için "register (yazmaç)" kavramını bilmek
    gerekir. CPU'nun içerisinde aritmetik, karşılaştırma, mantıksal ve bit işlemlerini yapan elektrik devreleri (mantık devreleri) bulunmaktadır. CPU'nun 
    bu kısmına "Aritmetik Lojik Birim (Arithmetic Logic Unit)" denir ve İngilizce "ALU" biçiminde kısaltılır. Yazmaçlar CPU içerisindeki ALU için iskele görevi 
    gören küçük bellek bölgeleridir. Bir işlemin yapılabilmesi için işleme sokulacak operand bellekten CPU içerisindeki yazmaçlara çekilir. 
    Çünkü ALU içerisindeki elektrik devreleri girdileri yazmaçlardan alacak biçimde tasarlanmıştır. Bu elektrik devreleri sonucu da yine yazmaçlara yerleştirmektedir. 
    Yazmaçların uzunlukları ve sayıları CPU'lar için önemlidir. Yazmaçların  uzunlukları aynı zamanda bir CPU'nun tek hamlede hangi büyüklükte bilgiler üzerinde 
    işlem yapabileceğini belirtmektedir. Örneğin "32 bit işlemci" demek tek hamlede 32 bit işlem yapabilen işlemci demektir. Dolayısıyla 32 bit işlemcilerdeki yazmaçlar da 32 bittir. 
    Örneğin 64 bit iki sayıyı 32 bit işlemcide tek hamlede toplayamayız. Önce onun düşük anlamlı 32 bitini sonra da yüksek anlamlı 32 bitini toplayıp sonucu buluruz. 
    Halbuki 64 bit bir işlemcide tek hamlede 64 bit iki sayı toplanabilmektedir. Dünyanın ilk mikroişelmsici 1974 yılında üretilen Intel'in 8080'i kabul eedilmektedir. 
    8080 8 bit bir mikroşlemciydi. Bunu 16 bit 8086, 32 bit 30386 ve 64 bit Pentium'un ileri modelleri izledi. Intel'in 32 bit işlemcilerine aile olarak x86 denilmektedir. 
    64 bit işlemcilerine ise X64 denilmektedir. ARM işlemcileri de ilkin 32 bit işlemciler olarak tasarlandı. Sonra onlar da 64 bite yükseltildi. Bugün 64
    bit işlemciler yaygın kullanılmaktadır. 128 bit işlemlere fazlaca gereksinim duyulmadığı için şimdilik 128 bit işlemciler yaygın biçimde kullanılmamaktadır. 
    Örneğin aşağıdaki gibi bir C kodu olsun:

    c = a + b;

    Burada a, b, ve c nesneleri RAM'dedir. Intel işlemcileri için derleme yapan derleyiciler de bu işlemi yapacak makina komutları üretir:

    MOV reg1, a
    MOV reg2, b
    ADD reg1, reg2
    MOV c, reg1

    Burada önce bellekteki a ve b CPU içerisindeki yazmaçlara çekilip ADD makine komutu ile ALU devreye sokularak iki yazmaçtaki değer toplanıp yeniden yazmaca 
    aktarılmıştır. En sonunda da bu yazmaçtaki değer yeniden belleğe aktarılmıştır. 
    
    Intel gibi CISC tarzı işlemcilerde makine komutları iki operand'a sahiptir. Operand'lardan biri yazmaç diğeri bellek olabilir. Örneğin aşağıdaki 
    gibi bir makine komutu Intel işlemcilerinde geçerlidir:

    ADD reg, mem_addr

    Burada yazmaçtaki değerle ilgili adresteki değer toplanıp sonuç yazmaçtaki değer bozularak yeniden yazmaca yerleştirilmektedir. Halbuki ARM gibi MIPS 
    gibi RISC tarzı işlemcilerde işlem yapan (aktarım yapan değil) makine komutları genel olarak üç operand'lıdır. Bu işlemciler de operand'ların ikisi de
    yazmaca çekilmiş olmalıdır. Örneğin:

    c = a + b;

    işlemi için derleyiciler RISC işlemcilerinde aşağıdaki makine komutlarını üretmektedir:

    LD reg1, a
    LD reg2, b
    ADD reg3, reg1, reg2
    ST c, reg3

    Bir değişken register anahtar sözcüğü ile tanımlanırsa bu şu anlama gelmektedir: "Derleyici ben bu değişkeni çok sık kullanacağım. Dolayısıyla 
    sen onu RAM'de tutmak yerine doğrudan CPU yazmaçlarının içinde tutmaya çalış. Böylece bu değişken işleme sokulduğunda her defasında yeniden CPU yazmaçlarının içerisine 
    çekilip (load işlemi) yeniden belleğe geri yazılmasın (store işlemi)". Ancak "register" belirleyicisi bir "emir" değil "rica" niteliğindedir. 
    Yani derleyici değişken register anahtar sözcüğü ile tanımlanmış olsa bile onu CPU yazmaçlarında tutmayabilir. Normal bir değişken gibi yine 
    onı RAM'de tutabilir. Bu durumda bir uyarı da vermeyebilir. Bugün derleyicilerin  kod optimizasyonları çok iyileştirilmiştir. Dolayısıyla derleyiciler 
    hangi değişkeni ne kadar süre hangi yazmaçlarda tutacağını iyi bir hesap ile zaten programcıdan çok daha iyi belirleyebilmektedir. (Buna kod optimizasyonunda 
    "yazmaç optimizasyonu (register optimization)" denilmektedir.) Dolayısıyla bugünkü yaygın derleyiciler "register" anahtar sözcüğünü hiç dikkate 
    almamaktadır. Bu anahtar sözcük de artık kullanım gereğini kaybetmiştir.

    Yazmaçların adresleri yoktur. Dolayısıyla register anahtar sözcüğü ile tanımlanmış bir nesnenin adresini alamayız. (O nesne yazmaçta tutulmuyor olsa bile 
    onun adresini alamayız.) alamayız. register belirleyicisi ile tanımlanmış bir değişkenin adresi alınmaya çalışılırsa bu durum geçersizdir (tipik olarak 
    "compile time error").

    Bazı mimarilerde CPU içerisinde az sayıda yazmaç bulunmaktadır. Örneğin Intel'in Pentium modeline kadar işlemcilerinde çok az yazmaç bulunmaktaydı. Bir değişkenin 
    bu yazmaçlardan birini sürekli kullanması genel performansı zaten bu sistemlerde düşürebilmektedir. Öte yandan RISC tabanlı ARM gibi MIPS gibi işlemcilerde 
    daha fazla yazmaç bulunma eğilimindedir. Derleyiciler nesneleri bu mimarilerde daha uzun süre yazmaçlarda tutabilmektedir. 

    C'de register yer belirleyicisi yerel değişkenlerle ve parametre değişkenleriyle kullanılabilir. Ancak global değişkenlerle kullanılamaz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                            58.Ders - 03/01/2023 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Büyük bir projeyi tek bir kaynak dosya biçiminde gerçekleştirmek iyi bir teknik değildir. Bunun iyi bir teknik olmamasının temel nedenleri şunlardır:

    - Çok büyük kaynak dosyaların edit edilmesi ve kodun bakımının yapılması güçtür. 

    - Çok büyük kaynak dosyalarda bir değişiklik yapıldığında tüm dosyanın yeniden derlenmesi gerekir. Büyük dosyaların derlenmesi dakikalarca ve hatta saatlerce 
    zaman alabilmektedir. 

    - Bir projenin tek bir kaynak dosya biçiminde oluşturulması projenin değişik parçalarının paralel bir biçimde değişik kişiler tarafından yazımını 
    engellemektedir. 

    İşte eğer büyük bir proje birden fazla kaynak dosyaya bölünürse bu kaynak dosyalar diğerlerinden bağımsız olarak derlenebilir. Sonra birlikte link
    edilerek çalıştırılabilir dosya elde edilebilir. Böylece bir değişiklik yapıldığında bütün projenin yeniden derlenmesine gerek kalma<z. Yalnızca değişikliğin
    yapıldığı kaynak dosya yeniden derlenip yine tüm amaç dosyalar birlikte link edilebilirler. 

    Link işlemi birden fazla amaç dosyayı (object files) alıp tek bir "çalıştırılabilir (executable)" dosya oluşturabilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir projeyi oluşturan kaynak dosyaların her birine C standartlarında "derleme birimi (translation unit)" denilmektedir. Biz burada "modül" terimini de kullanacağız. 
    Örneğin biz 100000 satırlıkk bir projei 10 kaynak dosyaya (translation unit) bölmülş olalım. Bu dosyalar da yaklaşık 10000 civarı satırdan oluşsun.
    Bu dosyalara a1.c, a2.c, a3.c, ..., a19.c ve a10.c isimlerinin verildiğini varsayalım. O zaman bu dosyaları diğerlerinden bağımsız olarak derleyip 
    amaç dosya oluşturacağız ve bu amaç dosyayı da link işlemine sokacağız. Bu amaç dosyaların a1.obj, a2.obj, a3.obj, ..., a9.obj, a10.obj isminde 
    olduğunj varsayalım. Şimdi biz bu amaç dosyaları link işlemine sokup tek bir "çalıştırılabilir dosya (executable file)" elde edeceğiz. 
    İşte burada iki önemli durum ortaya çıkacaktır. Buradaki modüller diğerlerindne bağımzı bir biçimde derlendiğine göre biz bir modülde başka bir modüldeki
    fonksiyonları nasıl çağıracağız ve başka bir modüldeki global değişken leri nasıl kullanacağız? Çünkü bu proje tek kaynak dosya biçiminde yazılmış 
    olsaydı bir global değişkeni her yerden kullanabilirdik. Oysa biz bu projeyi 10 parçaya böldüğümüz zaman bir global değişken bir modülde kalacaktır, 
    diğer modülden nasıl kullanılacaktır?
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir global değişkenin ya da bir fonksiyonun başka modüllerden kullanılabilirliğine C standartlarında "linkage" denilmektedir. Bir global değişken ya da 
    fonksiyon yalnızca tanımlandığı modülde kullanılabiliyorsa buna "internal linkage", tanımlandığı modülün dışındaki modüllerden de kullanılabiliyorsa
    buna da "external linkage" denilmektedir. Global değişkenler ya da fonksiyonlar "internal" ya da "external" linkage'a sahip olabilirler. Yerel 
    değişkenlerin ve parametre değişkenlerinin "linkage'ı yoktur". Bunlar zaten hiçbir zaman başka bir modül tarafından kullanılamazlar. 

    Bir global değişken ya da fonksiyon default durumda "external" linkage'a sahiptir. Global değişkeni ya da fonksiyonu "internal linkage'a" sahip 
    hale getirebilmek için "static" yer belirleyicisinin kullanılması gerekir. Örneğin:

    int g_x;			/* external linkage, yani başka modüllerden kullanılabilir */
    static int g_y;		/* internal linkage, başka bir modüllerden kullanılamaz */

    void foo(void)		/* foo external linkage'a sahip, başka modüllerden kullanılabilir */ 
    {
        /* ... */
    }

    static void bar(void)		/* bar internal linkage'a sahip başka modüllerden kullanılamaz */
    {
        /* ... */
    }

    Pekiyi bir modülde tanımlanmış olan external linkage'a sahip bir global değişkeni başka bir modülde nasıl kullanabiliriz? İşte bunun için "extern" yer 
    belirleyici anahtar sözcükten faydalanılmaktadır. Bir global değişken extern olarak bildirilirse bu durumda programcı derleyiciye adeta şunu 
    demektedir: "Derleyici bu global değişken için yer ayırma, çünkü bu global değişken aslında başka bir modülde tanımlanmış bir global değişkendir.
    Yani bu global değişken için yer başka bir modülde ayrılmış durumdadır. Ben bu global değişkeni kullandığımda aslında başka bir modülde yeri ayrılmış 
    olan o global değişkeni kullanmış oluyorum. Dolayısıyla ben bu global değişkeni kullandığımda bana kızma". İşte derleyici de extern olarak bildirilmiş 
    bir global değişken için yer ayırmaz. Dolayısıyla extern bildirimi bir tanımlama oluşturmamaktadır. Örneğin:

    extern int g_x;

    int main(void)
    {
        g_x = 10;
        printf("%d\n", g_x);

        return 0;
    }

    Burada derleyici g_x için yer ayırmayacaktır. Çünkü onun yeri zaten başka bir modülde ayrılmıştır. Pekiyi derleyici g_x'in yerini bilmediğine göre 
    nasıl makine komutları üretmektedir? İşte derleyici extern bir değişken kullanıldığında onun için ürettiği makine kodlarında bazı yerleri boş bırakmaktadır.
    Ancak nereleri boş bıraktığını DA amaç dosyanın belli bir kısmına yazmaktadır. Bu değişkeni diğer modüllerde arayıp bulmak ve derleyicinin boş bıraktığı 
    makine komutlarındaki yerleri doldurmak linker'ın görevidir. Pekiyi linker ya global değişkenin tanımlamasını başka bir modülde bulamazsa ne olur?
    İşte bu durumda link aşamasında "error" ortaya çıkacaktır. Tabii global değişken başka bir modülde tanımlanmış olabilir ancak "internal" linkage'a sahip 
    olabilir. Bu durumda da link aşamasında eror oluşacaktır. Şimdi de şöyle bir senaryo üzerinde duralım. Projemiz "a.c" ve "b.c" isimli iki kaynak 
    dosyadan oluşsun. "a.c" dosyasında da g_x global değişkeni tanımlanmış olsun, "b.c" dosyasında da g_x global değişkenş tanımlanmış olsun. Bu durumda
    her iki modül de bağımsız olarak derlenir. Her iki modülde de g_x için yer ayrılır. İki derleme de başarılı olur. Ancak link aşamasında sorun çıkar.
    Çünkü linker link işlemi sırasında aynı isimli birden fazla extern linkage'a sahip değişken görürse "error" oluşturmaktadır. O halde bir global değişken 
    tek bir modülde extern linkage'a sahip olacak biçimde global olarak tanımlanmalı, kullanılacak modüllerde extern olarak bildirilmelidir. Global değişkenin tüm modüllerde 
    extern bildirilmesi ancak hiçbir modülde global tanımlamanın yapılmaması yine link aşamasında "error" ile sonuçlanacaktır. Benzer biçimde global 
    değişkenin birden fazla modülde extern linkage'a sahip biçimde global olarak tanımlanması da link aşamasında "error" ile sonuçlanacaktır. 
    Global değişkenin hangi modülde extern linkagae'a sahip biçimde tanımlanmış olduğunun hiçbir önemi yoktur.

    Anımsanacağı gibi C'de derleyicinin yer ayırdığı bildirimlere "tanımlama (definitions)" deniyordu. O halde bildirimde extern kullanıldığında 
    bu bir tanımlama anlamına gelmemektedir. Örneğin:

    int g_x;				/* hem bildirim hem de tanımlama, g_x için yer ayrılıyor */
    extern int g_y;			/* bildirim ama tanımlama değil,i g_y için yer ayrılmıyor */

    C standartlarına göre extern bildirimlerinde bildirilen değişkene ilkdeğer verilirse artık bu durum "tanımlama" anlamına gelmektedir Dolayısıyla burada 
    "extern" anahtar sözcüğünün bir etkisi kalmamaktadır. Örneğin:

    extern int g_x;				/* bildirim tanımlama dğeil */
    exrtern int g_y = 10;		/* özel durum, artık bu bir tanımalamdır, çünkü ilkdeğer verilmiştir. g_y için yer ayrılacaktır */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyonlar için gerçekten "extern" anahtar sözcüğünün bir etkisi yoktur. Çünkü fonksiyon prototipleri zaten extern bildirimi kabul edilmektedir.
    Başka bir deyişle biz bir modülde (translation unit) bir fonksiyonun prototipini yazmışsa ancak tanımlamasını yapmamışsak zaten derleyici 
    onun başka bir modülde tanımlanmış bir fonksiyon olduğunu düşünmektedir. Bunun için extern bildirimine ayrıca gerek yoktur. Örneğin:

    void foo(void);

    int main(void)
    {
        foo();			/* foo zaten diğer moüllerde aranacak */

        return 0;
    }

    Burada derleyici zaten foo fonksiyonun tanımını görmediğinden amaç dosyaya linker için "foo'nun başka modüllerde aranması gerektiği notunu" 
    yazmaktadır. Fonksiyon prototiplerinin başına extern getirmeye gerek yoktur. Ancak getirilmesi de hata oluşturmaz. Örneğin:

    extern void foo(void);		/* extern'e gerek yok ancak kullanılabilir */

    Bu durumda C'de olmayan bir fonksiyonu çağırsak bile hata derleme aşamasında ortaya çıkmaz. Link aşamasında linker'ın fonksiyonu başka modüllerde bulamaması biçiminde
    ortaya çıkar. Anımsanacağı gibi derleyici bir fonksiyonun tanımlamasını ya da prototipini görmeden fonksiyon çağrılmışsa bu durum C99 ve sonrasında
    geçersizdir. Ancak C90'da fonksiyonun sanki geri dönüş değerinin int paranmetre parantezinin de boş olacak biçimde protipinin yazıldığı varsayılmaktadır. 

    O halde bir projenin bir modülünde başka bir modülünde tanımlanmış olan fonklsiyonu çağırmak için tek yapılacak şey fonksiyonun prototipinin 
    bulundurulmasıdır. Prototipte extern belirleyicisinin ayrıca kullanılmasına gerek yoktur.

    Anımsanacağı gibi C'de bir global değişken birden fazla kez aynı modülde (tanslation unit) tanımlanırsa bu duruma "tentative definition" deniliyordu.
    Bu durumu derleyici ""sanki tanımlama bir kez yapılmış gibi" ele alıyordu. Örneğin:

    int g_x;
    int g_x;
    int g_x;

    Bu durum C'de geçerlidir. Burada g_x için bir tane yer ayrılacaktır. Tentative definition durumunda bu tanımlamaların yalnızca birinde ilkdeğer verilebilir. 
    Örneğin:

    int g_x;
    int g_x = 10;
    int g_x;

    Derleyici burada g_x'e ilkdeğer olarak 10 verilmiş olduğunu varsaymaktadır. Ancak birden fazla kez ilkdeğer verilemez. Örneğin:

    int g_x = 10;
    int g_x = 20;		/* geçersiz! */
    int g_x;

    Ancak standartlara göre farklı modüllerde tentative definition geçerli değildir. Fakat derleyicilerin bir bölümü bunu geçerli olarak 
    ele alabilmektedir. Örneğin:

    /* a.c */

    int g_x;
    ...

    /* b.c */

    int g_x;
    ...

    Bu durum C standartlarına geçersizdir. Ancak Microsoft derleyicileri bu durumda bir hata mesajı vermemektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                59.Ders - 05/01/2023 Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    extern bildirimi genellikle global düzeyde programın tepesinde yapılmaktadır. Ancak C'de programcı isterse extern bildirimini yerel bir blokta da 
    yapabilir. Örneğin:

    void foo(void)
    {
        extern int g_x;

        /* ... */
    }

    void bar(void)
    {
        g_x = 10;		/* geçersiz! */

        /* ... */
    }

    extern bildiriminin yerel bir blokta yapılması kişilere tuhaf gelebilmektedir. Çünkü extern ile bildirilen değişken başka bir modüldeki 
    global bir değişkendir. O halde başka bir modüldeki global bir değişkenin yerel biçimde extern olarak bildirilmesinin ne anlamı olabilir? İşte extern bildirimi
    yerel bir blokta yapılırsa yine değişken global bir değişken olur. Ancak o global değişken yalnızca o blokta kullanılabilir. Yukarıdakii örnekte 
    g_x başka bir modülde tanımlanmış olan global bir değişkendir. Ancak biz bu g_x değişkenini yalnızca foo içerisinde kullanabiliriz. Dolayısıyla bu 
    değişkenin bar içerisinde kullanımı da geçersizdir. Tabii yukarıda da belirttiğimiz gibi en normal durum extern bildiriminin programın tepesinde yapılmasıdır.
    Örneğin:

    extern int g_x;

    void foo(void)
    {
        g_x = 20;			/* geçerli */

        /* ... */
    }

    void bar(void)
    {
        g_x = 10;			/* geçerli */

        /* ... */
    }

    extern belirleyicisi ile bildirilen değişkene ilkdeğer verilmesi durumunda extern belirleyicinin bir anlamı kalmıyordu. İşte yerel düzeyde extern olarak 
    bildirilen değişkenlere ilkdeğer verilememektedir. Örneğin:

    void foo(void)
    {
        extern int g_x = 10;		/* geçersiz! */

        /* ... */
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Birden fazla modülle çalışma nasıl yapılmaktadır? IDE'li sistemlerde bu çok kolaydır. Programcının tek yapacağı şey projeye birden fazla kaynak dosyayı 
    eklemektir. Biz örneğin Visual Studio IDE'sinde projeye "sample.c" ve "mample.c" biçiminde iki kaynak dosya eklemiş olalım. Bu durumda Ctrl+F5 
    tuşuna bastığımızda IDE önce bu "sample.c" ve "mample.c" dosyalarını bağımsız bir biçimde derler ve bunlardan "sample.obj" ve "mample.obj" 
    biçiminde iki amaç dosya oluşturur. Sonra da bu iki amaç dosyayı birleştirerek proje ismi ile aynı isimli "exe" dosya oluşturur. 

    Genel olarak IDE'lerde "build" ya da "make" işlemi "yalnızca değişmiş olan kaynak dosyaları yeniden derle ve hepsini birden link işlemine sok"
    anlamına gelmektedir. Örneğin biz Visual Studio IDE'sinde projemize 100 tane kaynak dosya eklemiş olalım. Bu projeyi ilk kez çalıştırdığımızda 
    bu dosyaların hepsi mecburen derlenecektir. Ancak daha sonra hangi kaynak dosyalar üzerinde değişiklik yapılmışsa "build" işlemi sırasında yalnızca
    o kaynak dosyalar yeniden derlenir ve hep birlikte tüm amaç dosyalar link işlemine katılır. Bu bizim istediğimiz bir şeydir. Biz Ctrl+F5 tuşuna 
    bastığımızda ya da Build menüsünden Build seçeneklerini seçtiğimizde tüm kaynak dosyalar değil yalnızca değişmiş olanlar yeniden derlenmektedir.
    Yalnızca Visual Studio'da değil tüm C IDE'lerinde durum böyledir. 

    IDE'lerde ayrıca bir de "Build All" ya da "Rebuild" seçenekleri de vardır. Bu seçenekler "yalnızca değişmiş olan dosyaların değil projedeki tüm
    dosyaların kayıt şartsız yeniden derlenmesi ve amaç dosyaların link edilmesi" anlamına gelmektedir. Bazen build sisteminde sorunlar oluştuğunda 
    programcı şüphe altında kalırsa tüm kaynak dosyaları yeniden derlemek isteyebilir. Tabii her defasında "rebuild" işleminin yapılması çalışma
    döngüsü bakımından anlamsızdır. 

    Pekiyi build sistemi bir kaynak dosyanın değişip değişmediğini nasıl anlamaktadır? IDE'ler build işlemini yapmadan önce kesinlikle kaynak dosyaları
    save etmektedir. Build sisteminin tek yaptığı şey "kaynak dosyanın son değiştirilme tarih ve zaman bilgisi ile amaç dosyanın son değiştirilme tarih ve zaman 
    bilgisini" karşılaştırmaktır. Eğer kaynak dosyanın son değiştirilme tarih ve zaman bilgisi amaç dosyanınkinden daha ileride ise bu durum kaynak dosyanın değişmiş olduğu 
    anlamnına gelmektedir. Bazen patolojik durumlarda dosyaların tarih ve zamana bilgileri bozulabilir. Ya da işletim sisteminin tarih ve zaman 
    bilgisi değiştirilmiş olabilir. Bu tür durumlarda build sistemi de bozulabilir. İşte programcı şüphe altında kaldığı bu tür durumlarda "rebuild" 
    işlemi yapmalıdır. 

    IDE'lerde "build", "rebuild" seçeneklerinin yanı sıra bir de "clean" biçiminde bir seçenek bulunabilmektedir. Build terminolojisinde "clean" işlemi demek
    "tüm amaç dosyaları ve çalıştırılabilen dosyaları silmek" demektir. Dolayısıyla biz "clean" yaptığımızda sanki hiç derleme yapmamış gibi bir durumda oluruz. 
    Tabii bu duurmda "build" yapılırsa mecburen tüm kaynak dosyalar yeniden derlenecektir. Pekiyi "clean" işlemine neden gereksinim duyulmaktadır? 
    İşte genellikle programcı projeyi birisine vermek ya da onu saklamak için "clean" işlemini tercih edebilmektedir. Bu sayede proje amaç dosyalardan 
    ve çalıştırılabilir dosyadan arındırılmış olmaktadır. Ayrıca clean işlemi build otomasyon sistemlerinde "rebuild" işlemine zorlamak için de kullanılabilmektedir. 

    Visual Studio IDE'sinde bir "solution" içerisinde birden fazla proje bulunabilmektedir. Ancak bu projelerin yalnızca bir tanesi aktif olmaktadır. 
    (Aktif proje siyahrenkle gösterilir) Build menüsündeki "Build Solution", "Rebuild Solution" ve "Clean Solution" menü elemanları solution içerisindeki tüm
    projeler için eylem belirtir. Halbuki "Build XXX", "Rebuild XXX" ve "Clean XXX" (burada XXX aktif projenin ismidir) yalnızca aktif olan projede eylem 
    belirtmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi birden fazla modülle çalışırken IDE kullanmıyorsak bu işlemleri nasıl yapabiliriz? Bunun iki yolu olabilir:

    1) Değişen dosyaları komut satırında manuel derlemek ve linker programını ayrıca açalıştırarak link işlemini de manuel biçimde yapmak.
    2) Bu işlemi otomatize eden ve ismine "build automation tool" denilen özel araçları kullanmak. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Örneğin "sample.c" ve "mample.c" dosyalarından oluşan bir projeyi Microsoft derleyicileri ile manuel bir biçimde derlemek isteyelim. Dosyalar şöyle olsun:

    /* sample.c */

    #include <stdio.h>

    void foo(void);

    extern int g_x;

    int main(void)
    {
        g_x = 10;

        foo();

        printf("%d\n", g_x); 
        
        return 0;
    }

    /* mample.c */

    #include <stdio.h>

    int g_x;

    void foo(void)
    {
        printf("foo\n");

        g_x = 20; 
    } 

    Bu işlem manuel olarak şöyle yapılabilir:

    cl /c sample.c
    cl /c mample.c

    link /OUT:test.exe sample.obj mample.obj

    Burada /c seçeneği "only compile" anlamına gelmektedir Yani yalnızca derleme yapılır ancak link işlemi yapılmaz. Microsoft'un linker programı "link.exe"
    isimli programdır. Burada /OUT seçeneği oluşturulacak çalıştırılabilir dosyaya isim vermek amacıyla kullanılmaktadır. Diğer bir seçenek de "cl.exe" 
    derleyicisini çalıştırırken tüm kaynak dosyaları vermek olabilir. Ancak bu durumda verilen tüm kaynak dosyalar koşulsuz derlenip link işlemine
    sokulmaktadır. Dolayısıyla bu seçenek az sayıda kaynak dosya için uygulanabilir bir seçenktir. Örneğin:

    cl /Fe:test.exe sample.c mample.c

    gcc ve clang derleyicilerinde işlemler benzerdir. "Only compile" için "-c" seçeneği kullanılır. Ancak bu sistemlerdeki "ld" isimli linker 
    maalesef kütüphane dosyalarını ve "start-up" dosyaları link işlemine katmamaktadır. Bunların link işlemine katılması biraz< zahmetlidir. Bunun yerine
    link işlemi de "gcc" programına yaptırılabilir. Tabii "gcc" derleyicisi link için çalıştırılınca aslında yine "gcc" programı arka planda "ld" 
    linker'ını çalıştırmaktadır. Ancak bu linker'ı kütüphane dosyalarını ve start-up dosyaları vererek çalıştırmaktadır. İşlemler şöyle yapılabir:

    gcc -c sample.c
    gcc -c mample.c
    gcc -o test sample.o mample.o

    gcc ve clang derleyicilerinin genel kullanımı aynıdır. UNIX/Linux sistemlerinde amaç dosyaların uzantılarının ".obj" biçiminde değil ".o" biçiminde 
    olduğuna dikkat ediniz. gccve clang derleyicilerinde de birden fazla kaynak dosya yine komut satırında belirtilebilir. Örneğin:

    gcc -o test sample.c mample.c

    Burada yine her iki dosya da şartsız derlenecek ve birlikte link işlemine sokulacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Manuel bir biçimde komut satırından yalnızca değişen kaynak dosyaların derlenerek link edilmesi az sayıda kaynak dosya olduğunda uygulanabilecek 
    bir yöntemdir. Ancak yüzlerce kaynak dosyanın bulunduğu bir projede (böyle çok proje vardır) build işleminin manuel yapılması çok zahmetlidir. 
    İşte bu nedenle bu işlemi otomatize eden ve ismine "build automation tools" denilen araçlar kullanılmaktadır. Aslında IDE'ler de arka planda bu build araçlarını 
    kullanmaktadır. Yani örneğin biz Visual Studio'da Ctrl+F5 tuşlarına bastığımızda ya da "Build" menüsünden "Build" seçeneğini seçtiğimizde arka planda
    aslında IDE Microsoft'un "MSBuild" denilen build otomasyon aracını devreye sokmaktadır.

    Build işlemini otomatize eden pek çok araç geliştirilmiştir. Build işlemi programlama dili ile de ilgilidir. Dolayısıyla örneğin Java'da kullanılan 
    build araçları ile C'de kullanılan build araçları birbirinden farklı olabilmektedir. C ve C++ dünyasında en çok kullanılan build araçları 
    Microsoft'un "MSBuild" denilen aracı ile GNU'nun "make" denilen aracıdır. Aslında "make" aracının orijinali eski UNIX sistemlerine dayanmaktadır. 
    Ancak bu araç GNU projesi kapsamında "GNU make" ismiyle yeniden yazılmıştır. Microsoft'un da "make" benzeri bir aracı vardır. Ona da "nmake" denilmektedir. 
    Tabii Microsoft uzun süredir XML tabanlı "MSBuild" aracını kullanmaktadır. 

    Build araçlarında çalışma biçimi şöyledir: Programcı build işlemi sırasında yapılacak şeyleri belli bir sentaks ve semantiği olan bir dilde
    bir dosya içerisine yazar. Sonra bu dosyayı işleten programı çalıştırır. Örneğin GNU Make denilen araç ile çalışılırken programcı önce
    ismine "Makefile" denilen bir dosya oluşturur. Bu dosyanın içerieine yönergeleri yazar. Sonra "make" denilen programı bu dosyayı vererek çalıştırır. 
    make programı da bu makle dosyasını okur yönergeleri yerine getirir. Make dosyaı oluştmanın bazı yarıntılı kurallaerı vardır. Yani bu küçük bir dil gibidir. 
    Make aracının kullanımı "Sistem Programlama ve İleri C Uygulamaları" kursunda anlatılmaktadır. Microsoft'un MSBuild aracı da XML tabanlı bir 
    dil kullanmaktadır. Yine biz Visual Studio IDE'sinde projeye bir kaynak dosya ekleyip, proje ayarlarını değiştirdiğimizde aslında Visual Studio
    arka planda bir XML dosyasını oluşturmaktadır. MSBuild aracı bu XML dosyasını okuyarak işlemlerini yapmaktadır.

    Bazı build araçlarına "üst düzey build araçları" denilmektedir. Bunlar aslında daha aşağı seviyeli build araçlarını kullanmaktadır. Bunların en ünlüleri 
    "cmake" ve "qmake" isimli araçlardır. Bu iki araç aslında ürün olarak make dosyaları üretmektedir. Bu araçların ürettikleri make dosyaları ayrıca "make"
    programıyla işletilmelidir. Ancak "cmake" ve "qmake" araçları daha basit bir yapıya sahiptir. Dolayısıyla aslında bu araçlar "make dosyası yazmayı"
    otomatize eden ve kullanımı daha basit olan araçlardır. Örneğin biz cmake ile çalışmak istediğimizde yine "cmake" diline uygun bir dosya oluştururuz. Bunu 
    "cmake" programına işletiriz. "cmake" programı buradan bir make dosyası oluşturur. Bu make dosyasını da "make" programıyla işleme sokarız. 

    Windows sistemlerinde GNU'nun make programının Windows port'u kullanılabilir. Ancak Microsoft'un GNU make programına benzer "nmake" denilen 
    bir aracı da vardır. Windows sistemlerinde nmake kullanmak daha uygun olabilmektedir. macOS sistemlerinde de ağırlıklı olarak "GNU make", "cmake"
    ve "qmake" araçları kullanılmaktadır. Ayrıca XCode'un kedni proje formatı da vardır. 

    Aşağıda "sample.c" ve "mample.c" dosyalarından oluşan basit projenin build edilmesi için bir make dosyasu örneği verilmiştir. (make programı default durumda
    eğer dosya ismi belirtilmezse "Makefile" isimli bir dosyayı işleme sokmaktadır.)

    # Makefile

    test: sample.o mample.o
        gcc -o test sample.o mample.o
    sample.o: sample.c
        gcc -c sample.c
    mample.o: mample.c
        gcc -c mample.c
    clean:
        rm -f *.o 
        rm -f test
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    static yer belirleyicisi yerel değişkenlerle ve global değişkenlerle kullanılabilmektedir. Ancak parametre değişkenleriyle kullanılamamaktadır. 
    static belirleyicisinin yerel değişkenlerle kullanımı "static yerel değişkenler", global değişkenlerle kullanımı "static global değişkenler"
    biçiminde isimlendirilebilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir global değişken "static" anahtar sözcüğü ile tanımlanırsa global değişken "internal linkage" özelliğine sahip olur. Yani bu global değişken başka bir 
    modülde extern olarak bildirilse bile kullanılamaz. Yalnızca o modülde (tanslation unit) kullanılabilir. 

    Aynı durum fonksiyonlar için de geçerlidir. Çünkü fonksiyonlar da "global değişkenler gibi" ele alınmaktadır. Yani biz bir fonksiyonun tanımlamasında
    static belirleyicisini kullanırsak o fonksiyonu yalnızca o modülde herhangi bir yerde çağırabiliriz. Başka bir modülden o fonksiyonu çağıramayız.  
    static fonksiyonlar da "internal" linkage özelliğine sahiptir. 

    Farklı modüllerde (translation unit'lerde) aynı isimli static global değişkenler ya da fonksiyonlar bulunabilir. Bu durum link aşamasında bir sorun oluşturmaz. 

    Aşağıdaki örnekte her iki modülde de g_x isminde static global nesneler tanımlanmıştır. Ancak her iki modüldeki nesneler farklı nesnelerdir. 
----------------------------------------------------------------------------------------------------------------------*/

/* sample.c */

#include <stdio.h>

void foo(void);
void bar(void);

static int g_x;

int main(void)
{
    g_x = 100;

    foo();
    bar();

    printf("%d\n", g_x);        /* 100 */

    return 0;
}

#include <stdio.h>

static int g_x = 10;

void foo(void)
{
    g_x = 20;
}

void bar(void)
{
    g_x = 30;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                            60.Ders - 10/01/2023 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    static global bildiriminde şöyle bir kural vardır: Bir global değişken ya da fonksiyon extern olarak bildirildiyse bu global değişken 
    ya da fonksiyon daha önce hangi linkage ile bildirilmişse o linkage'a sahip olur. Yani örneğin biz bir fonksiyonun prototipinde static anahtar sözcüğünü 
    belirtmişsek daha sonra static anahtar sözcüğünü belirtmesek bile bu fonksiyon static kabul edilir. Örneğin:

    static void foo(void);
    
    /* ... */
    
    void foo(void)		/* burada hiçbir şey yazılmmamışsa fonksiyonlar için sanki extern yazılmış gibi işlem yapılır, dolayısıyla linkage "internal" durumdadır */
    {
        /* ... */
    }

    Tabii okunabilirlik bakımından static anahtar sözcüğünün hem prototipte hem de tanımlamada belirtilmesi iyi bir tekniktir. 

    Ancak yukarıdaki durumun tersi geçerli değildir. Yani biz bir fonksiyonun önce external linkage olarak prototipini yazıp sonra onu internal linkage 
    ile tanımlayamayız. Örneğin:

    void foo(void);

    static void foo(void)		/* geçerli değil! */
    {
        /* ... */
    }
    
    Aynı dıırm global değişkenler için de geçerlidir. Örneğin:

    static int g_x;
    int g_x;

    Burada g_x static global ve internal linakage'a sahiptir. Ancak aşağıdaki tanımlama geçersizdir:

    int g_x;
    static int g_x;			/* geeçersiz! */
    
    Benzer biçimde bir global değişken ya da fonksiyon önce static belirleyicisi ile tanımlanıp sonra extern belirleyici ile tanımlansa da 
    yine tanımlama geçerlidir. Örneğin:

    static int g_x;
    extern int g_x;

    Burada g_x internal linkage'a sahiptir:.

    Yukarıdaki tanımlamalar "tentative" biçimde kabul edilmektedir. Yani bu örneklerde yalnızca bir tane nesne oluşturulmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi bir global değişken ya da fonksiyon ne zaman static yapılmalıdır? Eğer programcı tek bir kaynak dosya üzerinde çalışıyorsa ya da az sayıda 
    kaynak dosya ile çalışıyorsa global değişkenleri ya da fonksiyonları static yapmadan "external linkage"a sahip olarak yazabilir. Ancak büyük projelerde
    ya da çok sayıda kaynak dosya ile proje geliştirirken programcı başka bir modülden kullanılmayacak olan global değişkenleri ve fonksiyonları 
    static yapmalıdır. Bu sayede "isim kirliliğinin (name pollution)" önüne geçilebilir. Eğer büyük projelerde başka bir modül tarafından kullanılmayacağı halde
    bir global değişken ya da fonksiyon static yapılmazsa tesadüfen başka bir modülde aynı isimli bir global değişken ya da fonksiyon olabilir ve bu durum 
    link aşamasında soruna yol açabilir. 

    static fonksiyonların prototiplerinin başlık dosyalarına (header files) yerleştirilmesi anlamsızdır. Çünkü başlık dosyaları birden fazla kaynak dosyadan 
    include edilmek üzere oluşturulurlar. static fonksiyonlar zaten tek bir modülde kullanılabilmektedir.  
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yerel değişkenin başına static belirleyicisi getirilirse böyle değişkenlere "static yerel değişkenler" denir. Buradaki static belirleyicisinin
    global değişkenler ya da fonksiyonlardaki static belirleyicisi ile anlam bakımından hiçbir benzerliği yoktur. Bir yerel değişken static yapılırsa
    bu yerel değişken statik ömürlü olur. Yani program yüklendiğinde yaratılır, program sonlanana kadar bellekte kalır. C'de static yerel değişkenlere verilen ilkdeğerler
    sabit ifadesi olmak zorundadır. Çünkü bu ilkdeğerler derleme aşamasında derleyici tarafından hesaplanıp çalştırılabilen dosyaya yerleştirilmektedir. 
    Program çalışırken static yerel değişkenlere verilen ilkdeğerler artık etki göstermez. Örneğin:

    void foo(void)
    {
        static int i = 10;

        ++i;

        printf("%d\n", i);
    }

    Bir static terel değişkene verilen ilkdeğerler fonksiyonun her çağrılmasında o değişkene atanmamaktadır. Derleyiciler bu ilkdeğerleri derleme aşamasında 
    ele almaktadır. Programın çalışma zamanı sırasında bu ilkdeğerler adeta koddan kaldırılmaktadır.

    Burada foo çağrılsa da çağrılmasa da yerel i değişkeni static düzeyde program yüklendiğinde yaratılmış olacaktır. Bu fonksiyon çağrılıp sonlansa bile i
    değişkeni değerini koruyacaktır. Çünkü static yerel değişkenler blok bittiğinde yok edilmezler. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void foo(void)
{
    static int i = 10;

    ++i;

    printf("%d\n", i);
}

int main(void)
{
    foo();		/* 11 */
    foo();		/* 12 */
    foo();		/* 13 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    static yerel değişkenlere ilkdeğer verilmeyebilir. Bu durumda onların içerisinde 0 değeri bulunur. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void foo(void)
{
    static int i;

    ++i;

    printf("%d\n", i);
}

int main(void)
{
    foo();      /* 1 */
    foo();      /* 2 */
    foo();      /* 3 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    static yerel değişkenlerle global değişkenlerin her ikisi de statik ömürlüdür. Yani hem static yerel değişkenler hem de global değişkenler program 
    belleğe yüklendiğinde yaratılırlar. Program sonlanana kadar bellekte kalırlar. Ancak global değişkenler her yerde kullanılabilirken (hatta extern yapılarak 
    başka bir modülden de kullanılabilir) yerel değişkenler yalnızca o blokta kullanılabilirler. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi C'de bir fonksiyonun yerel bir değişkenin ya da dizinin adresiyle geri dönmemesi gerekmektedir. Çünkü fonksiyon bittiğinde bu 
    yerel değişken ya da dizi bellekten boşaltılacağı için artık geri döndürülen adres ratgele bir adres gibi olmaktadır. Ancak bir fonksiyonun static yerel bir 
    değişkenin ya da dizinin adresiyle geri dönmesinde bir sakınca yoktur. Çünkü fonksiyondan çıkılsa bile static yerel değişkenler ve diziler yaşamaya
    devam ederler. Tabii bu tür durumlarda biz fonksiyonu her çağırdığımızda aynı adresi elde ederiz. Aşağıdaki örnekte bu durum gösterilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

char *getname()
{
    static char name[64];

    printf("Adi soyadi:");
    gets(name);

    return name;        /* tamamen normal ve geçerli bir durum, dizi static */
}

int main(void)
{
    char *name;

    name = getname();
    printf("%p, %s\n", name, name);

    name = getname();
    printf("%p, %s\n", name, name);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi static yerel değişkenlere ne zaman gereksinim duyulmaktadır? İşte biz fonksiyon bittiği halde bir değişkenin değerini korumasını isteyebiliriz. 
    Bu durumda static yerel değişkenleri kullanbiliriz. Global değişkenler de statik ömürlüdür. Ancak eğer değişken yalnızca bir fonksiyon içerisinde kullanılacaksa 
    onun gereksiz bir biçimde global olarak tanımlanması kötü teknik olur. Tabii bir gerekçe yoksa static yerel değişken kullanmak da kötü bir tekniktir. 
    static yerel değişkenler sürekli yer kaplamaya devam ederler. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Adrese geri dönen bir fonksiyon için üç durum söz konusu olabilir:

    1) Fonksiyon bizim argüman olarak verdiğimiz adresin aynısına geri dönüyor olabilir. Bizim ona verdiğimiz nesne fonksiyonun çıkışında da yaşadığına göre bir sorun 
    yoktur. Örneğin:

    #include <stdio.h>

    char *revstr(char *str)
    {
        int n;
        char ch;

        for (n = 0; str[n] != '\0'; ++n)
            ;

        for (int k = 0; k < n / 2; ++k) {
            ch = str[k];
            str[k] = str[n - k - 1];
            str[n - k - 1] = ch;
        }

        return str;

    }

    int main(void)
    {
        char s[] = "ankara";
        char *str;

        str = revstr(s);

        puts(s);
        puts(str);

        return 0;
    }


    Burada revstr zaten bizim verdiğimiz adrese geri dönmektedir. Kodda herhangi bir tanısmsız davranış yoktur. 

    2) Fonksiyon dinamik bir biçimde tahsis edilen bir alanın adresiyle geri dönüyor olabilir. Bu durumda fonksiyon sonlansa bile dinamik alan yaşamaya devam edeceğine göre
    burada da bir sorun yoktur. Ancak bu durumda alanın free getirilmesi fonksiyonu çağıranın sorumluluğunda olur. Örneğin:

    #include <stdio.h>
    #include <stdlib.h>
    #include <string.h>

    char *getname(void)
    {
        char buf[1024];
        char *name;

        printf("Adi soyadi:");
        gets(buf);

        if ((name = (char *)malloc(strlen(buf) + 1)) == NULL)
            return NULL;

        strcpy(name, buf);

        return name;
    }

    int main(void)
    {
        char *name;

        if ((name = getname()) == NULL) {
            fprintf(stderr, "cannot allocate memory!..\n");
            exit(EXIT_FAILURE);
        }

        puts(name);

        free(name);

        return 0;
    }

    Buradaki getame fonksinu her çağrıldığında bize farklı bir adres verecektir. 

    3) Fonksiyon static yerel bir nesnenin ya da dizinin adresiyle geri dönmektedir. Bu durumda fonksiyonu her çağırdığımızda bize hep aynı adresi verecektir. Örneğin:

    #include <stdio.h>

    char *getname()
    {
        static char name[64];

        printf("Adi soyadi:");
        gets(name);

        return name;        /* tamamen normal ve geçerli bir durum, dizi static */
    }

    int main(void)
    {
        char *name;

        name = getname();
        printf("%p, %s\n", name, name);

        name = getname();
        printf("%p, %s\n", name, name);

        return 0;
    }

    static yerel nesnelerin adresleriyle geri dönmek çok thread'li uygulamalarda fonksiyonun "thread güvenli (thread safe)" olmamamasına yol açmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    static yerel nesnelerin adresleriyle geri dönen fonksiyonlara iyi bir örnek "localtime" isimli standart C fonksiyonudur. Fonksiyonun prototipi <time.h>
    dosyası içerisindedir:

    struct tm *localtime(const time_t *pt);

    Burada const anahtar sözcüğü izleyen bölümlerde ele alınmaktadır. Fonksiyon bizden time fonksiyonundan elde edilmiş olan değerin bulunduğu nesnenin 
    adresini alır. Pek çok sistemde time fonksiyonu 01/01/1970'ten geçen saniye sayısını vermektedir. localtime fonksiyonu bilgisayarın saatine bakmaz. 
    time fonksiyonu bilgisayarın saatine bakmaktadır. localtime fonksiyonu bu saniye sayısını yıl, ay, gün, saat, dakika, saniye bileşenlerine ayırır. 
    struct tm isimli static bir yapı nesnesinin içerisine yerleştirir ve bu static yapı nesnesinin adresiyle geri döner. struct tm yapısı da <time.h> 
    dosyası içerisinde aşağıdaki gibi bildirilmiştir:

    struct tm {
        int tm_sec;         /* seconds */
        int tm_min;         /* minutes */
        int tm_hour;        /* hours */
        int tm_mday;        /* day of the month */
        int tm_mon;         /* month */
        int tm_year;        /* year */
        int tm_wday;        /* day of the week */
        int tm_yday;        /* day in the year */
        int tm_isdst;       /* daylight saving time */
    };

    Yapının tm_sec, tm_min ve tm_hour elemanları zaman bilgisine ilişkindir. tm_mday elemanı ayın kaçıncı günü olduğunu belirtir. tm_mon elemanı 
    0 orijinlidir ve ayı belirtmektedir. tm_year elemanı 1900 orijinli olarak yılı belirtmektedir. tm_wday tarihin haftanın kaçıcncı günü olduğunu belirtir. 
    Burada 0 = Pazar anlamına gelmektedir. tm_yday elemanı tarihin 0 orijinli olarak o yılın kaçıncı günü olduğunu belirtmektedir. tm_isdst elemanı pozitif bir değerdeyse 
    tarih "ileri saat uygulamasının içerisinde" kalmaktadır. 0 ise "ileri saat uygulamasının içerisinde" kalmamaktadır. Negatif ise bu konuda bir bilgi yoktur. 
    localtime fonksiyonu geçersiz bir parametre için NULL adrese geri dönmektedir. 

    Aşağıdaki örnekte önce time fonksiyonu ile epoch'tan (01/01/1970) geçen saniye sayısı bulunup localtime fonksiyonu ile tarih zaman bilgisine dönüştürülmüştür. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t t;
    struct tm *pt;
    char *days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

    t = time(NULL);
    pt = localtime(&t);

    printf("%02d/%02d/%04d %02d:%02d:%02d - %s\n", pt->tm_mday, pt->tm_mon + 1, pt->tm_year + 1900, pt->tm_hour, pt->tm_min, pt->tm_sec, days[pt->tm_wday]);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                                61. Ders - 12/01/2023 Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    ctime isimli standart C fonksiyonu epoch orijininden geçen saniye sayısının bulunduğu nesnesin adresini alarak static yerel char türden bir dizinin adresiyle 
    geri dönmektedir. Bu dizi de ilgili tarih ve zamanın yazısal karşılığı bulunmaktadır. Fonksiyonun prototipi şöyledir:

    char *ctime(const time_t *pt);

    Fonksiyonun geri döndürdüğü yazı şu formattadır:

    Thu Jan 12 20:30:45 2023

    Gün ve ay bilgisi İngilizce ve üçer karakterdir. 

    Aşağıda fonksiyonun kullanımına yönelik bir örnek verilmiştir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t t;
    char *str;

    t = time(NULL);
    str = ctime(&t);
    puts(str);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    asctime isimli standart C fonksiyonu ctime fonksiyonun yaptığının aynısını yapmaktadır. Ancak parametre olarak struct tm yapısı türünden bir adres 
    alır. Prototipi şöyledir:

    char *asctime(const struct tm *tm);

    asctime fonksiyonu ctime(localtime(&t)) ile tamamen eşdeğerdir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t t;
    char *str;

    t = time(NULL);
    str = asctime(localtime(&t));
    puts(str);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    mktime isimli standart C fonksiyonu localtime fonksiyonunun ters işlemini yapmaktadır. Yani fonksiyon bizden bir struct tm yapı nesnesinin adresini alır. 
    Bize epoch'tan geçen saniye sayısını (bunun saniye sayısı olması standartlarda garanti edilmemiştir) verir. Fonksiyonun prototipi şöyledir:

    time_t mktime(struct tm *tm);

    time fonksiyonu bilgisayarın saatine bakarak o anki zamana ilişkin epoch'tan geçen değeri bize verir. (Biz belli bir zamana ilişkin epoch'tan geçen değeri 
    elde etmek isteyebiliriz. Örneğin 01/01/1990'a ilişkin epoch'tan geçen saniye sayısını elde etmek isteyebiliriz.)

    Fonksiyon başarısızlık durumunda -1 değerine geri dönmektedir. 

    mktime fonksiyonunu çağırırken biz struct tm yapısının tm_wday ve tm_yday elemanlarını doldurmayız. Fonksiyon çıkışta bu elemanları diğer elemanlardan harketele
    doldurmaktadır. struct tm yapısının tm_isdst elemanına biz pozitif bir değer geçersek ilgili tarihte ileri saat uygulamasının olduğu anlamına gelir. 0 değeri 
    olmadığı anlamına gelmektedir negatif bir değer ise fonksiyonun bu belirlemeyi kendisinin yapacağını belirtmektedir. 

    Yapının tm_year elemanı 1900'den itibaren yıl belirtmesi gerekir. tm_mon elemanında yine Ocak 0'dan başlatılmaktadır. 
    mktime fonksiyonu normalizasyon yapmaktadır. Yani biz struct tm yapısına büyük değerler girebiliriz. Bu durum fonksiyon 
    tarafından ele alınabilmektedir. (Örneğin yapının tm_min elemanına 500 gibi bir değer girsek bu durumda bu değer 60'a 
    bölünüp fazlalık yapının tm_hour kısmına aktarılacaktır.)

    Aşağıdaki mktime fonksiyonun kullanımına bir örnek verilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t t;
    struct tm tm = {0};
    struct tm *pt;

    char *days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};

    tm.tm_year = 80;
    tm.tm_mon = 0;
    tm.tm_mday = 1;
    tm.tm_isdst = -1;

    t = mktime(&tm);
    pt = localtime(&t);

    printf("%02d/%02d/%04d %02d:%02d:%02d - %s\n", pt->tm_mday, pt->tm_mon + 1, pt->tm_year + 1900, pt->tm_hour, pt->tm_min, pt->tm_sec, days[pt->tm_wday]);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Örneğin biz mktime fonksiyonu ile belli bir tarihten itibaren belli bir zaman ileriyi ya da geriyi elde edebiliriz. Ya da örneğin iki tarih arasında geçen süreyi de 
    yine bu sayade elde edebiliriz. Örneğin 17 Ağustos 1999 depreminden (03:02) ne kadar süre geçtiğini hesaplamaya çalışalım. Bunu şöyle yapabiliriz:

    time_t now, eqwk, result;
    struct tm tm = {0};
    struct tm *pt;

    tm.tm_year = 99;
    tm.tm_mon = 7;
    tm.tm_mday = 17;
    tm.tm_isdst = -1;
    tm.tm_hour = 3;
    tm.tm_min = 2;
    tm.tm_sec = 0;

    now = time(NULL);
    eqwk = mktime(&tm);
    result = now - eqwk;
    
    pt = localtime(&result);
    
    printf("%d yıl, %d ay, %d gün, %d saat, %d dakika, %d saniye\n", pt->tm_year - 70, pt->tm_mon + 1, pt->tm_mday, pt->tm_hour, pt->tm_min, pt->tm_sec);

    Tabii epoch orijini C'de standart bir biçimde belirlenmemiştir. Ancak UNIX/Linux sistemlerinde 01/01/1970 biçiminde 
    standart bir belirleme yapımıştır. Dolayısıyla kodun C standartları bağlamında taşınabilirliği yoktur.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <time.h>

int main(void)
{
    time_t now, eqwk, result;
    struct tm tm = {0};
    struct tm *pt;

    tm.tm_year = 99;
    tm.tm_mon = 7;
    tm.tm_mday = 17;
    tm.tm_isdst = -1;
    tm.tm_hour = 3;
    tm.tm_min = 2;
    tm.tm_sec = 0;

    now = time(NULL);
    eqwk = mktime(&tm);
    result = now - eqwk;
    
    pt = localtime(&result);

    printf("%d yıl, %d ay, %d gün, %d saat, %d dakika, %d saniye\n", pt->tm_year - 70, pt->tm_mon + 1, pt->tm_mday, 
        pt->tm_hour, pt->tm_min, pt->tm_sec);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    time_t türünün saniye belirtmesi ve dolayısıyla da iki time_t değerinin çıkartılması sonucunda elde edilen değerin 
    saniye belirtmesi standartlarca garanti edilmemiştir. Bu nedenle eğer iki time_t değeri arasındaki zaman farklı 
    saniye cinsindne bulunacaksa difftime fonksiyonu tercih edilmelidir. Fonksiyonun prototipi şöyledir:

    double difftime(time_t time1, time_t time0);

    Fonksiyon iki time_t değerinin farkını o sistemdeki epoch birimi dikkate alarak hesaplamaktadır. 

    Ancak gerek Windows sistemlerinde gerekse UNIX/Linux sistemlerinde time fonksiyonu 01/01/1970'den geçen saniye 
    sayısını vermektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    strftime isimli standart C fonksiyonu adeta sprintf fonksiyonunun tarih ve zaman için tasarlanmış biçimi gibidir. Bu fonksiyon belli format karakterlerine
    uygun olarak struct tm içerisindeki tarih bilgisini bir yazı biçiminde programcının verdiği adreste oluşturmaktadır. Fonksiyonun prototipi şöyledir:

    size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);

    Fonksiyonun birinci parametresi yazının yerleştirileceği dizinin bşlangıç adresini belirtir. İkinci parametre bu dizinin uzunluğunu belirtmektedir. 
    Fonksiyon diiyi taşırmamak için bu ikinci parametreyi almaktadır. Üçücncü parametresi format karakterlerinin bulunduduğu yazının adresini belirtir. 
    Bu parametre genellikle string biçiminde oluşturulmaktadır. Son parametre ilgili tarih ve zamanı belirten struct tm türünden yapının adresini belirtir. 
    Format karakterlerinin listesini ilgili dokümanlardan elde edebebilirsiniz. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısına geri dönmektedir. 
    Bu sayıya null karakter dahil değildir. Fonksiyon eğer ilgili yazı ikinci parametresiyle belirtilen uzunluğu aşarsa 0 ile geri dönmektedir. Bu duurmda dizinin
    içerisinde ne olacağının bir garantisi yoktur. 

    Aşağıdaki örnekte tarih zaman bilgisi ctime formatında ekrana yazdırılmıştır.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <time.h>

int main(void)
{
    char buf[4096];
    time_t t;
    struct tm *pt;

    t = time(NULL);
    pt = localtime(&t);

    strftime(buf, 4096, "%a %b %d %H:%M:%S %Y", pt);

    puts(buf);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                        62. Ders - 17/01/2023 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi C'de bildirimde kullanılabilen iki "tür niteleyici (type qualifier)" anahtar sözcük vardı: const ve volatile. Bu bölümde bu iki niteleyici
    üzerinde duracağız. Bu iki anahtar sözcüğe "tür niteleyicisi" denilmesinin nedeni bunların tür bilgisini değiştirmesindedendir. Yani örneğin int türü ile
    const int türü uyumlu olsa da farklı türlerdir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir nesne const anahtar sözcüğü kullanılarak tanımlanmışsa o nesneye ilkdeğer vermenin dışında bir daha atanamaz. Yani const 
    nesneleri tanımlarken ilkdeğer verebiliriz ancak onlara daha sonra değer atayamayız. Örneğin:

    const int a = 10;		/* geçerli */
    ...
    a = 20;					/* geçersiz! */

    const bir nesne tipik olarak ilkdeğer verilerek tanımlanır. Nesnenin ilkdeğer verilmeden tanımlanması geçerli olsa da anlamsızdır. Örneğin:
    
    {
        const int a;		/* geçerli ama anlamsız! */
        ...
    }

    C++'ta const nesnelere ilkdeğer verilmesi zorunlu tutulmuştur. Yukarıda da belirttiğimiz gibi Tür niteleyicileri türün bilgisinin bir paröasını 
    oluşturmaktadır. Yani bu haliyle const niteleyicisi dekleratöre ilişkin değil türe ilişkindir. Örneğin:

    const int a = 10, b = 20, c = 30;

    Burada a, b ve c nesnelerinin hepsi const nesnelerdir. Bu nesnelerin hepsi const int türündendir. Ancak const int türü int türü tamamen birbirleriyle 
    uyumlu türleridr. Yani int türü ile yapılabilen her işlem (atama dışında) const int türüyle de yapılabilmektedir. Her ne kadar int türü ile const int 
    türü farklı türler belirtiyorsa da bunları aynı türden kabul edebiliriz. Çünkü birinin kullanıldığı yerde diğeri de kullanılabilmektedir. 
    İstisna durumlar izleyen paragraflarda ele alınmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Tabii daha önceden de belirttiğimiz gibi bildirimde tür belirleyicisi ile tür niteleyicileri farklı sıralarda belirtilebilirler. Örneğin:

    const int a = 10;

    ile,

    int const a = 10;

    tamamen eşdeğerdir. 

    Bir dizi const yapılırsa dizinin tüm elemanları const yapılmış olur. Örneğin:

    const int a[] = {1, 2, 3, ,4, 5};

    a[2] = /* geçersiz! */

    Bir yapı nesnesi const yapılırsa o nesnenin tüm paraçaları const yapılmış olur. Örneğin:

    struct DATE {
        int day;
        int month;
        int year;  
    };
    ...
    const struct DATE date = {10, 12, 2007};      /* geçerli */
    
    date.month = 10;        					  /* geçersiz! nesnenin tüm parçaları const */
   
    Yapı bildiriminde yapının yalnızca bazı elemanları const yapılabilir. Bu durumda nesneyi tanımlarken const anahtar sözcüğü kullanılmasa bile 
    o elemanlar yine const olur. Ancak böyle bir kullanım amaç dışı ve çoğu kez anlamsız olduğu için programcılar tarafından kullanılmamaktadır. Örneğin:

    struct DATE {
        int day;
        const int month;
        int year;  
    };
    ...
    struct DATE date = {10, 12, 2007};      /* geçerli */
        
    date.day = 21;          /* geçerli */
    date.year = 2009;       /* geçerli */
    date.month = 7;         /* geçersiz! */
    
    C standartlarına göre aynı tür niteleyicisi bildirimde birden fazla kez belirtilse bile bu durum geçerlidir. Yalnızca bir kez belirtilmiş gibi 
    işlem görür. Örneğin:

    const int const a = 10;		/* geçerli ama anlamsız */
    
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de const değişkenler sabit ifadeleri gereken yerlerde kullanılamazlar. Örneğin:

    const int g_size = 10;
    int g_a[g_size];		/* geçersiz! */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    const nesnelerin kullanılmasının üç nedeni vardır:

    1) const nesneler okunabilirliği artırmaktadır. Bir nesnenin const olduğunu geren kişi onun içerisindeki değerin bir daha değiştirilmeyeceğini anlar
    ve kodu daha iyi anlamlandırır. Örneğin:

    const int number_of_students = 121;

    Burada öğrenci sayısının program içerisinde değiştirilmeyeceği anlaşılmaktadır.

    2) const nesneler kullanıldığında derleyiciler duruma göre daha iyi bir optimizasyon yapabilmektedir. Bunun nedeni derleyicilerin const nesnelerin
    değiştirilmeyeceğini bilmesinden kaynaklanmaktadır. Örneğin derleyici const bir nesneyi bir yazmaca çemişse bir süre sonra onun değerinin dolaylı bir 
    biçimde bile değiştirilmeyeceğini bildiği için nesneyi yeniden yazmaca yüklemez.

    3) const nesneler yanlışlıkla programcı tarafından değiştirilme durumlarında derleme zamanında bu yanlışlığın ortaya çıkartılmasını da sağlamaktadır. 
    Yani aslında dğeiştirilemeyecek bir nesneyi programcı yanlışlıkla değiştirirse hata derleme aşamasında ortaya çıkacaktır.

    Ancak const tür niteleyicisi aslında daha çok göstericilerle birlikte kullanılmaktadır.İzleyen paragraflarda const
    niteleyicisinin göstericilerle kullanımı ele alınmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir gösterici belirtildiğinde const olma durumu üç biçimde kendini göstermektedir. Örneğin:

    int *pi;

    Burada pi göstericisi bir nesnedir, int türden bir adres bilgisi tutmaktadır. Öte yandan pi göstericisinin gösterdiği yere *pi ya da pi[n] ifadeleriyle 
    de erişebilmekteyiz. Yani pi göstericisinin gösterdiği yerler de birer nesnedir. İşte pi'nin kendisinin mi yoksa gösterdiği yerin mi yoksa hem kendisinin hem de 
    gösterdiği yerin mi const olduğu önemlidir. const göstericiler üçe ayrılmaktadır:

    1) Kendisi değil gösterdiği yer const olan const göstericiler
    2) Gösterdiği yer değil kendisi const olan const göstericiler
    3) Hem kendisi hem de gösterdiği yer const olan const göstericiler

    En çok kullanılan const göstericiler "kendisi değil gösterdiği yer const olan const göstericilerdir". Biz önce bu üçü türü de tanıtıp daha sonra "kendisi 
    değil gösterdiği yer const olan const göstericiler" üzerinde biraz daha detaylı bir biçimde duracağız. Halk arasında "const gösterici" denildiğinde
    default olarak "kendisi değil gösterdiği yer const olan const göstericler" anlaşılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    const göstericinin nasıl bir const gösterici olduğu const anahtar sözcüğünün nereye getirildiği ile ilgilidir. 

    1) Eğer const anahtar sözcüğü dekleratörle değil türle ilgili kullanılmışsa bu "kendisi değil gösterdiği yer const olan" const gösterici anlamına gelmektedir. 
    Yani burada const anahtar sözcüğü *p ifadesinin soluna getirilmiş durumdadır. Örneğin:

    const int *p;

    Burada p nesnesi const değildir. Biz p'nin içerisine istediğimiz zaman bir adres yerleştirebiliriz. Burada const olan p'nin gösterdiği yer, yani *p nesnesidir. 
    Örneğin:

    int a[100];
    const int *p;
    
    p = &a[0];    /* geçerli p const değil */
    p = &a[1]     /* geçerli, p const değil */
    
    *p = 100;      /* geçersiz! *p const bir nesne */
    p[20] = 100;   /* geçersiz! derleme aşamasından geçilemez! */
    
    Burada dikkat edilmesi gereken bir nokta şudur. Örneğimizde p değil *p const durumdadır. Ancak bir göstericinin gösterdiği yerin const olması demek 
    * operatörü ile tam gösterdiği yerin const olması demek değildir. Bu gösterici ile erişilen her yer const durumdadır. Yani biz p göstericisi ile 
    nereye erişirsek erişelim orası const durumdadır. Örneğin:

    const int *p;

    Bu tanımlamada p'nin kendisi değil gösterdiği yer const durumdadır. Yani biz p'ye herhangi bir adres atayabiliriz. Ancak p'ye hangi adresi 
    atamış olursak olalım onun gösterdiği yeri değiştiremeyiz. Burada mademki p'nin kendisi const değildir. O zaman p'ye ilkdeğer vermeye gerek yoktur.

    Gösterdiği yer const olan const göstericinin türü belirtilirken bu const olma durumu da belirtilir. Örneğin:

    const char *s;
    
    Burada s "const char *" türündendir. "char *" türünden değildir. "const char *" türü demek "gösterdiği yer const olan char türden bir adres türü" demektir. 

    Kendisi değil gösteridiği yer const olan const göstericilerde const anahtar sözcüğünün başa gelmesi zorunlu değildir. Önemli olan *'ın soluna gelmesidir. 
    Örneğin:

    const int *p;

    ile,

    int const *p;

    tamamen eşdeğeridir. 

    Tabii gösterdiği yer const olan const gösterici bildirilirken birden fazla dekleratör bulundurulabilir. Örneğin:

    const int *p, a = 10;

    Burada p göstericisinin kendisi değil gösterdiği const durumdadır. Ancak a const bir nesnedir. 

    2) Eğer bildirimde const anahtar sözcüğü *'ın soluna değil sağına getirilirse, yani const anahtar sözcüğü tür ile değil gösterici dekleratörü ile 
    ilişkilendirilirse bu durumda "gösterdiği yer değil kendisi const olan" const bir gösterici oluşturulmuş olur. Örneğin:

    int a;
    int * const p = &a; 

    Burada p const bir nesnedir. Dolayısıyla biz ilkdeğer verdikten sonra p'ye başka bir adres atayamayız. Anck pi'nin gösterdiği yeri değiştirebiliriz. 
    Örneğin:

    int a, b;
    int * const p = &a;
    
    *p = 10;            /* geçerli, *p const değil! */
    printf("%d\n", a);   /* 10 */

    p = &b;            /* geçersiz! p const  

    Şimdi bildirimde başka dekleratörler de bulunduralım:

    int * const p = &a, x, y;

    Burada p const bir nesnedir ancak x ve y const nesneler değildir. Gösterdiği yer değil kendisi const olan const göstericilerde tür bilgisi belirtilirken 
    yine const anahtar sözcüğü belirtilir ancak *'dan sonraya getirilir. Örneğin:

    int * const p = &a;

    Burada p "int * const" türündendir. 

    3) Eğer const anahtar sözcüğü hem *'ın soluna hem de sağına getirilirse bu durumda "hem kendisi hem de gösterdiği yer const olan" const gösterici 
    oluşturulmuş olur. Örneğin:

    int a;
    const int * const p = &a;

    Burada artık biz p'ye de onun gösterdiği yere de atama yapamayız. Şimdi bildirimde birden fazla dekleratör bulunduralım:

    int a;
    const int * const p = &a, b = 20;

    Burada p "hem kendisi hem de gösterdiği yer const olan" const bir göstericidir. b de const bir nesnedir. Böyle göstericilerde tür bilgisi belirtilirken 
    her iki const anahtar sçzcüğü de belirtilir.

    int a;
    const int * const p = &a;

    Burada p "const int * const" türündedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi const bir nesnenin adresini bir göstericiye yerleştirip bu gösterici yoluyla const nesneyi değiştirebilir miyiz? Örneğin:

    const int a = 10;
    int *pi;

    pi = &a;			/* buraya dikkat! */
    *pi = 20;

    Burada aslında çaktırmadan a değiştirilmeye çalışılmıştır. Dolayısıyla programcı derleyiciye verdiği sözü kuralların arkasından dolaşarak ihlal etmektedir. 
    Fakat örneğin:

    int a = 10;
    const int *pi;

    pi = &a;

    Burada bir kötüye kullanım yoktur. Çüğkü a zaten const değildir. İşte birinci örnekteki kötüye kullanımı ortadan kaldırmak için C'de otomatik dönüştürmeye 
    yönelik şöyle bir kural bulunmaktadır: "T bir tür belirtmek üzere const T * türünden T * türüne otomatik dönüştürme yoktur. Böylece biz const bir nesnenin 
    adresini gösterdiği yer const olmayan bir göstericiye zaten atayamayız. Örneğin:

    const int a = 10;
    int *pi;

    pi = &a;		/* geçersiz! */
    *pi = 20;

    Tabii "const T *" türünden "const T *" türüne otomatik dönüştürme vardır. Zaten bunlar aynı türlerdir. Yani biz const bir nesnenin adresini 
    gösterdiği yer const olan bir göstericiye atayabiliriz. Örneğin:

    const int a = 10;
    const int *pi;

    pi = &a;	

    Burada atamanın iki tarafının türü de "const int *" biçimindedir. Bu atamada bir kötüye kullanım yoktur. Çünkü zaten pi ile artık onun gösterdiği 
    yer değiştirilemeyecektir. Eğer biz pi'yi kullanarak onun gösterdiği yeri değiştirmeye çalışsak derleme zamanında error ile karşılaşırız.

    Tabii biz const bir nesnenin adresini kendisi const olan bir gösteriye yine atayamayız. Yani "const T *" türünden "T * const" türüne otomatik 
    dönüştürme yoktur. Burada önemli olan göstericinin gösterdiği yerin const olmasıdır:

    const int a = 10;
    int * const pi = &a;		/* geçersiz! */

    Burada pi'nin gösterdiği yer const olmadığı için bu durum kötüye kullanıma açıktır. 

    Tabii const olmayan bir nesnenin adresinin, gösterdiği yer const olan bir göstericiye atanmasında bir kötüye kullanım yoktur. Dolayısıla "T *" türünden
    "const T *" türüne otomatik dönüştürme vardır. Örneğin:

    int a = 10;
    const int *pi;

    pi = &a;		/* geçerli, kötüye kullanım yok */

    Tabii biz burada pi göstericisini kullanarak yine onun gösterdiği yeri değiştiremeyiz. 

    Burada bir noktaya dikkatinizi çekmek istiyoruz. const T * türünden bir göstericiye hem T * türünden hem de const T * 
    türünden adresler atanabilmektedir. Örneğin:

    int a = 10;
    const int b = 20;
    const int *pi;

    pi = &a;		/* geçerli, "const T * = T *" */
    pi = &b;		/* geçerli, "const T * = const T *" */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    const bir dizinin ismi ifade içerisinde kullanıdığında gösterdiği yer const olan bir adres belirtmektedir. Örneğin:

    const int a[] = {1, 2, 3, 4, 5};

    Burada a ifadesi "const int *" türündedir. Dolayısıyla aşağıdaki işlemler geçersizdir:

    *a = 10;		/* geçersiz! */
    a[2] = 20;		/* geçersiz */

    const T * türünden T * türüne otomatik dönüştürme olmadığını anımsayınız:

    int *pi;

    pi = a;			/* geçersiz! */

    Burada const int * türünden adres int * türünden bir göstericiye atanmak istenmiştir. Tabii burada a adresini biz 
    gösterdiği yer const olan bir göstericiye atayabiliriz:

    const int a[] = {1, 2, 3, 4, 5};
    const int *pi;

    pi = a;		/* geçerli */

    Fakat artık bu gösterici yoluyla da dizi elemanlarını değiştiremeyiz. ""

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Her ne kadar const T * türünden T * türüne otomatik dönüştürme yoksa da biz tür dönüştürme operatörü ile bu 
    dönüştürmeyi yapabiliriz. Örneğin:

    const int a = 10;
    int *pi;

    pi = &a;		/* geçersiz! */

    Bu atama geçersizdir. Çünkü burada const T * türünden T * türüne otomatik dönüştürme yapılmak istenmiştir. Ancak 
    eğer bu dönüştürmeyi yapmak istiyorsak tür dönüştürme operatörünü kullanarak yapabiliriz:

    pi = (int *)&a;		/* geçerli */

    Burada artık const int * türü tür dönüştürme operatörüyle int * türüne dönüştürülmüştür. Bu dönüştürme tamamen geçerlidir. 
    Bu tür dönüştürmelere "const'luğu atan dönüştürmeler (const away cast)" de denilmektedir. Tabii biz T * türünden 
    tür dönüştürme operatörüyle const T * türüne de dönüştürme uygulayabiliriz. Ancak bu gereksizdir çünkü zaten T * 
    türünden const T * türüne otomatik dönüştürme vardır. Örneğin:

    int a = 10;
    const int *pi;

    pi = (const int *)&a;			/* geçerli */

    Burada zaten böylesi bir dönüştürmeye gerek yoktur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C standartlarına göre const bir nesnenin herhangi bir yolla (tipik olarak ir gösterici yoluyla) değerinin değiştirilmesi
    tanımsız davranışa (undefined behavior) yol açmaktadır. Örneğin:

    const int a = 10;
    int *pi;

    pi = (int *)&a;		/* geçerli */
    *pi = 20;			/* geçerli, ama tanımsız davranış */

    Burada C'ce geçersiz hiçbir nokta yoktur. Ancak sonuçta const bir nesne güncellenmiştir. İşte bu durum tanımsız 
    davranışa yol açmaktadır. Buradaki tanımsız davranış ağaıdaki deyimde ortaya çıkmaktadır:

    *pi = 20;			/* geçerli, ama tanımsız davranış */

    Gerçekten de pek çok derleyici (Micrsoft C derleyicisi, gcc ve clang derleyicileri) örneğin global const nesneleri 
    const bir bellek alanında tutmaktadır. Bu bellek alanına yazma yapılmak istendeiğinde program çökebilmektedir. 

    Tabii aklınıza "madem const bir nesnenin gösterici yoluyla değiştirilmesi tanımsız davranışa yol açıyor, bu durumda 
    const bir nesnenin adresinin tür döüştürmesi operatöryle const olmayan bir göstericiye atanmasının ne anlamı var"
    sorusu gelebilir. Bazen aslında const olmayan bir nesnenin adresi gösterdiği yer const olan bir gösteriye atanmış 
    olabilir. Sonra bunu biz const olmayan bir göstericiye atamak isteyebiliriz. Nasıl olsa nesnenin orijinali const 
    değildir ve onun güncellenmesi bir soruna yol açmayacaktır. Yani gösterdiği yer const olan bir göstericinin gösterdiği 
    yerde const bir nesne olmak zorunda değildir. Örneğin:
    
    int a = 10;
    const int *pci;
    int *pi;

    pci = &a;

    /* .... */

    pi = (int *)pci;		/* geçerli */

    *pi = 20;				/* geçerli */

    printf("%d\n", a);
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    63. Ders - 24/01/2023 Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Daha önceden de belirttiğimiz gibi "const gösterici" denildiğinde default olarak "gösterdiği yer const olan const göstericiler" anlaşılmaktadır.
    const göstericilerin en fazla kullanıldığı yer fonksiyobn parametreleridir. const göstericilerin fonksiyonların parametre değişkenlerinde kullanılması 
    önemli bir durum oluşturmaktadır. 

    Bir fonksiyonun parametre değişkeninin const bir gösterici (gösterdiği yer const olan bir gösterici) olması "fonksiyonun bizden bir nesnenin
    ya da dizinin adresini alacağı, ancak adresini aldığı nesnede ya da dizide dir değişiklik yapmayacağı" anlamına gelmektedir. Örneğin:

    void foo(const int *pi);

    Burada foo fonksiyonu bizden int türden bir nesnenin adresini almaktadır. Ancak foo fonksiyonu bu nesne üzerinde değişiklik yapmayacaktır. Çünkü
    gösterdiği yer const olan gösterilerle o göstericilerin gösterdiği yerlerde değişiklik yapılamaz. Örneğin:

    int a = 10;

    foo(&a);

    Burada biz fonksiyonun ne yaptığını bilmesek bile onun adresini verdiğimiz nesne üzerinde değişiklik yapmayacağını onu yalnızca kullanacağını anlarız. 
    Fonksiyon çıkışında a'da bir değişiklik olmayacaktır. Örneğin:

    int getmax(const int *pi, size_t size);

    Burada getmax fonksiyonu int türden bir dizinin başlangıç adresini ve uzunluğunu almaktadır. Ancak fonksiyon dizi üzerinde değişiklik yapmayacaktır. 

    Bir fonksiyon yazarken eğer fonksiyonun parametresi bir gösterici ise ve programcı adresiyle aldığı nesne ya da dizi üzerinde değişiklik yapmayacaksa 
    kesinlikle onu const bir gösterici yapmalıdır. Nesne üzerinde üzerinde değişiklik yapmadığı halde göstericinin const yapılmaması kötü bir tekniktir. 
    Örneğin:

    size_t mystrlen(char *str);		/* kötü teknik, göstericinin const olması gerekirdi */

    Burada mystrlen bir yazının karakter uzunluğuna geri dönüyor olsun. Bu fonksiyonun parametresiyle aldığı yazıda bir değişiklik yapma iddiası 
    yoktur. O halde onun parametresinin const bir gösterici olması gerekirdi. Bu uygulama kötü bir tekniktir. Fonksiyonun şöyle tasarlanması gerekirdi:

    size_t mystrlen(const char *str);		/* doğru teknik */

    const anahtar sözcüğünün fonksiyon parametresi olan göstericilerde kararlı bir biçimde kullanılması artık "const olmayan gösterici parametrelerinin" 
    nesne ya da dizide üzerinde kesinlikle değişiklik yapacağı" anlamına gelir. Çünkü eğer fonksiyon adresiyle aldığı nesnede ya da dizide değişiklik 
    yapmayacak olsaydı onun parametresini const gösterici yapardı. Örneğin:

    void foo(int *pi, size_t size);

    Burada fonksiyon int türden bir dizinin başlangıç adresini ve uzunluğunu bizden parametre olarak almaktadır. Eğer fonksiyon bu dizide değişiklik 
    yapmayacak olsaydı programcı fonksiyonun parametresini const bir gösterici yapardı. Demek ki fonksiyon parametresiyle aldığı dizide değişiklik yapacaktır. 
    Görüldüğü gibi artık "const olmayan gösterici parametrelerinin adresini aldığı nesnede değişiklik yapacağı" anlamı çıkmaktadır.

    Pekiyi fonksiyon parametrelerindeki göstericilerde const niteleyicisinin uygun kullanılmamasının nasıl bir dezavantajı olabilir? Örneğin:

    void myputs(char *str);

    Bu fonksiyon bizden adresini aldığı char bir dizinin karakterlerini yan yana null karakter görene kadar ekrana yazdırıyor olsun. Bu durumda fonksiyonun gösterici
    parametresinin const olması gerekirdi. Ancak programcı kötü bir teknik uygulayarak parametreyi const gösterici yapmamıştır. İşte bunun iki önemli 
    dezavantajı vardır:

    1) Fonksiyon aslında pek ala const bir dizi ile kullanılabilecekken artık kullanılamaz hale gelmiştir. Örneğin:

    const char s[] = "ankara";

    myputs(s);		/* geçersiz */

    Bu çağrı geçersizdir. Çünkü const bir dizinin adresi const olmayan bir göstericiye atanamaz. Halbuki fonksiyon aşağıdaki gibi tanımlanabilirdi:

    void myputs(const char *str);

    Artık biz bu fonksiyonu const bir diziyle de const olmayan bir diziyle de çağırabilirdik:

    const char s[] = "ankara";

    myputs(s);		/* geçerli */

    2) Böyle bir tasarım kodu ineleyen kişileri yanıltmaktadır. Örneğin:

    void myputs(char *str);

    Kodu inceleyen kişiler fonksiyonun parametresinin const olmayan gösterici olduüunu gördüklerinde onun adersi ile verilen dizide değişiklik yapacağını 
    sanırlar ve fonksiyonu anlamlandırmada zorluk çekerler. 
    
    O halde doğru teknik şudur: "Programcı parametresiyle aldığı adresteki nesneyi değiştirmeyecekse kesinlikle parametre değişkenini const gösterici 
    yapmalıdır, eğer parametresiyle aldığı adresteki nesneyi değiştirecekse parametre değişkenini const yapmamalıdır (zaten yapamaz da)".

    Şimdi parametre değişkeni olan göstericideki const olma durumu üzerinde çeşitli alıştırmalar yapalım.

    - strcpy fonksiyonunda birinci parametreye belirtilen hedef adrese ilişkin gösterici const olmamalıdır. Ancak ikinci parametreyle belirtilen 
    kaynak adrese ilişkin gösterici const olmalıdır. Gerçekten de fonksiyonun orijinal prototipi şöyledir:

    char *strcpy(char *dest, const char *source);

    - strchr fonksiyonu bizim adresini verdiğimiz yazıda karakter aramaktadır. Dolayısıyla fonksiyonun adresini verdiğimiz yazıda değişiklik yapma
    gibi bir niyeti yoktur. O halde fonksiyonunun gösterici parametresinin const olması gerekir:

    char *strchr(const char *str, int ch);

    - int bir diziyi sıraya dizen sort isimli bir fonksiyon yazılacak olsa dizinin başlangıç adresini alan göstericinin const olmaması gerekir. Örneğin:

    void sort(int *pi, size_t size);

    Çünkü fonksiyon dizide değişiklik yapmaktadır.

    -  gets fonksiyonu klavyeden (stdin dosyasından) alınan karakterleri char türden bir diziye yerleştirip sonuna null karakteri de eklemektedir. 
    Bu durumda gets fonksiyonunun parametresinin const olmayan bir gösterici olması gerekir:

    char *gets(char *str);

    - Bir yazıyı ters çeviren strrev isimli bir fonksiyon bulunuyor olsun. Fonksiyonun parametresi const bir gösterici olamaz:

    char *strrev(char *str);

    - double bir dizinin ortalaması ile geri dönen mean isimli bir fonksiyon bulunuyor olsun. Fonksiyonun parametresi const gösterici olmalıdır. 
    Örneğin:

    double mean(const double *pd, size_t size);

    Yapılardaki göstericilerin const olup olmaması önemli bir okunabilirlik sağlamaktadır. Eğer fonksiyonun yapı gösterici parametresi const ise 
    fonksiyon bizim verdiğimiz yapının içerisindeki kullanır ancak orada değişiklik yapmaz. Eğer fonksiyonun yapı gösterici parametresi const değilse
    bu durumda fonksiyon bizden aldığı yapı nesnesinin içini dolduracaktır. Örneğin sistem tarihini alan ve değiştiren iki fonksiyon olsun. 
    Bu fonksiyonlar struct DATE isimli bir yapıyı kullanıyor olsunlar. Fonksiyonların prototipleri şöyle olmalıdır:

    void getdate(struct DATE *pdate);
    void setdate(const DATE *pdate);

    Burada getdate fonksiyonuna biz bir strcut DATE nesnesi veririz. getdate fonksiyonu da bilgisayarın tarihine bakarak tarihi bizim verdiğimiz 
    yapı nesnesine yerleştirir. Görüldüğü gibi fonksiyon bizim adresini verdiğimiz yapı nesnesinde değişiklik yapmaktadır. Halbuki setdate fonksiyonu
    adresiyle verdiğimiz yapı nesnesinin içindekileri kullanarak bilgisayarın tarihini değiştirmektedir. O halşde getdate fonksiyonun parametresi const gösterici 
    olmamalıdır, setdate fonksiyonun parametresi ise const gösterici olmalıdır. 

    Şimdi bir veritabanından isim ile arama yapan ve eğer kaydı bulursa onun bütün bilgilerini bizim verdiğimiz struct PERSON türünden bir 
    nesnenin içerisine yerleştiren findperson isimli fonksiyonun parametrelerini const'luk durumuna göre inceleyelim. Fopnksiyon aşağıdaki gibi 
    kullanılacaktır:

    struct PERSON per;
    ...

    if (!findperson("Kaan Aslan", &per)) 
        fprintf(stderr, "cannot find person!..\n";)
    else {
        printf("person found...\n);
        ...
    }

    Burada fonksiyonun bizden aldığı isim üzerinde bir değişiklik yapma niyeti yoktur. Ancak fonksiyon bizden aldığı struct PERSON nesnesinin içini
    doldurmak istemektedir. O halde fonksiyonun prototipi aşağıdaki gibi olabilir:

    bool findperson(const char *name, struct PERSON *per);

    get_sys_info isimli fonksiyon sistem hakkında çeşitli bilgileri (işlemci sayısı, RAM miktarı vs.) SYSINFO isimli bir yapıya yerleştiriyor olsun.
    Bu durumda fonksiyonun parametresi const gösterici olamaz:

    typedef struct tagSYSINFO {
        ....
    } SYSINFO;

    void get_sys_info(SYSINFO *si);
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    const niteleyicisi typedef edilmiş bir adres ile kullanıldığında her zaman nesnenin kendisini nitelemktedir. Örneğin:

    typedef struct tagPERSON {
        ...
    } PERSON, *PPER;

    const PPER per;			/* özel durum, burada per gösterdiği yer değil, kendisi const olan const gösterici */

    Burada per göstericinin gösterdiği yer const değildir. per göstericisinin kendisi const durumdadır. Bu özel ve istisna bir durumdur. Yani buradaki
    bildirimin eşdeğeri şöyledir:

    struct tagPERSON * const per;

    Biz burada göstericinin gösteridği yeri const yapacaksak gösterici typedef ismini değil yapı typedef ismini kullanmalıyız. Örneğin:

    const PERSON *per;

    Burada artık per gösteridiği yer const olan const bir göstericidir. Başka bir deyişle const niteleyicisi başa getirilmişse her zaman dekleratörü const 
    yapmaktadır. Ancak programcılar const göstericiler için de typedef yapmayı tercih edebilmektedir. Örneğin:

    typedef struct tagPERSON {
        ...
    } PERSON, *PPERSON;

    typedef const struct PERSON *PCPERSON;

    Burada artık PCPERSON ismi "const struct PERSON *" anlamına gelmektedir. Örneğin:

    void disp_person(PCPERSON per);

    Burada per artık göstweridği yer const olan const bir göstericidir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

typedef int *PI;

int main()
{
    int a = 10, b = 20;
    const PI pi = &a;

    pi = &b;     /* geçersiz! pi'nin kendisi const */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    const bir adres için typedef işlemi uygulamak yaygın bir durumdur.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

typedef const int *PCI;

int main()
{
    int a = 10, b = 20;
    PCI pi = &a;

    pi = &b;        /* geçerli, pi'nin gösterdiği yer const */
    printf("%d\n", *pi);
    *pi = 20;       /* geçersiz! pi'nin gösterdiği yer const! */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Gösterici olmayan parametre değişkenlerinin const olmasının okunabilirlik bakımından hiçbir anlamı yoktur. Örneğin:

    void foo(const int a);

    Burada a'nın const olup olmaması fonksiyonu yazanı ilgilendiren kullanını ilgilendirmeyen bir durumdur. Dolayısıyla dış dünyaya faydalı hiçbir bilgi 
    vermemektedir. Parametre değişkenlerinin gösterici olmadıktan sonra const biçimde bildirilmesi iyi bir teknik değildir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    volatile tür niteleyicisi de kullanım bakımından const tür niteleyeicisine oldukça benzemektedir. volatile niteleyicisi yine türe ilişkindr. Örneğin:

    volatile int x;

    Burada x "volatile int" türündendir. 

    volatile const niteleyicisine göre oldukça seyrek kullanılan bir niteleyicidir. Bu niteleyicinin anlaşılması bazı başka temaları gerektirdiği için 
    biraz zor olabilmektedir. 

    volatile anahtar sözcüğünün işlevini anlayabilmek için derleyicilerin optimizasyonları hakkında bazı bilgilere sahip olunması gerekir. 
    Derleyiciler kodun anlamını değiştirmeden kodu daha hızlı çalışabilir ya da daha az yer kaplar hale getirebilmektedir. Optimizasyonun temel prensibi 
    "programcının niyet ettiği şeylerin asla bozulmamasıdır." Yani kodun optimize edilmiş haliyle edilmemiş hali tamamen aynı etkilere yol açmalıdır.
    Programcı kodun optimize edilmiş olduğunu bilmek zorunda değildir. Örneğin:

    x = a + 10;
    y = a + 20;

    Burda derleyici a değişkenini nellekten yazmaca çektikten sonra yeniden yazmaca çekmek istemeyebilir. Intel mimarisinde derleyici şöyle bir kod üretebilir:

    mov reg1, a
    mov reg2, 10
    add reg2, reg1
    mov x, reg2
    add reg1, 20
    mov y, reg1

    Burada derleyici a'yı reg1 yazmacına çekmiştir. Sonra da a zaten reg1 yazmacında olduğu için onu yeniden yazmaca çekmeden reg1 yazmacındaki değeri 
    yeniden kullanmıştır. Şimdi başka bir akışın birinci deyim bitiğinde a'yı değiştirdiğini düşünelim. Yani a dışarıdaki bir el tarafından değiştirilmiş 
    olsun:

    x = a + 10;
    ---> burada a'nın bilinmeyen bir biçimde programdan bağımsız olarak değiştirildiğini düşünelim
    y = a + 20

    Bu değişikliğin üretilen makine komutunda yapıldığı yer aşağıda belirtilmiştir:

    mov reg1, a
    mov reg2, 10
    add reg2, reg1
    mov x, reg2
    ----> a burada değiştirilmiş
    add reg1, 20
    mov y, reg1

    İşte burada dışarıdan yapılan değişiklik derleyicinin ürettiği kod tarafından anlaşılamamktadır. Çünkü derleyici a'nın değerini daha önce reg1 yazmacına
    yerleştirmiştir. Dılşarıdan başka bir elin a'yı değiştirebileceğini dikkat elmamıştır. Optimizasyon sırasında dışarıdan başka bir elin bu değişikliği 
    yapması derleyiciler tarafından dikkate alınmaz. Bizim burada isteğimiz dışarıdan a'da yapılan değişikliğin koda yansımasını sağlamak olsun. Eğer derleyici
    a'nın reg1 yazmacındaki halini kullanmasaydı her a kullanıldığında onu bellekten yeniden yükleseydi program bu değişikliği görebilirdi. Örneğin:

    mov reg1, a
    mov reg2, 10
    add reg2, reg1
    mov x, reg2
    ----> a burada değiştirilmiş
    mov reg1, a
    add reg1, 20
    mov y, reg1 

    Burada artık ilgili noktada a değiştirildiğinde derleyicinin ürettiği kod a'yı yeniden bellekten yazmaca yüklediğinden program içerisinde değişiklik 
    dikkate alınacaktır. 

    Burada anlatılmak istenen şey şudur: Derleyiciler nesneleri yazmaçlara çekip nasıl olsa onların değerleri yazmaçta diye o yazmaçtaki değeri 
    kullanabilmektedir. Bu durum programın daha hızlı çalışmasına yol açmaktadır. Ancak bilinmeyen bir el dışarıdan ilgili nesneyi değiştirdiğinde 
    derleyicinin üretmiş olduğu kod bu değişikliği dikkate alamamktadır. Bu değişikliğin kodda hemen dikkate alınabilmesi için derleyicinin ilgiyi nesneyi 
    yazmaçlarda uzun süre tutmaması ve nesne ne zaman kullanılırsa onu yeniden bellekten alması gerekir. Daha dramatik bir örnek şöyle verilebilir:
    

    int flag;
    ...
    flag = 0;

    while (flag == 0) {
        ...
    }

    derleyici flag değişkenini bir yazmaca çekerek döngü içerisinde hep o yazmaçtaki değeri kullanıyor olabilir. Bu durumda dışarıdaki bir el 
    flag nesnesini 1'e çekse bile derleyicinin üretmiş olduğu bu kod döngüden çıkmayacaktır. Çünkü derleyici flag nesnesinin içeriği zaten yazmaçta 
    var diye onun yazmaçtaki halini kullanıyor olabilir. Oysa biz flag nesnesi dışarıdan başka bir el tarafından değiştirildiğinde döngüden çıkmak isteyebiliriz. 

    Burada kişilerin aklına iki soru gelmektedir. 

    1) Dışarıdaki bir el benim programındaki bir nesnenin değerini nasıl değiştirebilir? İşte bu başka el bazen bir thread olabileceği gibi bazen de bir 
    "kesme kodu (interrupt handler)" olabilmektedir. 

    2) Derleyicinin bu nesnenin dışarıdan değiştirilebileceğini dikkate alıp bu tür optimizasyonları yapmaması gerekmez mi? Derleyiciler bir nesnenin başka 
    bir el tarafından dışarıdan değiştirileceğini varsaymamaktadır. Bu çok özel bir durumdur. Eğer derleyiciler böyle bir varsayımda bulunup ona göre kod 
    üretselerdi üretilen kodun kalitesi hız bakımından düşük olurdu. Bu özel durumun programcı tarafından dikkate alınması daha uygun bir tasarımdır. 

    Şimdi volatile anahtar sözcüğünün neyi sağladığını açıklayalım. Biz bir nesneyi volatile anahtar sözcüğü ile tanımlarsak derleyiciye şunları
    söylemiş olmaktayır: "Derleyici, bu nesnenin dışarıdan başka bir el tarafından değiştirilme gibi bir durumu var. Dolayısıyla ben bu nesne dışarıdan
    değiştirildiğinde değişikliği hemen anlamak istiyorum. Ben ne zaman bu nesneyi kullansam sen onu yazmaca çekmiş olsan bile onun yazmaçtaki değerini 
    kullanma. Taze taze onu yeniden bellekten çek ve kullan". Örneğin:

    volatile int a;
    ...
    x = a + 10;
    ---> burada dışarıdan a değiştirilmiş olsun
    y = a + 20;

    Burada artık derleyici a volatile olduğu için a her kullanıldığında onu bellekten yeniden taze taze yükler. Böylece ok ile belirtilen noktada 
    a dışarıdan değiştirilmiş olsa bile bizim kodumuz bu değişikliği hemen görecektir. Örneğin:

    volatile int flag;
    ...
    flag = 0;
    while (flag == 0) {
        ...
    }

    Burada flag nesnesi volatile olduğu için her flag kullanıldığında derleyici onu bellekten taze taze yeniden alacaktır. Böylece dışarıdaki bir el
    flag değişkenini 1'e çektiğinde döngüden çıkılacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        64. Ders - 26/01/2023 Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    volatile niteleyicisi seyrek biçimde kullanılmaktadır. Eğer çok thread'li uygulamalar ya da birtakım kesme kodlarının oluşturulduğu uygulamalar söz konusu 
    değilse volatile niteleyicisine gereksinim duyulmaz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    volatile niteleyicisi de göstericlerle kullanılabilmektedir. Tıpkı const niteleyicisinde olduğu gibi kendisi volatile olan, gösterdiği yer volatile olan,
    hem kendisi hem gösterdiği yer volatile olan göstericiler söz konusu olabilmektedir. Öneğin:

    volatile int *pi;					/* kendisi değil, gösterdiği yer volatile olan gösterici */
    int * volatile pi;					/* gösterdiği yer değil kendisi volatile olan gösterici */
    volatile int * volatile pi;			/* hem kendisi hem de gösterdiği yer volatile olan gösterici */

    Göstericinin gösterdiği yerin volatile olması "o gösterici ile * ya da [] oprratörü kullanılarak erişim yapıldığında erişilen yerin volatile olması"
    anlamına gelmektedir. Yani bu durumda derleyici ne zaman göstsricinin gösterdiği yere erişilse onu yeniden taze taze bellekten yeniden alır. Örneğin:

    int a = 10, b;
    volatile int *pi = &a;
    ...

    a = *pi + 10;
    b = *pi + 20;

    Derleyici bu durumda *pi bir yazmaçta olsa bile her defasında yeniden pi'nin gösterdiği yere erişecektir. 

    volatile anahtar sözcğü de türün bir parçasını oluşturmaktadır. Örneğin:

    volatile int a;

    Burada a "volatile int" türündendir. Tamamen const niteleyicisinde olan durumlar volatile niteleyicisinde de söz konusudur. Yani biz volatile bir 
    nesnenin adresini gösterdiği yer volatile olmayan bir göstericiye atayamayız. Ancak volatile olmayan bir bir nesnenin adresini gösterdiği yer volatile 
    olan bir göstericiye atayabiliriz. Başka bir deyişle volatile T * türünden T * türüne otomatik dönüştürme yoktur, ancak T * türünden volatile T * 
    türüne otomatik dönüştürme vardır. 
    
    Örneğin:

    int a;
    volatile int b;
    int *pi1;
    volatile int *pi2;

    pi1 = &b;		/* geçersiz, volatile int * türünden int * türüne otomatik dönüştürme yoktur. */
    pi2 = &b;		/* geçerli */
    pi2 = &a;		/* geçerli, int * türünden volatile int * türüne otomatik dönüştürme vardır */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    const ve volatile tür niteleyicileri beraber de kullanılabilmektedir. Bildirimde hangisinin önce yazılacağının bir önemi yoktur. Örneğin:

    const volatile int *pi;

    Burada pi'nin gösterdiği yer hem const hem de volatile durumdadır. Örneğin:

    const volatile int x = 10;

    Örneğin:

    const volatile int *pi = &x;
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    enum türleri ve sabitleri ilk kez C'de tasarlanmış ancak pek çok programlama diline benzer sentakslarla sokulmuştur. "enum" sözcüğü İngilizce 
    "enumarate" ve "enumaration" sözcüklerinden kısaltma yapılarak uydurulmuştur. "enumerate" tek tek saymak, numaralandırmak gibi anlamlara gelmektedir. 
    "enumeration" sözcüğü için Türkçe "sayımlama" sözcüğü de kullanılmaktadır. "enum" sözcüğü İngilizce "inam" ya da "inum" biçiminde okunmaktadır. 

    Bir enum bildiriminin genel biçimi şöyledir:

    enum [isim] {
        <enum sabit listesi>
    };

    Enum sabit listesi virgüllerle ayrılmış sabit belirten değişken isimlerinden oluşmaktadır. Örneğin:

    enum COLOR {
        Red, Green, Blue
    };

    Burada Red, Green ve Blue birer isimdir ancak sabit belirtmektedir. İlk enum sabitinin değeri 0'dır. Sonraki enum sabitleri öncekilerden bir fazla olarak devam eder. 
    Yani bu bildirimde Red 0 değerini, Green 1 değerini ve Blue 2 değerini belirtmektedir. Enum sabitlerine İngilizce "enumeration constans" denildiği gibi 
    kısaca "enumerator" da denilmektedir. Enum sabitleri isimsel olsa da aslında sabit bir sayı belirtmektedir. Dolayısıyla bunlara atama yapamayız. Örneğin:

    Blue = 10;		/* geçersiz bu işlem adeta 2 = 10 gibi bir işlemdir */

    Enum sabitleri sabit ifadeleri gereken yerlerde kullanılabilmektedir. Örneğin:

    switch (color) {
        case Red:
            ...
        case Green:
            ...
        case Blue:
            ...
    }

    Buradaki Red, Green ve Blue sanki #define ile oluşturulmuş olan sembolik sabitler gibidir:

    #define Red			0
    #define Green		1
    #define Blue		2

    Her ne kadar enum sabitleri ile #define sabitleri semantik bakımdan birbirlerine benziyorsa da aralarında önemli bazı farklılıklar da vardır. 
    #define komutu önişlemci modülüne ilişkindir. Dolayısıyla bunların "faaliyet alanı (scope)" biçiminde bir kavramları yoktur. Oysa enum sabitleri 
    derleme modülüne ilişkindir. Bunların faaliyet alanları vardır. enum aynı zamanda bir tür de belirtmektedir. Enum sabitleri enum bildirimi hangi 
    faaliyet alanına yerleştirilmişse o faaliyet alaına sahip olur. Örneğin biz enum bildirimini global alana yerleştirirsek enum sabitleri de 
    global değişken gibi faaliyet gösterir.
    
    Enum sabitleri gerçek sayı türlerine ilişkin olamazlar. Yalnızca tamsayı değerleri temsil ederler. 

    Bir enum bildirimi yapılırken enum ismi vermek zorunlu değildir. C++ programcıları genellikle enum bildiriminde tür ismi belirtmezler. Örneğin:

    enum { sunday, monday, tuesday, wednesday, thursday, friday, saturday};
    enum { January, February, March, April, May, June, July, August, September, October, November, December};

    Bir enum sabitine '=' sentaksı ile bir sabit ifadesi kullanılarak değer verilebilir. Tabii bu işlem atama anlamına gelmez. Yalnızca "bu enum 
    sabitinin değeri bu olsun" anlamına gelir. Diğer enum sabitleri bu değeri izlemektedir. 

    enum COLOR {
        Red = 10, Green, Blue
    };

    Burada Red = 10, Green = 11 ve Blue = 12 olur. Tabii istediğimiz birden fazla enum sabitine bu biçimde değerler verebiliriz. Örneğin:

    enum { sunday, monday = 3, tuesday, wednesday, thursday = 7, friday, saturday};

    Burada sunday = 0, monday = 3, tuesday = 4, wednesday = 5, thursday = 7, friday = 8, saturday = 9 olacaktır. Örneğin:

    enum COMMAND {ADD = 1, DEL = 3, LIST = 5, QUIT = 7};

    Birden fazla enuma sabitinin aynı değerde olmasının bir sakıncası yoktur. Örneğin:

    enum KEY { Enter = 1, Return = 1, Insert, Home, PgDown = 12, PgUp = 20};

    Bir enum sabiti gerçek sayı değerlerini alamaz. Enum sabitlerinin değerleri int türünün o sistemdeki sınırlarını aşamaz. Örneğin int türünün 4 byte yer 
    kapladığı bir sistemde söz konusu olsun:

    enum COLOR {
        Red = 2147483646, Green, Blue			/* geçersiz! Blue int sınırlarını aşmış! */
    };

    Tabii enum sabitleri negatif değerler de alabilir. Örneğin:

    enum TEST {AA = -1, BB, CC};

    Burada AA = -1, BB = 0 ve CC = 1 değerlerini belirtecektir. Örneğin:

    enum TEST = {A = 'a', B = 'b', C = 'd'};		/* geçerli */

    Tek tırnak içerisine alınmış karakterlerin int türden sabit belirttiğini anımsayınız. Yukarıdaki enum bildirimi geçerlidir. Örneğin:

    enum TEST = {A = "ankara", B = "izmir", C = "istanbul"};		/* geçersiz! */

    Bir enum sabitine iki tırnak ile değer veremeyiz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıda da belirttiğimiz gibi enum sabitleri enum bildirimi nerede yapılmışsa sanki orada bildirilmiş isimler gibi faaliyet alanına sahip olur. 
    Örneğin:

    #include <stdio.h>

    enum COLOR {
        Red, Green, Blue		/* enum sabitleri her yerde kullanılabilir */
    };

    int main(void)
    {
        int a = Red;		/* geçerli */

        return 0;
    }

    void foo(void)
    {
        int x = Red;		/* geçerli */
    }

    Burada enum bildirimi global düzeyde yapıldığı için enum sabitleri de global düzeyde her yerde kullanılabilir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        enum COLOR {
            Red, Green, Blue		/* enum sabitleri her yerde kullanılabilir */
        };
    
        int a;

        a = Red;		/* geçerli */

        return 0;
    }

    void foo(void)
    {
        int x = Red;		/* geçersiz! */
    }

    Burada enum bildirimi yerel düzeyde yapılmıştır. Bu durumda enum sabitleri de sanki yerel değişkenlermiş gibi yalnızca o blokta kullanılabilecektir. 
    Hemen her zaman enum sabitleri global düzeyde bildirilmektdir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    enum sabitleri her zaman int türden sabitler olarak kabul edilmektedir. Örneğin:

    enum {Red, Green, Blue};

    Burada Red, Green ve Blue sabitleri int türdendir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    enum bildirimi aynı zamanda bir tür de oluşturmaktadır. Dolayısıyla enum türünden nesneler de tanılanabilmektedir. Tür ismi "enum" sözüğü ile enum isminden 
    oluşmaktadır. Örneğin:

    enum COLOR {Red, Green, Blue};

    Burada oluşturulan tür "enum COLOR" ismindedir. enum türündne nesneler de oluşturulabilmektedir. Örneğin:

    enum COLOR c;

    Pek çok programalama dilinde enum türünden bir nesneye yalnızca o enum türünün enum sabitleri atanabilmektedir. Ancak C'de durum böyle değildir. 
    C Standartlarına göre bir enum türü char türü ya da işaretli ya da işaretsiz tamsayı türlerinden biri ile "uyumlu (compatible)" olmak zorundadır. Yani enum 
    türleri derleyici için tamamen kendi seçtiği bir tamsayı türü ile özdeştir. Tabii derleyicinin o enum türü için seçtiği tamsayı türü o enum türünün 
    enum sabitlerinin değerlerini kapsayıcı olmalıdır. Buradan çıkan sonuç aslında enum türünden nesnelerin tamsayı türünden nesneler gibi ele alındığıdır. 
    Bu nedenle biz onlara tamsayı değerler atayabiliriz. Örneğin:

    enum COLOR a = Red;		/* geçerli */
    enum COLOR b = 123;		/* geçerli */
    enum COLOR c = 12.3;	/* geçerli, gerçek sayı değerleri tamsayı türünden nesnelere atanırsa noktadan sonraki kısım atılır */

    Bir enum türünden nesnenin adresi doğrudan onunla uyumlu olan tamsayı türünden göstericiye de atanamaz. Örneğin:

    enum COLOR c = 123;
    int *pi;

    pi = &c;		/* enum COLOR int ile uyumlu olsa bile atama geçersizdir! */

    Tabii enum türünden göstericiler de söz konusu olabilir. Örneğin:

    enum COLOR *pc;

    C'de enum türünden nesne tanımlamanaın pratik önemli bir faydası yoktur. Ancak C++ gibi, Java ve C# gibi dillerde en azından enum türü derleme zamanında 
    bir kontrol sunmaktadır. Bu dillerde enum türünden nesnelere biz yalnızca enum türünün sabitlerini atayabiliriz. Dolayısıyla o dillerde enum türünden nesneler
    bu denetimden dolayı yaygın biçimde kullanılmaktadır. Örneğin aşağıdaki gibi bir C# kodu olsun:

    enum Color {
        Red, Green, Blue
    };
    Color c;

    c = Color.Red;		/* geçerli */
    c = 0;				/* geçersiz! */

    Bu nedenlerden dolayı C'de programcılar enum türlerine genellikle isim de vermemektedir. Örneğin:

    enum { Red, Green, Blue};

    Tabii programcılar tarafından enum türleri okunabilirliği artırmak için de kullanılabilmektedir. Örneğin:

    void foo(enum Color color)
    {
        /* ... */
    }

    Burada biz foo fonksiyonunu çağırırken argüman olarak enum Color türünde belirtilen enum sabitlerini kullanmazsak 
    herhangi bir sorun ortaya çıkmaz. Ancak kodu inceleyen kişiler fonksiyonun bir renk isteyen bir parametreye sahip 
    olduğunu anlar ve kodu daha iyi anlamlandırır. Tabii ilgili derleyicide enum Color eğer int türü anlamına geliyorsa 
    aslında bu fonksiyonun bildiriminin kullanım bakımından aşağıdakinden bir farkı yoktur:

    void foo(int color)
    {
        /* ... */
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    enum sabitlerinin faaliyet alanı enum bildiriminin yerleştirildiği yere ilişkin olduğu için ve aynı faaliyet alanında 
    aynı isimli tek bir değişken bulunabileceği için dikkatli olmak gerekir. Örneğin:

    enum Fruit {
        Apple, Orange, Banana
    };

    enum Company {
        Apple, Microsoft, Oracle		/* geçersiz! */
    };

    int Oracle;						/* geçersiz! */

    Burada aynı faaliyet alanı içerisinde iki defa aynı Apple ismi kullanılmıştır. Bu durum geçerli değildir. Benzer biçimde Oracle ismi de aynı faaliyet alanında 
    iki kez bildirilmiştir. Bu da geçerli değildir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir enum sabitine değer verilirken öncek enum sabitlerinden faydalanılabilir. Örneğin:

    enum {Red, Green = Red + 2, Blue = Green + 2};			/* geçerli */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    enum türleri de typedef edilebilir. Örneğin:

    enum Color {
        Red, Green, Blue
    };

    typedef enum Color Cl;

    Cl c;		/* c enum Color türündendir */

    Yapılarda olduğu gibi enum bildiriminde küme parantezinden sonra değişken listesi yazılırsa aynı zamanda o enum türünden nesneler de tanımlanmış olur. Örneğin:

    enum Color {
        Red, Green, Blue
    } c, *pc;

    Burada c enum Color türünden bir nesne ve pc de enum Color türünden bir göstericidir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bu bölümde göstericilerle ilgili bazı karmaşık konular ele alınacaktır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Göstericiler de birer nesne olduğuna göre göstericilerin de adresleri alınabilir. Örneğin:

    int a = 10;
    int *pi = &a;

    Burada pi'nin içerisinde a'nın adresi vardır. Ancak pi'nin de adresi alınabilir. Daha öncedne de belirttiğimiz gibi göstericinin 
    türü sembolik olarak T * ile temsil edilmektedir. Örneğin:

    int *pi;

    Burada pi "int *" türündendir. Bir nesnenin adresini aldığımızda tür bileşeni o nesnenin türünden olan sayısal bileşeni o nesnenin 
    bellekteği başlangıç adresi olan bir adres bilgisini elde ederiz. Örneğin:

    int a;

    Burada biz &a işlemiyle "int *" türünden bir adres elde ederiz. Şimdi bir göstericinin adresini alalım:

    int *pi;

    Burada &pi ile pi göstericisinin adresini aldığımızda adresin tür bileşeni int * olacaktır. Pekiyi bu adresi hangi türden bir göstericiye yerleştirebiliriz?
    Tabii ki tür bileşeni int * olan bir göstericiye yerleştrebiliriz. C'de iki yıldızlı göstericilere "göstericileri gösteren göstericiler (pointers to pointers)"
    denilmektedir. Örneğin:

    int **ppi;

    Burada ppi nir göstericiyi gösteren gösterisidir. Buradaki göstericinin tür bileşeninin "int *" olduğuna dikkat ediniz:

    int * *ppi;

    Burada *ppi yani ppi'nin gösterdiği yerdeki nesnenin türü "int *" biçimindedir. Yani burada ppi bir göstericiyi göstermektedir. C'de "int *" türü
    int nesneyi gösteren bir adres türü anlamına geldiğine göre "int **" türü de int * türündne nesneyi gösteren bir adres türü anlamına gelmektedir. 
    Örneğin:

    int a = 10;
    int *pi;
    int **ppi;

    pi = &a;
    ppi = &pi;		

    Buradan da anlaşıldığı gibi T türünden bir göstericinin adresi alındığında bunun T ** biçiminde bildirilmiş bir göstericiye atanması gerekir. Örneğin:

    int **ppi;

    Bu bildirimde ppi'in türü "int **" biçimindedir. Biz ppi'yi * operatörüyle kullandığımızda elde edeceğimiz nesne "int *" türünden olacaktır. 
    Yani ppi int türden bir göstericinin adresini tutmalıdır. Örneğn:

    int a = 10;
    int *pi;
    int **ppi;

    pi = &a;
    ppi = &pi;	

    Burada *ppi ile biz pi'ye erişmiş oluyoruz. * operatörü sağdan sola öncelikli olduğuna göre **ppi ifadesi ile a'ya erişiriz. Burada ppi "int **"
    , *ppi int * türünden, **ppi ise int türdendir. 

    Özetle yinelersek bir göstericinin adresini biz o gösterici ile aynı türden çift yıldızlı bir göstericiye atayabiliriz. 
    Aşağıdaki gibi bir atama geçersizdir:

    int *pi;
    char **ppc;

    ppc = &pi;		/* geçersiz! */

    Burada ppc'ye char türden bir göstericinin adresini atayabiliriz. int türden bir göstericinin adresini atayamayız. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi bir göstericiyi gösteren gösterici de bir nesne olduğuna göre biz onun da adresini alabilir miyiz? Evet alabiliz, ancak 
    onun adresini atayacağımız göstericinin üç yıldızlı bir gösterici olması gerekir. Örneğin:

    int a = 10;
    int *pi;
    int **ppi;
    int ***pppi;

    pi = &a;
    ppi = &pi;
    pppi = &ppi;

    Benzer biçimde dört yıldızlı, beş yıldızlı ve daha çok yıldızlı göstericiler de tanımlanabilmektedir. Ancak her ne kadar çok yıldızlı göstericiler 
    tanımlanabiliyorsa da bunların kullanılmasının gerektiği gerçek uygulamalar yok gibidir. Üç yıldızlı bir göstericilere çok çok seyrek gereksinim duyulmaktadır. 
    Ancak iki yıldızlı göstericiler yaygın kullanılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                65. Ders - 31/01/2023 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Dizilerin isimleri bir ifadede kullanıldığında bu isimler dizilerin başlangıç adresleri anlamına geliyordu. Dizilerin başlangıç adresleri ilk elemanlarının 
    adresleriyle aynı anlamdadır. O halde bir gösterici dizisinin ismi de aslında o gösterici dizisinin ilk elemanın adresi anlamına gelmektedir. Örneğin:

    int x = 10, y = 20, z = 30;
    int *a[] = {&x, &y, &z};

    Burada a ifadesi int ** türündendir ve bu gösterici dizisinin başlangıç adresini yani onun ilk elemanının adresini belirtir. Bu durumda biz bu a ifadesini
    aynı ürden bir göstericiyi gösteren göstericiye atayabiliriz. Örneğin:

    int **ppi;

    ppi = a;
    
    Burada artık *ppi ifadesi dizinin ilk elemanını belirtmektedir. Bu eleman da zaten int türden bir göstericidir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        int x = 10, y = 20, z = 30;
        int *a[] = {&x, &y, &z};
        int **ppi;


        ppi = a;
        for (int i = 0; i < 3; ++i)
            printf("%d\n", *ppi[i]);

        return 0;
    }

    Burada ppi[i] ifadesi ile biz aslında gösterici dizisinin i'inci indisli elemanına erişiyoruz. Bu eleman da bir gösterici olduğuna göre o göstericinin de
    gösterdiği yere erişmek için *ppi[i] ifadesini kullanıyoruz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bu durumda biz bir gösterici dizisini bir fonksiyona parametre yoluyla aktarmak istersek fonksiyonun parametresinin göstericiyi gösteren gösterici olması 
    gerekir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void disp_names(char **names, size_t size)
{
    for (size_t i = 0; i < size; ++i)
        puts(names[i]);
}

int main(void)
{
    char *names[] = {"ali", "veli", "selami", "ayse", "fatma"};

    disp_names(names, 5);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii mademki NULL adres hiçbir nesnenin adresi olamayacak bir adres belirtmektedir. Bu durumda programcılar gösterici dizilerinin sonuna 
    NULL adres yerleştirip dizinin uzunluğunu fonksiyona geçirmeyebilirler. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void disp_names(char **names)
{
    for (size_t i = 0; names[i] != NULL; ++i)
        puts(names[i]);
}

int main(void)
{
    char *names[] = {"ali", "veli", "selami", "ayse", "fatma", NULL};

    disp_names(names);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun bizim verdiğimiz bir göstericinin içerisine adres yerleştirebilmesi için fonksiyonun parametresinin gösterciyi gösteren gösterici olması 
    ve bizim de fonksiyonu bir göstericinin adresiyle çağırmamız gerekir. 

    Aşağıdaki örnekte alloc_intarray fonksiyonu belli uzunlukta int bir diziyi dinamik biçimde tahsis edip tahsis edilen adresi parametresiyle aldığı 
    göstericinin içine yazmıştır. Fonksiyon başarısız olursa programı sonlandırmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

void alloc_intarray(int **ppi, size_t size)
{
    if ((*ppi = (int *)malloc(sizeof(int) * size)) == NULL) {
        fprintf(stderr, "cannot allocate array!..\n");
        exit(EXIT_FAILURE);
    }
}

int main(void)
{
    int *pi;

    alloc_intarray(&pi, 10);

    for (int i = 0; i < 10; ++i)
        pi[i] = i;

    for (int i = 0; i < 10; ++i)
        printf("%d ", pi[i]);
    printf("\n");

    free(pi);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki fonksiyonun alternatif baka bir tasarımı da yapılabilrdi. Fonksiyon dinamik tahsis edilmiş olan dizinin adresine geri dönebilirdi. 
    Genellikle programcılar	bu tür fonksiyonları adrese geri dönecek biçimde yazmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int *alloc_intarray(size_t size)
{
    int *pi;

    if ((pi = (int *)malloc(sizeof(int) * size)) == NULL) {
        fprintf(stderr, "cannot allocate array!..\n");
        exit(EXIT_FAILURE);
    }

    return pi;
}

int main(void)
{
    int *pi;

    pi = alloc_intarray(10);

    for (int i = 0; i < 10; ++i)
        pi[i] = i;

    for (int i = 0; i < 10; ++i)
        printf("%d ", pi[i]);
    printf("\n");

    free(pi);

    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
    void bir göstericiye herhangi bir türden adres tür dönüştürmesi yapılmadan atanabiliyordu. Çift yıldızlı bir adres de benzer biçimde void bir göstericiye 
    atanabilmektedir. Yani T ** türünden void * türüne otomatik dönüştürme vardır. Tabii böyle bir atama yıldız konusunda bir dengesizlik oluşturur. 
    Yani yanlış anlaşılmalara yol açabilmektedir. Örneğin:

    char *names[] = {"ali", "veli", "selami"};
    void *pv;
    char **ppnames;

    pv = names;					/* geçerli tür dönüştürmesine gerek yok */
    ppnames = pv;				/* C'de geçerli C++'ta geçersiz */
    ppnames = (int **)pv;		/* Hem C'de hem de C++'ta geçerli */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Çift yıldızlı void göstericiler daha önce gördüğümüz tek yıldızlı void göstericiler gibi değildir. Biz tek yıldızlı void göstericilere herhangi bir
    nesnenin adresini doğrudan atayabiliriz. Tek yıldızlı void göstericilere biz örneğin int türden bir göstericinin de adresini atayabiliriz. Ancak 
    iki yıldızlı (tabii daha fazla yıldız da olabilir) bir void göstericiye biz herhangi bir nesnenin adresini atayamayız. Yalnızca void bir göstericinin 
    adresini atayabiliriz. Başka bir deyişle T bir tür belirtmek üzere T * türünden void * türüne "otomatik dönüştürmesi (implicit conversion)" vardır ancak 
    T** türünden void ** türüne otomatik dönüştürmesi yoktur. void **ppv gibi bir göstericiye biz yalnızca void * türünden bir göstericinin adresini atayabiliriz. 
    Örneğin:

    char *names[] = {"ali", "veli", "selami"};
    void *pv;
    void **ppv;

    pv = names;		/* geçerli */
    ppv = names;	/* geçersiz! */	
    ppv = &pv;		/* geçerli */

    void ** türünden bir gösterici void bir gösterici değildir. Bu gösterivcinin gösterdiği yer void değildir, gösterdiği 
    yer void * türündendir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi fonksiyon parametresi olan göstericiler dizi sentaksıyla da belirtilebiliyordu. Köşeli parantezlerin içerisine sabit ifadeleri de 
    yerleştirilebiliyordu. Buraya yerleştirilen sabit ifadelerinin hiçbir önemi yoktu. Örneğin:

    void foo(int pi[])
    {
        /* ... */
    }

    Bu tanımlama tamamen aşağıdakiyle eldeğerdir:

    void foo(int *pi)
    {
        /* ... */
    }

    Yine aşağıdaki gibi bir tanımlama da yukarıdailerle tamamen eşdeğerdir:

    void foo(int pi[100])
    {
        /* ... */
    }

    Burada bu 100 değerinin hiçbir önemi yoktur. Yine fonksiyonun parametresi aslında int türden bir göstericidir. 

    Aşağıdaki tanımlamaya dikkat ediniz:

    void foo(const int a[])
    {
        /* ... */
    }

    Bu tanımlama da aşağıdakiyle tamamen eşdeğerdir:

    void foo(const int *a)
    {
        /* ... */
    }

    Benzer biçimde fonksiyonun göstericiyi gösteren gösterici parametresi de dizi sentaksıyla belirtilebilmektedir:

    void foo(char *argv[])
    {
        /* ... */
    }

    Bu tanımlamada da aslında argv göstericiyi gösteren göstericidir. Yani bu tanımlama tamamen aşağıdakiyle eşdeğerdir:

    void foo(char **argv)
    {
        /* ... */
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Göstericiyi gösteren göstericlerde const ve volatile niteleyicileri üç pozisyonun yalnızca birinde ya da birden fazlasında bulunabilmektedir. 
    T bir tür belirtmek üzere:

    1) const T **ppt;
    2) T * const * ppt;
    3) T ** const ppt = addr;

    Önce birinci durumu ele alalım:

    const T **ppt;

    Buada ppt const değildir, *ppt de const değildir, ancak **ppt const durumdadır. Tabii ppt göstericisine gösterdiği yer const olan 
    bir nesnenin adresi yerleştirilmelidir. Örneğin:

    const int a = 10;
    const int *pi;
    int *pi2;
    const int **ppi;

    ppi = &pi;		/* geçerli */
    pi = &a;		/* geçerli */
    ppi = &pi2;		/* geçersiz! */

    Burada dikkat edilmesi gereken nokta ppi'nin ve *ppi'nin const olmadığıdır. Yani, *ppi kendisi const olan bir gösterici değildir. 
    Gösterdiği yer const olan bir göstericidir. 

    Şimdi ikinci durumu ele alalım:

    T * const * ppt;

    Burada ppt'nin kendisi const değildir, onun gösterdiği yer const durumdadır. Yani *ppt const durumdadır. Ancak **ppt const değildir. 
    Örneğin:

    int * const *ppi;
    int *pi;
    int a = 10;

    ppi = &pi;		/* geçerli */
    pi = &a;		/* geçerli */
    **ppi = 20;		/* geçerli */
    *ppi = &a;		/* geçersiz! *ppi const */

    Elimizde kendisi const olan bir gösterici varsa biz onun adresini const anahtar sözcüğünün ortada olduğu bir göstericiyi gösteren 
    göstericiye yerleştirebiliriz. Örneğin:

    int * const *ppi;
    
    Burada ppi'nin kendisi değil *ppi const durumdadır. Kendisi const olan bir göstericinin adresi bu biçimdeki bir const 
    göstericiye atanabilir. Örneğin:

    int a = 10;
    int * const pi = &a;

    ppi = &pi;		/* geçerli */

    Burada &pi ifadesi int * const * türündendir. Dolayısıyla bu da int * const * türünden bir göstericiyi gösteren gösteriye 
    atanabilir. Aşağıdaki atama geçersizdir:

    int **ppi;
    int a = 10;
    int * const pi = &a;
    
    ppi = &pi;		/* geçersiz! */

    Şimdi de üçüncü durumu ele alalım:

    T ** const ppt = addr;

    Burada ppt'nin kendisi const durumdadır, *ppt ya da **ppt const durumda değildir. Örneğin:

    int a = 10;
    int *pi = &a;
    int **const ppi = &pi;
    
    **ppi = 20;		/* geçerli */
    *ppi = &pi;		/* geçerli */

    printf("%d\n", a);

    Göstericiyi gösteren göstericilerdeki const ve volatile durumları tür bilgisini etkilemektedir. Örneğin:

    const int * const *pi;

    Burada ppi'nin türü ""const int * const"" biçimdedir. Örneğin:
    
    int * const pi = addr;
    
    Burada &pi'nin türü "int * const *" biçimindedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de (ve tabii C++'ta) iki yıldızlı göstericilerde const ve volatile niteleyicilerinde programcıların anlamakta zorlandığı ilginç bir durum vardır. 
    Anımsanacağı gibi C'de "T *" türünden "const T *" türüne otomatik dönüştürme tanımlıdır. Örneğin:

    int a = 10;
    const int *pi;

    pi = &a;	/* geçerli */

    Burada &a int * türündendir bu ifade const int * türünden olan pi'ye atanmıştır. Tabii yine anımsanacağı gibi bunun tersi geçersizdir. 
    Yani "const int *" türünden "int *" türüne otomatik dönüştürme yoktur. Örneğin:

    const int a = 10;
    int *pi;

    pi = &a;	/* geçersiz! */

    Fakat C'de "T **" türünden "const T**" türüne otomatik dönüştürme yoktur. Örneğin:

    int *pi;
    const int **ppi;

    ppi = &pi;		/* geçersiz! */

    Genellikle programcılar bu durumun "zarasız dolayısıyla da geçerli olması gerektiğini" düşünmektedir. Oysa bu durum zaralı sonuçlara yol açabileceğinden 
    geçersizdir. Bu zararlı sonuç aşağıdaki örnekle anlaşılabilir:

    const int a = 10;
    int *pi;
    const int **ppi;

    ppi = &pi;		/* geçersiz ama geçerli olduğunu varsayalım */
    *ppi = &a;		/* geçerli çünkü *ppi "const int *" türünden biz de ona "const int" nesnenin adresini atayabiliriz. 

    Burada ppi = &pi ataması aslında "int **" türünün "const int **" türüne atanması işlemidir. Bu durum geçersizdir. Ancak bunun geçerli olduğunu varsayalım. 
    Daha sonra yapılan *ppi = &a ataması da tamamen geçerlidir. Çünkü *ppi "const int *" türündendir. const bir nesnenin adresi gösterdiği yer 
    const olan bir göstericiye atanabilir. Bu durumda bu atama geçerli olur. Ancak burada hileli bir durum oluşmuştur. Aslında burada çaktırmadan const nesnenin 
    adresi gösterdiği yer const olmayan göstericiye atanmış olmaktadır. Yani bir yasak bir şeyi arkasından dolanarak yapmış olmaktayız. Örneğin:

    *pi = 20;	

    Şimdi biz burada const nesneyi değiştirmiş olduk. İşte bunun engellenmesinin tek yolu işin başında ppi = &pi atamasının kabul edilmemesidir. 
    Bu nedenle T ** türünden const T ** türüne otomatik dönüştürme yasaklanmıştır.

    Pekiyi T ** türü const olan hangi türe atanabilir? Bunun yanıtı "T * const *" türüdür. "T **" türünden "T * const *" türüne otomatik dönüştürme vardır.

    Örneğin:

    const int a = 10;
    int *pi = &a;
    int * const *ppi;

    ppi = &pi;	/* geçerli */
    
    Pekiyi const niteleyicisini ortaya alınca atama neden geçerli oldu? Çünkü const niteleyicisin ortaya alınması artık yukarıda açıkladığımız 
    arkadan dolaşma durumunu ortadan kalkmaktadır. Örneğin:

    const int a = 10;
    int *pi;
    const int * const *ppi;

    ppi = &pi;		/* geçerli */
    *ppi = &a;		/* geçersiz! *ppi const durumda */

    Tabii T ** türünün T ** türüne atanmasında bir sakınca yoktur. Zaten bunlar aynı türlerdir. Örneğin:

    int **ppi;
    int *pi;

    ppi = &pi;		/* geçerli, türler aynı */

    T ** türünü const olan bir göstericiye atayacaksak mutlaka ortada const niteleyicisinin ortada bulunması gerekir. 

    Pekiyi T ** türünden const T * const türüne otomatik dönüştürme var mıdır? Örneğin:

    const int * const *ppi;
    int *pi;

    ppi = &pi;		/* bu dönüştürme geçerli mi? */

    Aslında burada tukarıda bahsettiğimiz kötüye kullanım söz konusu değildir. Ancak C standartları buradaki göstericilerin
    türlerinin farklı olmasından dolayı otomatik dönüştürmeyi kabul etmemktedir. C++'ta bu atama geçerlidir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                        66. Ders - 02/02/2023 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de çok boyutlu diziler de tanımlanabilmektedir. Çok boyutlu diziler aslında doğal türler değildir. Çünkü bilgisayarımızın belleği tek boyutludur, 
    çok boyutlu değildir. Ancak yine de doğaaki bazı olguları temsil edebilmek için çok boyutlu dizi kavramından faydalanılmaktadır. C'de çok boyutlu diziler
    dekleratörde birden fazla köşeli parantez ile tanımlanmaktadır. Örneğin:

    int a[3][2];

    Burada 3 satır 2 sütuna sahip iki boyutlu bir dizi tanımlanmıştır. Örneğin:

    int b[3][2][5];

    Buada üç boyutlu bir dizi tanımlanmıştır. Her ne kadar boyut sayısı istenildeiğ kadar çok olabilirse de pratikte en çok iki boyutlu diziler kullanılmaktadır. 
    İki boyutlu dizilere "matris" de denilmektedir. Üç boyutlu dizilere çok seyrek gereknimin duyulmaktadır. 

    Çok boyutlu dizilerde elemana erişmek için bireden fazla köşeli parantez kullanılır. Örneğin:

    int a[3][2];

    a[0][0] = 10;
    a[0][1] = 20;
    a[1][0] = 30;
    a[1][1] = 40;
    a[2][0] = 50;
    a[2][1] = 60;

    Bütün boyutlar 0'ıncı indeksten başlatılarak indekslenmektedir. 

    Aşağıdaki örnekte iki boyutlu bir dizi matris görünümüyle iç içe iki döngü kullanılarak dolaşılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[3][2];

    a[0][0] = 10;
    a[0][1] = 20;
    a[1][0] = 30;
    a[1][1] = 40;
    a[2][0] = 50;
    a[2][1] = 60;

    for (int i = 0; i < 3; ++i) {
        for (int k = 0; k < 2; ++k)
            printf("%d ", a[i][k]);
        printf("\n");
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında çok boyutlu diziler C'de "dizi dizisi" gibi ele alınmaktadır. Örneğin:

    int a[3][4];

    Bu tanımlamayı derleyici adeta "3 elemanlı, her elemanı 4 elemanlı int değerlerden oluşan bir dizi" tanımlaması olarak ele almaktadır. İlk köşeli 
    parantez asıl diziyi belirtir. Sonraki köşeli parantezler eleman olan dizileri belirtmektedir. Bir dizinin türünün sembolik olarak "T [N]" ile temsil 
    edildiğini belirtmiştik. Yukarıdaki a dizisi aslında her elemanı "int[4] (4 elemanlı int dizi)" türünden olan 3 elemanlı bir dizi dizisidir. Burada a'nın
    türü "int[3][4]" biçiminde belirtilmektedir. Yani burada a "int[3][4]" türündendir. a dizisinin elemanları da "int[4]" türündendir. Dizi elemanları 
    ardışıl olduğuna göre yukarıdaki a dizisinin bellekteki organizasyonu şöyle olacaktır:

    a[0][0]
    a[0][1]
    a[0][2]
    a[0][3]
    a[1][0]
    a[1][1]
    a[1][2]
    a[1][3]
    a[2][0]
    a[2][1]
    a[2][2]
    a[2][3]

    Burada a her elemanı 4 elemanlı bir int dizi olan dizidir. Dolayısıyla biz bu a dizisini şöyle temsil edebiliriz:

    4 elemanlı dizi
    4 elemanlı dizi
    4 elemanlı dizi

    Burada dikkat edilmesi gereken durum şudur: Bu tanımlamada a aslında 3 elemanlı bir dizidir. Bu 3 eleman ardışıl olmalıdır. Öte yandan a'nın her elemanı da 
    bir dizi olduğuna göre o dizinin elemanları da ardışıl olmak zorundadır. O halde organizasyon mecburen yukarıdaki gibi olacaktır. Yani buradaki tüm int 
    değerler aslında bellekte ardışıl bir biçimde bulunmaktadır. Örneğin:

    int a[3][4][2];

    Burada asıl dizi 3 elemanlıdır. Ancak bu 3 elemanlı dizinin her elemanı aslında bir matristir. a "int [3][4][2]" türündedir. a'nın elemanları ise 
    "int[4][2]" türündedir. Bu eleman olan dizinin elemanları ise "int[2]" türündendir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Çok boyutlu dizilere ilkdeğer verilirken onun aslında bir dizisi dizisi olduğu dikkate alınmalıdır. Verilen ilkdeğerler tek tek yukarıdan aşağıya doğru 
    dizi elemanlarına yerleştirilir. Örneğin:

    int a[3][2] = {10, 20, 30, 40, 50, 60};

    Bu dizinin bellekteki organizasyonu şöyledir:

    a[0][0]
    a[0][1]
    a[1][0]
    a[1][1]
    a[2][0]
    a[2][1]

    İşte verilen ilkdeğerler de bellekteki organiasyona göre elemanlara yerleştirilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[3][2] = {10, 20, 30, 40, 50, 60};

    for (int i = 0; i < 3; ++i) {
        for (int k = 0; k < 2; ++k)
            printf("%d ", a[i][k]);
        printf("\n");
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Ancak çok boyutlu dizilere ilkdeğer verilirken eleman olan her dizi ayrıca küme parantezine alınabilir. Bu iyi bir tekniktir. Örneğin:

    int a[3][2] = {
        {10, 20}, 
        {30, 40}, 
        {50, 60}
    };

    Örneğin:

    int a[3][2][3] = {
        {
            {1, 2, 3}, 
            {4, 5, 6}
        }, 
        {
            {7, 8, 9}, 
            {10, 11, 12}
        }, 
        {
            {13, 14, 15}, 
            {16, 17, 18}
        }
    };

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[3][2] = {
        {10, 20}, 
        {30, 40}, 
        {50, 60}
    };

    for (int i = 0; i < 3; ++i) {
        for (int k = 0; k < 2; ++k)
            printf("%d ", a[i][k]);
        printf("\n");
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi uzunluk belirtmeden diziye ilkdeğer verebiliyorduk. Çok boyutlu dizilerde yalnızca ilk köşeli parantezin içi boş bırakılabilmektedir. 
    Örneğin:

    int a[][2] = {{10, 20}, {30, 40}, {50, 60}, {70, 80}};

    Aşağıdaki ilkdeğer verme geçersizdir:

    int a[][] = {{10, 20}, {30, 40}, {50, 60}, {70, 80}};
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Mademki char türden bir diziye iki tırnak ifadesiyle ilkdeğer verilebilmektdir. O halde char türden bir dizi dizisinin 
    her elemanına da iki tırnak ile ilkdeğer verebiliriz. Örneğin:

    char s[][10] = {"ali", "veli", "selami"};
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    char s[][10] = {"ali", "veli", "selami"};

    for (int i = 0; i < 3; ++i)
        puts(s[i]);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi C99 ile birlikte C'ye "designated initializer" ismi altında dizinin yalnızca istenilen elemanlarına ilkdeğer verilebiliyordu. 
    Çok boyutlu dizilerde iç bir elemana bu biçimde değer verebiliriz. Ya da dış elemana da küme parantezleri ile değer verebiliriz. 
    Her zaman designated initializer sonrasındaki eleman son verilen elemandan itibaren devam ettrilmektedir. Örneğin:
    
    int a[4][3] = {[1][0] = 3, 5, [2] = {1, 2}, 4};

    Burada 3 değeri dizinin [1][0]  elemanına yerleştirilmiştir. Bu durumda 5 değeri [1][1] elemanına yerleştirilecektir. 
    {1, 2} ilkdeğeri dizinin 2'inci elemanı olan diziye yerleştirilmiştir. Sonraki eleman artık [3][0] elemanı olduğu için 
    4 değeri bu elamana yerleitirilmiştir. Dolayısıyla oluşturulan matris şöyledir:

    0 0 0
    3 5 0
    1 2 0
    4 0 0
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[4][3] = {[1][0] = 3, 5,[2] = {1, 2}, 4};

    for (int i = 0; i < 4; ++i) {
        for (int k = 0; k < 3; ++k)
            printf("%d ", a[i][k]);
        putchar('\n');
    }

    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
    Yukarıda belirttiğimiz gibi çok boyutlu diziler aslında dizi dizilerdir. Örneğin:

    int a[4][3];

    Burada her biri 3 elemandan oluşan 4 elemanlık bir dizi söz konusudur. a[i] ifadesi de aslında bu dizi dizisinin 
    i'inci elemanına ilişkin diziyi belirtmektedir. O halde a[i][k] ifadesi aslında a dizi dizisinin i'inci elemanın 
    belirttiği dizinin k'ıncı elemanıdır. Tabii burada yine a[i] ifadesi bir nesne belirtmez. Yani bu ifadeye atama 
    yapamayız. Bu ifade kod içerisinde kullanıldığında dizi dizsinin i'inci elemanına ilişkin dizinin başlangıç adresi 
    anlamına gelmektedir. İki boyutlu dizileri gösteri dizileri ile karıştırmayınız. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de "dizi göstericileri (pointer to array)" biçiminde ilginç bir gösterici türü vardır. Bir dizi göstericisi dizinin türü T ve uzunluğu N olmak üzere
    aşağıdaki gibi tanımlanmaktadır:

    T (*p)[N];

    Buradaki normal parantezler p'nin bir dizi göstericisi olduğunu belirtmektedir. Eğer bu parantezler olmasaydı p "gösterici dizisi" belirtirdi:

    T *p[N];

    Bir dizi göstericisi * operatörüyle kullanıldığında tek bir eleman değil dizinin tamamı elde edilmektedir. Örneğin:

    int (*pai)[5];

    Burada *pai ifadesi bir nesne belirtmez. Sanki bir dizi ismi gibi ele alınmaktadır. *pai ifadesi adeta bir dizinin ismi gibi düşünülmelidir. 
    
    Anımsanacağı gibi diziler C'de bir nesne belirtirler fakat dizilerin isimleri ifade içerisinde kullanıldığında artık o ifade dizinin ilk 
    elemanının adresini belirtmektedir. C'de bir dizi nesnesinin adresi & operatörüyle alınabilir. Bu durumda elde ed,len adres bir dizi göstericisine 
    yerleştirilmektedir. Örneğin:

    int a[5];
    int (*pai)[5];
    int *pi;

    pai = &a;	/* geçerli */
    pi = a;		/* geçerli */
    pi = &a;	/* geçersiz! */

    C'de bir dizinin adresi programcı tarafından nadiren alınır. Ancak dizinin bu biçimde adresi alınmışsa onun bir dizi göstericisine yerleştirilmesi 
    gerekir. Bir dizinin adresi aynı uzunlukta bir göstericisine yerleştirilmelidir. Örneğin:

    int a[5];
    int (*pai)[6];

    pai = &a;		/* geçersiz! */

    Dizi göstericileri tanımlarken köşeli parantezin için boş bırakılamaz. Örneğin:

    int (*pai)[];		/* geçersiz! */

    Bir dizinin adresi alındığında elde edilen tür sembolik olarak T (*)[N] ile gösterilmektedir. Örneğin:

    int a[5];

    Burada a dizisi int[5] türündendir. a bir ifade içerisinde kullanıldığında int * türüne dönüştürülür. a'nın adresi 
    alındığında (yani &a ifadesi) int (*)[5] türündendir. 

    Bir dizi göstericisi * operatörüyle kullanıldığında bu ifade bir nesne belirtmez. Bir dizi belirtir. Örneği:

    int a[5];
    int (*pai)[5];

    pai = &a;		/* geçerli */
    *pai = 10;		/* geçersiz *pai bir nesne belirtmiyor */

    Biz *pai ifadesi ile dizinin tamamına erişmiş gibi oluruz. Onun da elemanlarına erişmemiz gerekir. Örneğin:

    int a[5] = {1, 2, 3, 4, 5};
    int (*pai)[5];
    int *pi;

    pai = &a;
    
    (*pai)[1] = 10;		/* dizinin 1'inci indisli elemanı güncelleniyor */
    
    Burada (*pai)[1] ifadesinde *pai için parantez kullanıldığına dikkat ediniz. Bu parantezler olmasaydı önce [] operatörü yapılırdı bu da başka bir 
    anlama gelirdir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizinin ismi dizinin ilk elemanın adresi anlamına geliyordu. Yani dizinin ismi dizi elemanı türünden bir adres belirtiyordu. örneğin:

    int a[3][2];

    Burada a dizisi aslında 2 elemanlı int dizilerin dizisidr. Bu durumda bu dizinin ismi ilk elemanın adresi olacağına göre ancak 2 elemanlı bir dizi göstericisine
    atanabilir. Başka bir deyişle biz burada a ismini kifade içerisinde kullandığımızda aslında int[2] türünden bir dizinin adresini kullanmış gibi oluruz. 
    O halde biz çok boyutlu dizilerin isismlerini göstericiyi gösteren göstericilere değil, normal göstericilere de değil, ancak dizi göstericilerine
    atayabiliriz. Örneğin:

    int a[3][2];
    int (*pai)[2];

    pai = a;	/* geçerli 

    Burada dizi göstericisinin matrisin sütun uzunluğuna ilişkin olduğuna dikkat ediniz.Pekiyi burada pai nereyi gösteriyor? Tabii ki dizi dizisinin 
    ilk dizisini gösteriyor. Bir dizi dizisi 1 artırılırsa adresin sayısal bileşeni dizinin uzunluğu kadar artacaktır. Bu durumda pai[0], pai[1] ve pai[2] 
    aslında bu dizi dizisinin elemanları olan dizileri belirtmektedir. Tabii bu ifadelerin hiçbiri nesne belirtmez. O halde biz çok boyutlu bir dizinin 
    ismini bir dizi göstericisine atayıp o dizi göstericisini iki köşeli parantez operatörü ile kullanabiliriz. Örneğin:

    int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
    int (*pai)[2];

    pai = a;

    for (int i = 0; i < 3; ++i) {
        for (int k = 0; k < 2; ++k)
            printf("%d ", pai[i][k]);
        printf("\n");
    }

    Burada bir noktaya dikkatinizi çekmek istiyoruz. T a[N][K] gibi bir matrisin ismine biz ancak T (*pa)[K] biçiminde 
    bir dizi göstericisine atayabiliriz. Yani dizi göstericimizin bildiriminde belirtilen uzunluğun matrisin sütun uzunluğu
    ile aynı olması gerekmektedir. Tüm matrislerin atanabileceği genel bir gösterici yoktur. Örneğin:

    int a[3][2];
    int b[5][2];
    int c[5][3];

    int (*pai)[2];

    pai = a;		/* geçerli */
    pai = b;		/* geçerli */
    pai = c;		/* geçersiz */
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
    int(*pai)[2];

    pai = a;

    for (int i = 0; i < 3; ++i) {
        for (int k = 0; k < 2; ++k)
            printf("%d ", pai[i][k]);
        printf("\n");
    }

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir matrisi fonksiyona parametre yoluyla geçireseksek fonksiyonun parametre değişkeninin dizi göstericisi olması gerekir. Dizi göstericilerini tanımlarken 
    köşeli parantez içerisinde uzunluk belirtilmek zorunda olduğumuza göre böyle fonksiyonlar ancak sütun uzunluğu belirlenmiş olan matrisleri kabul edebilirler. 
    Örneğin:

    void disp_matrix(const int (*pai)[2], int size)
    {
        ...
    }

    Biz burada fonksiyona herhangi bir int türden matrisi geçiremeyiz. Sütun uzunluğu 2 olan int türden matrisleri geçirebiliriz. 
    
    Pekiyi aşağıdaki gibi üç boyutlu bir diziyi aktaracağımız fonksiyonun parametre değişkeni nasıl olmalıdır?

    int a[3][4][2];

    İşte çok boyutlu bir dizi göstericisi de söz konusu olabilir. Örneğin:

    int (*pai)[4][2];

    Bu dizi göstericisi her elemanı 4x2 olan bir matrisi gösteren göstericidir. O halde yukarıdaki üç boyutlu diziyi alacak olan fonksiyonun parametrik yapısı şöyle
    olmalıdır:

    void foo(int (*pai)[4][2] , int size)
    {
    }
    ...
    foo(a, 3);

    Özetle n boyutlu bir matrisin ismini atayabileceğimiz bir göstericinin ilk boyut uzunluğu dışındaki tüm boyut uzunluklarının 
    aynı olması gerekir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void disp_matrix(const int(*pai)[2], int size)
{
    for (size_t i = 0; i < size; ++i) {
        for (int k = 0; k < 2; ++k)
            printf("%d ", pai[i][k]);
        printf("\n");
    }
}

int main(void)
{
    int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};

    disp_matrix(a, 3);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıda belirttiğimiz gibi bir matrisi fonksiyona adres yoluyla aktaracaksak fonksiyonun parametresi matrisin sütun uzunluğu kadar olan 
    bir göstericisi olmalıdır. O halde biz her türlü matrisi aktaracağımız bir fonksiyonu dizi göstericisi yoluyla oluşturamayız. Örneğin:

    void foo(int (*pa)[5])
    {
        /* ... */
    }

    Biz bu fonksiyona sütun uzunşuğu 5 olan matrisleri aktarabiliriz. Sütun uzunluğu farklı olan matrisleri aktaramayız. 

    Pekiyi biz bu durumda satır ve sütun uzunluğu herhangi biçimde olan bir matrisi alan bir fonksiyonu nasıl yazabiliriz? Yani örneğin aşağıdaki gibi iki matrisi de 
    parametre olarak alabilen bir fonksiyon yazılabilir mi?

    int a[3][2];
    int b[5][4];

    Maalesef C'de dizi göstericileriyle ya da başka yöntemlerle bunu yapmanın pratik bir yolu yoktur. Bunu sağlamanın tek yolu fonksiyonun parametresinin bir 
    void gösterici olması, sonra bu void göstericinin fonksiyon içerisinde ilgili türden bir göstericiye atanması ve sonra da bu gösterici yoluyla matrisin bellek 
    organizasyonu bilindiğine göre elemanlara erişilmesidir. Örneğin:

    void foo(void *pmatrix, size_t rowsize, size_t colsize)
    {
        int *pi = (const int *)pmatrix;

        /* ... */
    }

    Burada foo fonksiyonu içerisinde biz artık matrisin i'inci satır k'ıncı sütun elemanına pi[i * colsize + k] ifadesiyle erişlebiliriz. 
    Örneğin:

    int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
    int b[4][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}};

    foo(a, 3, 2);
    foo(b, 4, 3);

    Dizi göstericilerinde köşeli parantez içinin sabit ifadesi olması gerekir. Aşağıdaki gibi bir bildirim geçerli değildir:

    void foo(size_t rowsize, size_t colsize, int (*pai)[colsize]);		/* geçerli değil */

    Satır ve sütun uzunluklarıyla bir matrisi alan fonksiyonun parametresi bir göstericiyi gösteren gösterici olamaz. Göstericiyi gösteren göstericinin 
    gösterdiği yerde bir göstericinin olması gerekir. 

    Bu biçimde genel bir fonksiyonu yazmanın aşağıdakinden daha pratik bir yolu yoktur.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void disp_matrix(const void *pmatrix, size_t rowsize, size_t colsize)
{
    const int *pi = (const int *)pmatrix;

    for (size_t i = 0; i < rowsize; ++i) {
        for (size_t k = 0; k < colsize; ++k) 
            printf("%d ", pi[colsize * i + k]);
        printf("\n");
    }
}

int main(void)
{
    int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
    int b[4][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}};

    disp_matrix(a, 3, 2);
    disp_matrix(b, 4, 3);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Tabii çok boyutlu dizilerin tüm elemanları bellekte ardışıl olduğuna göre çok boyutlu diziler için de dinamik tahsisat yapılabilir. 
    Örneğin biz 3x2 boyutlarında int türden bir matris için aşağıdaki gibi dinamik tahsisat yapabiliriz:

    int (*pa)[2];

    pa = (int (*)[2]) malloc(sizeof(int) * 3 * 2);

    /* ... */

    free(pa);

    Artık biz bu pa gösterici yoluyla tahsis edilen alanı bir matris gibi kullanabiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

void disp_matrix(int **pmatrix, size_t rowsize, size_t colsize)
{
    const int *pi = (const int *)pmatrix;

    for (size_t i = 0; i < rowsize; ++i) {
        for (size_t k = 0; k < colsize; ++k) 
            printf("%d ", pi[colsize * i + k]);
        printf("\n");
    }
}

int main(void)
{
    int (*pai)[2];

    if ((pai = (int(*)[2]) malloc(sizeof(int) * 3 * 2)) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < 3; ++i) 
        for (int k = 0; k < 2; ++k)
            pai[i][k] = i + k;
        

    for (int i = 0; i < 3; ++i) {
        for (int k = 0; k < 2; ++k)
            printf("%d ", pai[i][k]);
        printf("\n");
    }

    free(pai);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir matris etkisi göstericiyi gösteren göstericilerle de dolaylı bir biçimde sağlanabilir. Örneğin ppi N elemanlı int türden bir göstericisi dizisini 
    gösteriyor olabilir. Bu gösterici dizisinin her elemanı da birer int diziyi gösteriyor olabilir. 
    
    ppi -------> 
                ptr ----> dizi elemanları
                ptr ----> dizi dizi elemanları
                ptr ----> dizi dizi elemanları
                ...
                ptr ----> dizi dizi elemanları
    
    Bu durumda biz ppi[i][k] ifadesi ile aslında gösterici dizisinin i'inci elemanının gösteridği dizinin k'ıncı elemanına erişmiş oluruz. 
    Bu da bir çeşit matris erişimi gibi olur. Pekiyi bir matris göstericiyi gösteren gösterici yoluyla ve çok boyutlu dizi yoluyla oluşturulabiliyorsa 
    bunların arasındaki farklar nasıldır?

    - Göstericiyi gösteren gösterici yolu ile matris tasarımında matris toplamda daha fazla yer kaplamaktadır . Çünkü bu tasarımda gösterici dizisinin kendisi de 
    yer kaplar. 

    - Çok boyutlu dizilerde her satırda eşit sayıda sütun bulunmak zorundadır. Oysa gösterici dizisi yönteminde her satırda eşit sayıda sütun elemanı bulunmak zorunda değildir. 

    - Gösterici dizilerinde elemana erişim matrislerdeki elemana erişimden (nano düzeyde) daha yavaş olmaktadır. Çünkü elemana erilişirken bir makine komutu daha
    kullanılmak durumundadır.

    - Göstericiyi gösteren gösterici yoluyla matris oluştururken matris elemanaları birbirinden uzak yerlerde olabilmektedir. Bir veri yapısında elemanların birbirlerinden
    uzak ya da yakın olmasına İngilizce "locality of reference" denilmektedir. Elemanların birbiriden uzak olması genel olarak eleman erişimini yavaşlatabilmektedir. 

    - Göstericiyi gösteren göstericilerle matrisler oluşturulduğunda matrisin her satırı eşit sayıda eleman içermek zorunda değildir. Yani her satır 
    farklı uzunlukta elemanlar içerebilir. Halbuki çok boyutlu dizilerde bütün boyutların aynı uzunlukta olması gerekir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                            67. Ders - 16/02/2023 - Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Göstericiler konusunda nispeten karmaşık olan bir konu da "fonksiyon göstericileri (pointer to function)" konusudur. Bu bölümde fonksiyon göstericileri üzerinde
    duracağız. Ancak bu konu daha derinlemesine ve uygulamalı olarak "Sistem Programlama ve İleri C Uygulamaları 1" kursunda ele alınmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyon makine kodlarından oluşmaktadır. Bu makine kodları da bellekte ardışıl bir biçimde bulunmaktadır. Yani bir fonksiyonun makine kodları 
    bellekte ardışıl byte topluluğu biçiminde bulunur. O halde fonksiyonların da başlangıç adreslerinden bahsedilebilir. Bir fonksiyonun başlangıç adresi
    onun makine kodlarının bellekte bulunduğu yerin başlangıç adresidir. Fonksiyonları çağırmak için makine dilinde CALL isimli makine komutları bulunmaktadır.
    Bu CALL komutları fonksiyonun başlangıç adresini alır ve işlemcinin o adresten itibaren komut çalıştırmasını sağlar. Örneğin:

    CALL <adres>

    Biz C'de adresleri "data" ve "kod" adresleri biçiminde ikiye ayırabiliriz. Şimdiye kadar gördüğümüz adreslerin hepsi "data" adresiydi. Data adresi 
    demekle fonksiyon olmayan nesnelerin adresleri kastedilmektedir. 

    C'de fonksiyonların başlangıç adreslerini gösteren göstericilere "fonksiyon göstericileri" denilmektedir. Bir fonksiyon göstericisi tanımlamanın genel 
    biçimi şöyledir:

    <geri_dönüş_değerinin_türü> (* <gösterici_ismi>)([parametre bildirimi]);

    Örneğin:

    void (*pf1)(void);
    int (*pf2)(int, int);
    double (*pf3)(double, double);

    Fonksiyon göstericileri her türlü fonksiyonları gösteremez. Geri dönüş değeri ve parametre türleri belli biçimde olan fonksiyonları gösterebilir. 
    Örneğin:

    int (*pf)(int, int);

    Burada pf fonksiyon göstericisi "geri dönüş değeri int olan, parametresi int, int olan" fonksiyonları gösterebilir. Yani biz bu fonksiyon göstericisinin 
    içerisine "geri dönüş değeri int olan, parametreleri int, int olan fonksiyonların" adreslerini yerleştirebilir.z Fonksiyon göstyericileri bildirilirken 
    paramtre değişkenleri için isimler de verilebilir. Bu isimler derleyici tarafından kullanılmaz. Yalnızca okunabilirliği artırmak için kullanılmaktadır. Örneğin:

    int (*pf)(int a, int b);

    Tabii buradaki parametre isimlerinin prototipteki ya da tanımlamadaki parametre isimleriyle uyuşması gerekmemektedir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aslında C standartlarına göre fonksiyonlar da bir çeşit nesne gibidir. Bir fonksiyonun ismi bir ifadede kullanıldığında artık bu isim o fonksiyonun başlangıç adresi
    anlamına gelir. Buna standartlarda "function to pointer conversion" denilmektedir. O halde C'de bir fonksiyonun yalnızca ismi (parantezler olmadan) o fonksiyonun 
    bellekteki başlangıç adresini belirtmektedir. Aslında fonksiyonu çağırırken kullandığımız parantezler "falanca adresteki fonksiyonu çağır" anlamına gelmektedir. 
    Örneğin:

    void foo(void)
    {
        printf("foo\n");
    }

    Burada biz yalnızca "foo" ifadesini kullandığımızda bu ifade "foo fonksiyonunun bellekteki başlangıç adresi" anlamına gelir. foo() ifadesi de "foo adresindeki
    kodu çalıştır" anlamına gelmektedir. Fonksiyon çağırma operatörü "tek operandlı sonek (unary postfix)" bir operatördür. Bu operatörün operandının bir 
    fonksiyon adresi olması gerekir. 
        
    Bir fonksiyon göstericisine her fonksiyonun adresi yerleştirilemez. Geri dönüş değeri ve parametrik yapısı uyumlu olan fonksiyonların adresleri yerleştirilebilir. 
    Örneğin:

    void foo(void)
    {
        /* ... */
    }

    void bar(int a)
    {
        printf("bar\n");
    }

    int tar(int a, int b)
    {
        /* .... */
    }

    int (*pf)(int, int);

    pf = foo;		/* geçersiz! */
    pf = bar;		/* geçersiz */
    pf = tar;		/* geçerli */

    Fonksiyon göstericisine atama yapılırken yanlışlıkla parantez kullanmayınız. Örneğin:

    pf = tar();		/* geçersiz! */

    Burada tar fonksiyonun adresi pf'ye atanmamaktadır, tar fonksiyonunun geri dönüş değeri pf'ye atanmaktadır. Bir fonksiyon göstericisine bir data adresi de
    atanamaz. Ancak uyumlu bir fonksiyonun adresi atanabilir. Örneğin:

    int *pi;
    int (*pf)(int, int);
    ...

    pf = pi;		/* geçersiz! */

    pf bir fonksiyon göstericisi olmak üzere bu göstericinin içerisindeki adreste bulunan fonksiyonu çağırabilmek için yine fonksiyon çağırma operatörü kullanılır. 
    Tabii fonksiyonun parametreleri varsa yine argümanlar parantez içerisine yazılmalıdır. Örneğin:

    #include <stdio.h>
    #include <shellapi.h>

    void foo(void)
    {
        printf("foo\n");
    }

    int main(void)
    {
        void (*pf)(void);

        pf = foo;

        pf();       /* pf göstericisinin içerisindeki adreste bulunan fonksiyonu çağır */
        
        return 0;
    }

    C'de aslında resmi olarak pf bir fonksiyon göstericisi olmak üzere pf göstericisinin içerisindeki adreste bulunan fonksiyonu çağırmak için iki sentaktik
    biçim vardır: pf(...) ve (*pf)(...). Örneğin:

    void foo(void)
    {
        printf("foo\n");
    }
    ...
    void (*pf)(void);

    pf = foo;

    pf();
    (*pf)();

    Programcılar tarafından pf(..) biçimindeki doğal çağrım daha fazla tercih edilmektedir. 
    
    Tabii bir fonksiyon göstericisine ilkdeğer de verilebilir. Örneğin:

    void (*pf)(void) = foo;
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte fonksiyon göstericisine onunla uyumlu fonksiyonların adresleri atanarak fonksiyon göstericisi yoluyla bu fonksiyonlar çağrılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int mul(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}

int main(void)
{
    int (*pf)(int, int);
    int result;

    pf = add;

    result = pf(20, 10);
    printf("%d\n", result);     /* 30 */

    pf = sub;

    result = pf(20, 10);
    printf("%d\n", result);     /* 10 */

    pf = mul;

    result = pf(20, 10);       /* 2000 */
    printf("%d\n", result);

    pf = div;

    result = pf(20, 10);
    printf("%d\n", result);     /* 2 */
      
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyon gösterici dizisi de söz konusu olabilir. Örneğin:

    void (*pfs[5])(void);

    Burada pfs her elemanı ""geri dönüş değeri void olan parametresi void"" olan bir fonksiyonu gösteren gösterici dizisidir. 

    Aşağıdaki örnekte bir fonksiyon gösterici dizisine çeşitli fonksiyonların adresleri yerleştirilmiş sonra döngü içerisinde bu fonksiyonlar
    çağrılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int mul(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}

int main(void)
{
    int (*pfs[4])(int, int) = {add, sub, mul, div};
    int result;

    for (int i = 0; i < 4; ++i) {
        result = pfs[i](20, 10);
        printf("%d\n", result);
    }
      
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon adreslerinin sembolik tür isimleri oluşturulurken * atomu parantez içerisine alınmaktadır. Örneğin:

    int (*pf1)(int, int);
    void (*pf2)(void);
    
    Burada pf1 "int (*)(int, int)" türündendir. pf2 ise "void (*)(void)" türündendir. Örneğin:

    int square(int a)
    {
        return a * a;
    }

    Burada square ismi ifade içerisinde kullanıldığında int (*)(int) türündendir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon adres türleri de typedef edilebilmektedir. Örneğin:

    void (*PF)(void);

    Burada PF'nin türü "void (*) (void)" biçimindedir. Şimdi bildirimin başına typedef getirelim:

    typedef void (*PF)(void);

    Artık PF "void (*)(void)" türünü temsil etmektedir. Örneğin:

    PF pf;

    bu bildirim aşağıdaki ile eşdeğerdir:

    void (*pf)(void);

    Fonksiyon göstericilerine typedef işlemi yazımı kolaylaştırmaktadır. Örneğin:

    PF pfs[3];

    Burada 3 elemanlı, her elemanı void (*)(void) türünden bir fonksiyon adresi tutan dizi tanımlaması yapılmıştır. Yani
    bu tanımlama aşağıdaki ile eşdeğerdir:

    void (*pfs[3])(void);
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int add(int a, int b)
{
    return a + b;
}

int sub(int a, int b)
{
    return a - b;
}

int mul(int a, int b)
{
    return a * b;
}

int div(int a, int b)
{
    return a / b;
}

typedef int (*PF)(int, int);

int main(void)
{
    PF pfs[4] = {add, sub, mul, div};
    int result;

    for (int i = 0; i < 4; ++i) {
        result = pfs[i](20, 10);
        printf("%d\n", result);
    }
      
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun parametresi bir fonksiyon göstericisi olabilir. Örneğin:

    void foo(void (*pf)(void))
    {
        ...
    }

    Burada foo fonksiyonun parametresi bir göstericidir. Ancak bu gösterici bir fonksiyon göstericisidir. foo fonksiyonu "geri dönüş değeri void 
    ve parametresi void" olan bir fonksiyonun adresiyle çağrılmalıdır. Örneğin:

    void bar(void)
    {
        ...
    }
    ...
    foo(bar);

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi fonksiyon göstericilerine neden gereksinim duyulmaktadır? İşet bunun temelde iki nedeni vardır:

    1) Callback fonksiyon mekanizmasını oluşturabilmek.
    2) Fonksiyonları işlevsel olarak genelleştirmek

    Sistem programlamada "bir olay olduğunda çağrılması istenen fonksiyona callback fonksiyon" denilmektedir. Örneğin GUI uygulamalarında bir düğmeye 
    tıklandığında birşey yapılması istenebilir. Ya da örneğin bir dizin içerisinde her dosya bulunduğunda onun üzerinde bir şey yapılması istenebilir. 
    Ya da örneğin belli bir zaman dolduğunda bir fonksiyonun çağrılması istenebilir. Tüm bunları sağlayabilmek için "fonksiyon göstericisi" kavramının 
    bulunuyor olması gerekir. 

    Aşağıdaki örnekte set_alarm isimli fonksiyon belli bir zamanda verilen fonksiyonu çağırmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <time.h>

void set_alarm(time_t target, void (*pf)(void))
{
    time_t t;

    for (;;) {
        t = time(NULL);
        if (t == target) {
            pf();
            break;
        }
    }
}

void foo(void)
{
    printf("Ok\n");
}

int main(void)
{
    time_t t;

    t = time(NULL) + 10;

    set_alarm(t, foo);

    printf("continues..\n");
      
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon göstericileri "genelleştirme" için de sıkça kullanılmaktadır. Aşağıdaki örnekte for_each isimli fonksiyon int türden bir dizinin adresini,
    onun eleman sayısını ve bir de callback fonksiyon adresini almaktadır. for_each fonksiyonu dizinin her elemanının adresi ile callback fonksiyonu 
    çağırmaktadır. Callback fonksiyonun ikinci parametresi çağrımın sonuna gelinip gelinmediğini belirtmektedir. Bu parametre FOREACH_PROGRESS ya da 
    FOREACH_END değerlerini almaktadır. Callback fonksiyonun dizi elemanlarının adresleriyle çağrıldığına dikkat ediniz. Bu sayede callback fonksiyon 
    dizi elemanları üzerinde değişiklikler yapabilmektedir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

enum { 
    FOREACH_PROGRESS, 
    FOREACH_END
};

void for_each(int *pi, size_t size, void (*pf)(int *, int))
{
    size_t i;

    for (i = 0; i < size - 1; ++i)
        pf(&pi[i], FOREACH_PROGRESS);

    pf(&pi[i], FOREACH_END);
}

void disp(int *pi, int status)
{
    printf("%d", *pi);
        
    putchar(status == FOREACH_PROGRESS ? ' ' : '\n');
}

void square(int *pi)
{
    *pi = *pi * *pi;
}

int main(void)
{
    int a[5] = {1, 2, 3, 4, 5};

    for_each(a, 5, disp);
    for_each (a, 5, square);
    for_each(a, 5, disp);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon göstericilerinin kullanımına diğer bir örnek de standart atexit fonksiyonudur. Bu fonksiyonun prototipi 
    <stdlib.h> dosyası içerisinde bulunmaktadır:

    int atexit(void (*function)(void));

    Fonksiyon parametre olarak geri dönüş değeri void ve parametresi void olan bir fonksiyonun adresini almaktadır. atexit fonksiyonu kendisine verilen 
    fonksiyonları saklar. Program sonlanırken bunları "ters sırada" çağırır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda sıfır dışı 
    bir değere geri dönmektedir. Fonksiyonun başarısız olmasının tek nedeni fazla sayıda fonksiyonun register ettirilmesi olabilir.

    atexit fonksiyonun amacı program sonlanırken belli boşaltım işlemlerinin otomatik çağrılan bir fonksiyona yaptırılmasıdır. atexit fonksiyonunun 
    kendisine verilen fonksiyonları ters sırada çağırdığına dikkat ediniz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

void foo(void)
{
    printf("foo\n");
}

void bar(void)
{
    printf("bar\n");
}

int main(void)
{
    atexit(foo);
    atexit(bar);

    printf("main ends...\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    atexit fonksiyonu callback fonksiyon için parametre almadığından ancak bu fonksiyona verilen callback fonksiyonlar 
    global değişkenleri kullanabilir. Fonksiyonun bu bakımdan tasarımı biraz kusurlu gözükmektedir. Fonksiyon bizden 
    aynı zamanda callback fonksiyona geçirilecek değeri de parametrleri de alsaydı bu durumda daha faydalı kullanıma 
    yol açardı. Bu tür durumlarda callback fonksiyona geçirilecek değerin void gösterici olması anlamlıdır. Çünkü void
    göstericiler genel bir bilgi tutucu olarak kullanılabilirler. 
    
    Burada belirttiğimiz ana fikri anlatan örnek kodu aşağıda veriyoruz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

#define MAX_ATEXIT      1024

void (*g_pfs[MAX_ATEXIT])(void *);
void *g_ptrs[MAX_ATEXIT];
int g_count;

void myatexit(void (*pf)(void *), void *ptr)
{
    g_pfs[g_count] = pf;
    g_ptrs[g_count] = ptr;
    ++g_count;
}

void myexit(int status)
{
    for (int i = g_count - 1; i >= 0; --i)
        g_pfs[i](g_ptrs[i]);
    
    exit(status);
}

void free_mem(void *pv)
{
    printf("memory frees...\n");

    free(pv);
}

void free_some_resource(void *pv)
{
    int handle = (int)pv;

    printf("resource frees with %d handle...\n", handle);
}

int main(void)
{
    int *pi1;
    int *pi2;

    if ((pi1 = (int *)malloc(sizeof(int))) == NULL)
        myexit(EXIT_FAILURE);

    myatexit(free_mem, pi1);

    if ((pi2 = (int *)malloc(sizeof(int))) == NULL)
        myexit(EXIT_FAILURE);

    myatexit(free_mem, pi2);

    myatexit(free_some_resource, (void *)123);

    myexit(EXIT_SUCCESS);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                        68. Ders - 21/02/2023 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon adresleri tür dönüştürme operatörüyle bile data adreslerine, data adresleri de fonksiyon adreslerine dönüşürülememktedir. Örneğin:

    int *pi;
    void (*pf)(void);
    ...

    pf = (void (*)(void))pi;		/* geçersiz! */

    Benzer biçimde örneğin:

    int *pi;
    void (*pf)(void);
    ...

    pi = (int *)pf;		/* geçersiz! */

    C'de void göstericiler data göstericileridir. Yani biz C'de void göstericiye herhangi türden bir data adresini atayabiliriz. Ancak fonksiyon adresini atayamayız. 
    Örneğin:

    void foo(void)
    {
        ...
    }

    void *pv;

    pv = foo;		/* geçersiz! */

    Benzer biçimde void bir adres de herhangi türden bir göstericiye tür dönüştürme operatörü uygulansa bile atanamaz. Zira C'de data adreslerinden fonksiyon adreslerine,
    fonksiyon adreslerinden data adreslerine tür dönüştürmesi yoktur. C'de her türlü fonksiyonun adresini atayabileceğimiz void bir fonksion göstericisi de yoktur. 
    Ancak tür dönüştürmesi yoluyla bir fonksiyon adresi başka bir fonksiyon adresine dönüştürülebilmektedir. Örneğin:

    int add(int a, int b)
    {
        return a + b;
    }

    void (*pf)(void);

    pf = add;						/* geçersiz! */

    pf = (void (*)(void))add;		/* geçerli */

    Tabii bu tür durumlarda yazımı kolaylaştırmak için fonksiyon adresleri typedef edilebilir. Örneğin:

    typedef void (*PF)(void);
    
    int add(int a, int b)
    {
        return a + b;
    }

    PF pf;

    pf = (PF)add;		/* geçerli */

    C'de bir fonksiyonun adresini almakla doğrudan fonksiyonun ismini kullanmak arasında standartlara göre hiçbir farklılık yoktur.
    Örneğin foo bir fonksiyon ismi olmak üzere foo ifadesi ile &foo ifadesi tamamen eşdeğerdir. 

    Bir fonksiyon adresinin bir data adresine bir adata adresinin fonksiyon adresine dönüştürülmesinin doğrudan bir yolu yoktur. 
    Ancak derleyicilerin önemli bir bölümü bu tür dönüştürmelere standartlarca yasak olmasına karşın ses çıkartmamaktadır. 
    Çok seyrek olarak bu biçimde işlemlerin yapılması gerektiğinde derleyicilerin bunu kabul ettiğini göreceksiniz. 
    Ancak C'de aşağıdaki gibi dolambaçlı bir arkdadan dolaşma yöntemiyle standartlara uygun bir biçimde bir fonksiyon 
    göstericisine bir data adresei atanabilmektedir:

    int a;
    void (*pf)(void);

    *(void **)&pf = &a;

    Burada pf bir fonksiyon göstericisidir ve türü int (*)(void) biçimdedir. Ancak biz buun adresini aldığımızda artık 
    elde ettiğimiz adres bir fonksiyon değil data adresi olur. Dolayısıyla iki data adresini tür dönüştürme operatörü ile 
    dönüştürebiliriz. *(void **)&pf ifadesi void bir gösterici olduğuna göre oraya herhangi bir data nesnesinin adresini 
    atayabiliriz. Bu öntem standratlarda geçerli olsa da aslında bir arkadan dolaşma işlemidir. Tabii bunun tersinini de 
    yapabiliriz. Örneğin biz void göstericiye doğrudan bir fonksiyonun adresini atayamayız ancak bunu aşağıdaki dolayı 
    vir biçimde yapabiliriz:

    void foo(void);
    void *pv;

    *(void (**)(void))&pv = foo;

    Örneğin POSIX sistemlerinde dinamik kütüphanelerin içerisindeki data nesnelerinin ve fonksiyonların adreslerini veren 
    dlsym isimli bir fonksiyon vardır. Bu fonksiyonun prototipi şöyledir:

    void *dlsym(void *handle, const char *name);

    Fonksiyon bize aslında data adresi de fonksiyon adresi de verebilmektedir. Eğer biz bu fonksiyonla bir fonksiyon adresi elde 
    etmek istesek fonksiyon bize fonksiyon adresini bir data adresi gibi vermektedir. gcc ve clang derleyicileri aşağıdaki
    dönüştürmeye kızmıyor olsalar da aslında bu dönüştürme standartlarca geçerli değildir:

    int (*add)(int, int);

    add = (int (*)(int, int))dlsym(handle, "add");		/* C'de aslında geçersiz! */

    İşte biz bu tür durumlarda yukarıda bahsettiğimiz arakadan dolaşmayı uygulayabiliriz:

    int (*add)(int, int);

    *(void **)&add = dlsym(handle, "add");		
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon göstericileri ile ilgili C'de şöyle bir ayrıntı vardır: Bir fonksiyon göstericisinde parametre parantezlerinin içinin boş bırakılmasıyla oraya 
    void yazılması farklı anlamlara gelmektedir. Parametre parantezinin içi boş bırakılırsa bu biçimde tanımlanmış olan fonksiyon göstericilerine 
    geri dönüş değeri uygun olmak koşulu ile herhangi bir parametrik yapıya sahip fonksiyonların adresleri atanabilmektedir. Örneğin:

    void (*pf)();

    Buradaki pf göstericisine "geri dönüş değeri void olmak üzere herhangi bir parametrik yapıya sahip fonksiyonun adresi" atanabilir. 
    Oysa parametre parantezinin içine void yazılırsa bu dururmda göstericiye ancak parametresi olmayan fonksiyonların adresleri atanabilir. 
    Örneğin:

    void (*pf)(void);

    Burada pf göstericisine "geri dönüş değeri void olan, parametresi olmayan" fonksiyonların adresleri atanabilir. Örneğin:
    
    void foo(void)
    {
        ...
    }

    void bar(int a)
    {
        ...
    }

    int tar(int a)
    {
        ...
    }

    void (*pf)(void);

    pf = foo;		/* geçerli */
    pf = bar;		/* geçersiz! */
    pf = tar;		/* geçersiz! */

    Ancak örneğin:

    void (*pf)();

    pf = foo;		/* geçerli */
    pf = bar;		/* geçerli */
    pf = tar;		/* geçersiz! */

    Ancak C++'ta parametre parantezinin için boş bırakılmasıyla void yazılması arasında bir farklılık yoktur. İkisi de 
    "parametresi olmayan fonksiyonun" adresinin atanabileceği anlamına gelir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Fonksiyon göstericilerinin kullanımına tipik bir örnek qsort isimli standart C fonksiyonudur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir byte'tan uzun olan tamsayı türünden nesnelerin bellekteki yerleşimleri işlemciden işlemciye değişebilmektedir. Buna işlemcinin "endian'lığı (endianness)"
    denilmektedir. Intel işlemcilerinde sayının düşük anlamlı byte'ı düşük adreste olacak biçimde yerleşim uygulanmaktadır. Power PC, SPARC, Alpha 
    gibi işlemcilerde sayının yüksek anlamlı byte değeri düşük adreste tutulur. Örneğin:

    int a = 0x12345678;

    Intel işlemcilerinde bu int nesne içerisindeki byte'lar şöyle tutulacaktır:

    78
    56
    34
    12

    Halbuki PowerPC işlemcilerinde tutulma şöyle olacaktır:

    12
    34
    56
    78

    Sayının düşük anlamlı byte değerinin düşük adreste tutulmasına "little endian", sayının yüksek anlamlı byte değerinin düşük adreste tutulmasına ise
    "big endian" yerleşim denilmektedir. Burada "endian" sözcüğü alakasız bir biçimde uydurulmuştur. 

    Bazı işlemciler her iki endian'lığa göre de çalışabilecek biçimde tasarlanmış durumdadır. Örneğin ARM işlemcileri default durumda "little endian" 
    çalışmaktadır. Ancak işlemcinin modunu değiştirerek onu "big endian" çalışacak biçime getirebiliriz. Little edian ve big endian tasarım arasında
    bir performans farklılığı yoktur. 
    
    Endianlık'ta byte bitleri arasında da bir farklılık yoktur. Byte'ların birbirlerine göre dizilimleri arasında farklılık söz konusudur. Biz örneğin bir int nesnenin 
    adresini aldığımızda her zaman en düşük adresi elde ederiz. O adresteki byte'a bakarak işlemcimizin litlle endian mı yoksa big endian mı kullandığını anlayabiliriz. Örneğin:

    int a = 0x12345678;
    unsigned char *pc;

    pc = (unsigned char *)&a;
    printf("%02X\n", *pc);		/* Little endian ise 78, big endian ise 12 görürüz */
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 0x12345678;
    unsigned char *pc;

    pc = (unsigned char *) &a;
    printf("%02X\n", *pc);      /* little endian ise 78, big endian ise 12 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Elimizde unsigned short türden ps isimli bir gösterici olsun. Bu gösterici aşağıdaki byte yığınının başlangıç adresini gösteriyor olsun:

    13
    62
    e3
    4a

    Burada biz *ps dediğimizde ne elde ederiz? Eğer Little Endian bir sistemse bizim 0x6213 elde etmemiz gerekir. Çünkü bu sistemlerde erişim sırasında da 
    düşük adreste düşük anlamlı byte değerinin olduğu varsayılmaktadır. Ancak sistem big endian ise 0x1362 elde ederiz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 0x12345678;
    unsigned short *ps;

    ps = (unsigned short *) &a;
    printf("%04X\n", *ps);      /*  0x5678  */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte ekrana 78, 56, 34, 12 değerleri çıkacaktır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 0x12345678;
    unsigned char *pc;

    pc = (unsigned char *) &a;
    
    for (int i = 0; i < 4; ++i)
        printf("%02X\n", pc[i]);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Birlikler (unions) yapılara benzer ancak onlardan organizasyon bakımından farklı olan bir veri yapılarıdır. Bir birlik union anatar sözcüğü kullanılarak 
    aşağıdaki genel biçim ile bildirilir:

    union [isim] {
        <birlik eleman bildirimi>
    };

    Bildirim konusunda birlik ile yapı arasındaki tek fark struct anahtar sözcüğü yerine union anahtar sözcüğünün kullanılmasıdır. 

    nion türünden nesnelerin tanımlanması da benzer biçimde yapılmaktadır. Birlik türleri "union" anahtar sözcüğü ile birlik isminden oluşur. 
    Örneğin:

    union SAMPLE {
        int a;
        long b;
        double c;
    };

    union SAMPLE s;

    Burada s "union SAMPLE" türündendir. Bir birlğin elemanlarına yine nokta operatörü ile erişilir. Örneğin s.a, s.b ve s.c gibi. Birlikler türünden 
    göstericiler olabilir. Yine gösterici ile birlik elemanlarına -> operatörü ile erişilebilir. Örneğin:

    union SAMPLE s;
    union SAMPLE *ps;

    ps = &s;

    union türleri de benzer biçimde typedef edilebilir. Örneğin:

    typedef union tagSAMPLE {
        int a;
        long b;
        double c;
    } SAMPLE;

    Aynı isimli birlik ve yapı aynı faaliyet alanında bildirilemez. Örneğin:

    union SAMPLE {
        /* ... */
    };

    struct SAMPLE {		/* geçersiz! */
        /* ... */
    };

    Bir birlik nesnesine ilkdeğer verilebilir. Ancak birliğin yalnızca ilk elemanına değer verilebilir. Örneğin:

    union SAMPLE {
        int a;
        long b;
        double c;
    };

    union SAMPLE s = {1, 2, 3};		/* geçersiz! */
    union SAMPLE k = {1};			/* geçerli, değer a elemanına verilmiş */

    C99 ile eklenen "designator inializer" sentaksı ile birliğin herhangi bir elemanına da ilkdeğer verilebilir. Ancak birden fazla elemana yine ilk değer verilemez.
    Örneğin:

    union SAMPLE s = {.c = 1};			/* geçerli! */
    union SAMPLE k = {.b = 1, 2};		/* geçersiz! */

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Birlikler C'de çok seyrek kullanılmaktadır. Birliklerle yapılar arasındaki tek önemli fark elemanların yerleştirilmesi ile ilgilidir. 
    Bir birlik elemanları çakışık yerleştirilmektedir. Yani birliğin her elemanı aynı adresten itibaren çakışık bir biçimde yerleştirilmektedir. Bu nedenle 
    bir birlik nesnesi için birliğin en büyük elemanı kadar yer ayrılır. Örneğin:

    union SAMPLE {
        char a;
        short b;
        int c;
        double d;
    };

    union SAMPLE s;

    Burada s'in elemanları için ayrı yerler ayrılmamaktadır. Bu birliğin en büyük elemanı double türdendir. O da 8 byte'tır. O halde bu birlik nesnesi için 
    8 byte yer ayrılacaktır. Bu 8 byte'ın ilk byte'ı a elemanı, ilk iki byte'ı b elemanı, ilk dört byte'ı c elemanı ve ilk 8 byte'ı d elemanı olacaktır. 
    Örneğin:

    #include <stdio.h>

    union SAMPLE {
        char a;
        short b;
        int c;
        double d;
    };

    int main(void)
    {
        union SAMPLE s;

        printf("%zu\n", sizeof(s));		/* 8 */

        return 0;
    }

    Şüphesiz bir birliğin bir elemanına bir değer atadığımızda diğer elemanların değerlerini de değiştirmiş oluruz. Bu durumda birliğin yalnızca belli bir elemanı 
    belli bir süreçte kullanılmalıdır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Birlikler iki nedenle kullanılmaktadır:

    1) Bir grup bilgiden yalnızca birinin kullanıldığı durumlarda yer kazancı sağlamak için.
    2) Parçalardan bütünü oluşturmak ve bütünü parçalarına ayırmak için.

    Bir grup bilgiden yalnızca birine gereksinim duyulduğu durumlar seyrektir. Örneğin bir kişinin iletişim bilgisi "telefon numarası" ya da "e-posta adresi"
    ya da "TC kimlik numarası" olabilir. Bu bilgilerin hepsinin bulunmasına gerek olmayabilir. Bunlardan yalnızca birinin bulunması yeterli olabilir. Örneğin:

    struct PERSON {
        char name[64];
        int no;
        union {
            char telno[11];
            char tcid[12];
            char email[64];
        } cinfo;
    };

    struct PERSON per;

    Burada per nesnesinin cinfo elemanı bir birliktir. Bu birliğin elemanları çakışık yerleştirilecektir. Çünkü buradaki bilgilerden yalnızca birinin
    bulunması yeterlidir. Pekiyi biz per nesnesi içerisindeki cinfo nesnesinin hangi elemanın set edilmiş olduğunu nasıl anlayabiliriz? Bir birliğin hangi 
    elemanının kullanılıyor olduğunu anlamanın pratik bir yolu yoktur. O zaman bu bilgiyi de yapının içerisinde tutmak gerekir:

    enum { CINFO_TELNO, CINFO_TCID, CINFO_EMAIL };

    struct PERSON {
        char name[64];
        int no;
        int cinfo_flag;
        union {
            char telno[11];
            char tcid[12];
            char email[64];
        } cinfo;
    };

    void disp_person(struct PERSON *per)
    {
        printf("Adi Soyadı: %s\n", per->name);
        printf("No: %d\n", per->no);
        
        switch (per->cinfo_flag) {
            case CINFO_TELNO:
                printf("Tel No: %s\n", per->cinfo.telno);
                break;
            case CINFO_TCID:
                printf("TC Kimlik: %s\n", per->cinfo.tcid);
                break;
            case CINFO_EMAIL:
                printf("E-Posta: %s\n", per->cinfo.email);
                break;
        }
    }
    ...
    struct PERSON per = { "Ali Serce", 123, CINFO_EMAIL, {.email = "aslank@csystem.org"} };

    disp_person(&per);
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>


enum { CINFO_TELNO, CINFO_TCID, CINFO_EMAIL };

struct PERSON {
    char name[64];
    int no;
    int cinfo_flag;
    union {
        char telno[11];
        char tcid[12];
        char email[64];
    } cinfo;
};

void disp_person(struct PERSON *per)
{
    printf("Adi Soyadı: %s\n", per->name);
    printf("No: %d\n", per->no);
    
    switch (per->cinfo_flag) {
        case CINFO_TELNO:
            printf("Tel No: %s\n", per->cinfo.telno);
            break;
        case CINFO_TCID:
            printf("TC Kimlik: %s\n", per->cinfo.tcid);
            break;
        case CINFO_EMAIL:
            printf("E-Posta: %s\n", per->cinfo.email);
            break;
    }
}

int main(void)
{
    struct PERSON per = { "Ali Serce", 123, CINFO_EMAIL, {.email = "aslank@csystem.org"} };

    disp_person(&per);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Parçalardan bütünü oluşturmak ve bütünü parçalara ayrımak için de birlikler kullanılabilmektedir. Aşağıdaki birliğe dikkat ediniz:

    #include <stdint.h>

    union DWORD {
        uint32_t dword;
        uint8_t bytes[4];
    };

    union DWORD x;

    Burada DWORD isimli birliğin iki elemanı vardır: dword ve bytes. Bunlar çakışık bir biçimde yerleştirilecektir. O halde biz x.dword elemanına 4 byte 
    yerleştirdiğimizde bunların parçalarını x.bytes[0], x.bytes[1], x.bytes[2] ve x.bytes[3] elemanlarından alabiliriz. Tabii işlemcinin little endian 
    ya da big endian olup olmadığına göre parçaların sırası da değişebilecektir. 

    Aşağıdaki örnekte biz birliğin dword elemanına 4 byte'lık bir bilgi yerleştirip onları tek tek birliğin bytes elemanından elde ettik. Sonra da bu işlemin 
    tersini yaptık.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>

union DWORD {
    uint32_t dword;
    uint8_t bytes[4];
};

int main(void)
{
    union DWORD x;

    x.dword = 0x12345678;

    printf("%02X %02X %02X %02X\n", x.bytes[0], x.bytes[1], x.bytes[2], x.bytes[3]);	/* 78 56 34 12 */

    for (int i = 0; i < 4; ++i)
        printf("%02X ", x.bytes[i]);
    printf("\n");

    x.bytes[0] = 0x10;
    x.bytes[1] = 0x20;
    x.bytes[2] = 0x30;
    x.bytes[3] = 0x40;

    printf("%08lX\n", (unsigned long)x.dword);		/* 40302010 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aynı şeyi birliğin bir elemanını yapı yaparak da sağlayabiliriz. Örneğin:

    struct BYTES {
        uint8_t b0;
        uint8_t b1;
        uint8_t b2;
        uint8_t b3;
    };

    union DWORD {
        uint32_t dword;
        struct BYTES bytes;
    };

    union DWORD x;

    Burada DWORD x nesnesinin dword ve bytes elemanları yine çakışık yerleştirilecektir. Yapı elemanlarının ilk yazılan eleman düşük adreste olacak biçimde
    ardışıl yerleştirildiğini biliyoruz. Bu durumda biz x.dword elemanına değer atadığımızda onun parçalarını x.bytes.b0, x.bytes.b1, x.bytes.b2 ve 
    x.bytes.b3 ifadeleriyle elde edebiliriz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>

struct BYTES {
    uint8_t b0;
    uint8_t b1;
    uint8_t b2;
    uint8_t b3;
};

union DWORD {
    uint32_t dword;
    struct BYTES bytes;
};

int main(void)
{
    union DWORD x;

    x.dword = 0x12345678;

    printf("%02x %02X %02X %02X\n", x.bytes.b0, x.bytes.b1, x.bytes.b2, x.bytes.b3);	/* 78 56 34 12 */

    x.bytes.b0 = 0x10;
    x.bytes.b1 = 0x20;
    x.bytes.b2 = 0x30;
    x.bytes.b3 = 0x40;

    printf("%08lx\n", (unsigned long)x.dword);		/* 40302010 */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                        69. Ders - 23/02/2023 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bileşik sabitler (compound literals) konusu C'ye C99 ile birlikte eklenmiştir. Ancak C++ bu özelliği benimsememiştir. Bileşik sabitler  
    yapılar ve dizilerin ifade içerisinde oluşturulmasına olanak sağlamaktadır. Örneğin ekrandaki iki nokta arasında doğru çizen
    aşağıdaki gibi bir fonksiyon olsun:

    struct POINT {
        int x;
        int y;
    };

    void draw_line(struct POINT pt1, struct POINT pt2)
    {
        /* ... */
    }

    Normal olarak biz bu fonksiyonu struct POINT türünden iki nesne oluşturarak çağırırız. Örneğin:

    struct POINT pt1 = {3, 4}, pt2 = {13, 21};

    draw_line(pt1, pt2);

    Böylesi çağırmalarda işin başında nesnelerin tanımlanma zorunluğu kodu kalabalık hale getirmektedir. Benzer biçimde bir yapı nesnesinin elemanlarına değer atamanın da
    C90'da ilkdeğer vermenin dışında pratik bir yolu yoktur. Örneğin:

    struct DATE{
        int day, month, year;
    };
    struct DATE date;
    ...

    date.day = 10;
    date.month = 12;
    date.year = 2009;

    Halbuki bu işlemin tek satırda yapılması yalın bir görünüm oluşturacaktır. İşte bileşik sabitler bu tür durumlar için yazım kolaylığı sağlamak amacıyla
    dile eklenmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Her ne kadar burada açıklayacağımız konuya C standartlarında "bileşik sabitlşer (compound literals)" denildiyse de aslında semantik olarak 
    burada ele alacağımız özelliğin sabitle bir ilgisi yoktur. Zaten standartlarda da konu "Expressions" bölümünde ele alınmıştır. Bileşik sabit 
    oluşturmanın genel biçimi şöyledir:

    (<tür>) { <eleman_listesi>}

    Aslında bu işlem bir tür dönüştürme işlemi gibidir. Ancak bu işlemin daha önce gördüğümüz tür dönüştürme operatöründen farkı 
    yanında küme parantezlerinin olmasıdır. Biz bir dizi türüne ya da yapı türüne tür dönüştürmesi yapamayız. Ancak bileşik sabitlerle bu durum 
    mümkün hale getirilmiştir. Örneğin:

    struct POINT {
        int x;
        int y;
    };

    void draw_line(struct POINT pt1, struct POINT pt2)
    {
        /* ... */
    }
    ...

    draw_line((struct POINT){3, 4}, (struct POINT){13, 21});

    Burada biz adeta sabit formunda yapı nesneleri oluşturmuş olduk. Yani o anda bir yapı nesnesi yaratıp onu fonksiyona yolladık. Örneğin:

    struct POINT pt;
    ...
    pt = (struct POINT) {10, 20};

    Burada da yapı elemanlarına tek satırda atama yapmış olduk. 

    Bileşik sabitler her ne kadar isminde sabit olsa da aslında nesne belirtmektedir. Biz bir bileşik sabitin adresini de alabiliriz. Örneğin:

    struct DATE {
        int day, month, year;
    };

    void disp_date(struct DATE *pdate)
    {
        printf("%02d/%02d/%04d\n", pdate->day, pdate->month, pdate->year);
    }
    ...
    int main(void)
    {
        disp_date(&(struct DATE) { 10, 12, 2020 });

        return 0;
    }

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

struct DATE {
    int day, month, year;
};

void disp_date(struct DATE *pdate)
{
    printf("%02d/%02d/%04d\n", pdate->day, pdate->month, pdate->year);
}

int main(void)
{
    struct DATE d;

    disp_date(&(struct DATE) { 10, 12, 2020 });

    d = (struct DATE){21, 12, 2008};
    disp_date(&d);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir dizi türü için de bileşik sabit oluşturabilmektedir. Bu işlemin genel biçimi de şöyledir:
    
    (tür [uzunluk]) {değer_listesi}

    Eğer dizi uzunluğu belirtiliyorsa sabit ifadesi biçiminde olması gerekir. Tabii küme parantezleri içerisindeki değer listesinde
    sabit ifadesi kullanmak zorunlu değildir. 

    Örneğin:

    disp((int[5]) { 10, 20, 30, 40, 50 }, 5);

    Burada 5 elemanlı int bir dizi bileşik sabit yöntemiyle yaratılmış ve başlangıç adresi disp fonksiyonuna geçilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

void disp(const int *pi, size_t size)
{
    for (size_t i = 0; i < size; ++i)
        printf("%d ", pi[i]);
    printf("\n");
}

int main(void)
{
    disp((int[5]) { 10, 20, 30, 40, 50 }, 5);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bileşik sabit yöntemiyle dizi nesnesi yarattığımızda o bileşik sabit artık aynı zamanda o yaratılan dizinin başlangıç adresi anlamına gelmektedir.
    Yani biz bu biçimde yarattığımız dizileri aynı türden göstericilere atayabiliriz. Örneğin:

    int *pi;

    pi = (int[5]) {10, 20, 30, 40, 50};

    Burada pi yine yaratılmış olan 5 elemanlı int diziyi gösterecektir. Tabii aslında bileşik sabit yolu ile dizi oluştururken dizi uzunluğunun
    belirtilmesine gerek yoktur. Örneğin:

    pi = (int[]) {10, 20, 30, 40, 50};

    Derleyici bu durumda küme parantezleri içerisindeki eleman sayısı uzunluğunda diziyi yaratacaktır. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bileşik sabitler sol taraf değeri oluşturmaktadır. Yani onların değerleri değiştirilebilir. Örneğin:

    int *pi = (int[]){10, 20, 30};

    *pi = 100;       /* geçerli */

    Bileşik sabitler yerel bir blokta oluşturulmuşlarsa tıpkı yerel nesneler gibi, global biçimde oluşturulmuşsa global nesneler 
    gibi ömre sahip olurlar. Yani bileşik sabitler eğer global düzeyde oluşturulmuşsa statik ömürlü olurlar. Program çalışma başladığında
    yaratılırlar program sonlanana kadar yaşamaya devam ederler. Bileşik sabitler eğer bir bloğun içerisinde oluşturulmuşlarsa ömürleri 
    oluşturuldukları yerden blok sonuna kadarki akış içerisinde devam eder. 
    
    Bu durumda bileşik sabitler sabitin kullanıldığı yerde isimsiz bir nesne oluşturulmasına yol açmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int *pi;

    pi = (int[]){10, 20, 30};

    *pi = 100;       /* geçerli */

    for (int i = 0; i < 3; ++i)
        printf("%d\n", pi[i]);
    printf("\n");
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonun yerel düzeyde yaratılmış bir bileşik sabitin adresiyle geri dönmesi tanımsız davrana yol açacaktır. 
    Örneğin:

    int *foo(void)
    {
        return (int[]) { 1, 2, 3, 4, 5 };		/* tanımsız davranış */
    }

    Tabii derleyiciler bileşik sabit gördüklerinde optimizasyon yaparak gereksiz işlemleri elimine edebilmektedir. Örneğin:

    struct POINT pt;
    ...

    pt = (struct POINT) {10, 20};

    Burada aslında aynı türden iki yapı nesnesi birbirine atanıştır. Ancak derleyici bileşik sabit biçiminde oluşturulmuş olan 
    nesneyi hiç yaratmadan 10 ve 20 değerlerini doğrudan pt nesnesinin elemanlarına yerleştirebilir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bugün kullandığımız pek çok işlemcide yerel değişkenler iç blokta tanımlanmış olsa bile sanki fonksiyonun ana bloğunun başında tanımanmış gibi 
    derleyiciler kod üretmektedir. Ancak C standartlarında böyle bir zorunluluk yoktur. Örneğin aşağıdaki kodda aslında döngünün her yinelenmesinde 
    iç bloktaki a nesnesinin yeniden yaratılıp yok edilmesi beklenir. Ancak yukarıda da belirttiğimiz gibi bugünkü mimarilerde genellikle 
    (her zaman değil) derleyiciler aslında bu nesneyi ana blokta yaratıp fonksiyon sonlanana kadar muhafaza ederler. Dolayısıyla aşağıdaki kodda 
    ekrana hep aynı adresin yazıldığına şaşırmayınız:

    for (int i = 0; i < 10; ++i) {
        int a = 10;

        printf("%d, %p\n", %d, &a);
    }

    Her iç blokta nesneyi yeniden yaratıp yok etmenin maliyeti daha yüksektir. Derleyiciler optimizasyon gereği onu ana bloğun başında yaratmayı tercih 
    etmektedir. Tabii ne olursa olsun biz bu örnekteki a değişkenini tanımlandığı bloğun dışnda yine kullanamayız. Ancak yukarıdaki örnekte döngünün
    hger yinelenmesinde a nesnesine atama işlemi yine yapılmaktadır. 

    Bileşik sabitlerde de aynı durum söz konusudur. C'deki resmi anlatıma göre bileşik sabitler programın akışı sabitin yaratıldığı noktaya geldiğinde yaralıp 
    blok sonlanınca yok edilmektedir. Ancak derleyiciler yine eğer gözlemlenebilecek bir değişiklik oluşmnayacaksa bu yaratımı blok başlarında birt kez 
    yapabilmektedir. Örneğin:

    {
        int *pi;

        REPEAT:
            pi = (int[]) {10, 20, 30};
            ...
            goto REPEAT;
    }

    Burada aynı faaliyet alanı içerisinde goto deyimi ile yukarıya atlanmıştır. Ancak derleyiciler optimizasyon uygulayarak yaratımı 
    fonksiyon başlarında yapabilirler. Fakat her yinelemede yine diziye bu elemanlar atanacaktır. Aynı durum döngü içerisinde bileşik sabit 
    oluştururken de karşımıza çıkabilir. Örneğin:

    int *pi;
    ...
    for (int i = 0; i < 10; ++i) {
        pi = (int[]) {10, 20, 30};
        ...
    }

    Tabii burada yine derleyicileerin optimizasyon mekanizması devreye girebilecektir. Eğer bu döngü içerisinde söz konusu dizide hiçbir değişiklik yapılmıyorsa
    derleyici de her defasında bu atamayı yapmayabilir. 

    Tabii bileşik sabitler const biçimde oluşturulursa bu durumda derleyici de optimizasyon yaparak onları ilkdeğerlerini her defasında vermeyebilir. Örneğin:

    const int *pi;
    ...
    for (int i = 0; i < 10; ++i) {
        pi = (const int[]) {10, 20, 30};
        ...
    }

    Burada derleyicinin döngünün her yinelenmesinde dizi elemanlarına bu atamayı yapmayabilir. Nasıl olsa const bir dizinin elemanları değiştirilemez ve
    onların const olmayan göstericiler yoluyla değiştirilmesi de "tanımsız davranışa (undefined behavior)" yol açmaktadır. Dolayısıyla yukarıdaki kodu derleyici
    aşağıdaki eşdeğerlikle derleyebilir:

    const int *pi;
    const int temp[] = {1, 2, 3}
    ...
    for (int i = 0; i < 10; ++i) {
        pi = temp;
        ...
    }

    Örneğin benzer biçimde:

    for (int i = 0; i < 10; ++i) {
        foo((const int[]) {1, 2, 3});
        ...
    }

    Burada derleyici muhtemelen döngünün her yinelenmesinde yeniden diziyi yaratıp dizi elemanlarına atama yapmayacaktır. 

    Aşağıdaki kodu çalıtırıp test ediniz:

    #include <stdio.h>

    int main(void)
    {
        for (int i = 0; i < 3; ++i) {
            int *pi = (int[]){1, 2, 3};

            for (int k = 0; k < 3; ++k)
                printf("%d ", pi[k]);
            printf("\n");

            pi[0] = 10;
            pi[1] = 20;
            pi[2] = 30;

            for (int k = 0; k < 3; ++k)
                printf("%d ", pi[k]);
            printf("\n");

        }
    
        return 0;
    }

    Kodu çalıştırdığımızda aşağıdaki gibi bir çıktı elde edilmiştir:

    1 2 3
    10 20 30
    1 2 3
    10 20 30
    1 2 3
    10 20 30

    Özetle aşağıdaki gibi bir kod söz konusu olsun:

    pi = (int[]){1, 2, 3};

    Aslında bu işlemin eşdeğeri şyledir:

    int temp[] = {1, 2, 3};
    pi = temp;
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aslında bileşik sabitler dizi ve yapıların dışında normal türler için de oluşturulabilmektedir. Örneğin:

    int *pi;
    ...
    pi = &(int){10};		/* geçerli */

    Biz burada int türden bir nesne yaratıp onun adresini pi'ye atamış olduk. Bileşik sabitlerin aslında sabit oluşturmadığına bir nesne oluşturduna
    dikkat ediniz. Yukarıdaki işlemin eşdeğeri aşağıdaki gibi düşünülebilir:

    int *pi;
    ...
    int temp = 10;
    pi = &temp;

    C'de normal türlere de küme parantezleri ile ilkdeğer verilebildiğini anımsayınız:

    int a = {10};		/* geçerli */
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bileşik sabitlerde yine C99 ile birlikte gelen "designated initializer" sentaksı kullanılabilmektedir. Örneğin:

    pi = (int[10]){[5] = 100, [7] = 200, 300};
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int *pi;
    
    pi = (int[10]){[5] = 100,[7] = 200, 300};

    for (int i = 0; i < 10; ++i)
        printf("%d \n", pi[i]);
    printf("\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'de ismine "bit operatörleri (bitwise operators)" denilen bir grup operatör vardır. Bu operatörler sayıları bütünsel olarak değil bitsel olarak 
    işleme sokmaktadır. Başka bir deyişle bit operatörleri sayıların karşılıklı bitlerini işleme sokarlar. Bu tür bitsel işlemler sistem programlamada
    yoğun olarak kullanılmaktadır. C'nin bit operatörleri şunlardır:

    ~	(Bit NOT)
    &	(bit AND) 
    ^	(Bit EXOR)
    |	(bit OR)
    <<	(Sola öteleme)
    >>	(Sağa öteleme)
    
    Bit operatörlerinin öncelik tablosundaki konumları şöyledir:

    () [] .	->						Soldan-Sağa
    + - ++ -- !	& *	(tür) sizeof ~	Sağdan-Sola
    * / %							Soldan-Sağa
    + -								Soldan-Sağa
    << >>							Soldan-Sağa
    <  > <= >=						Soldan-Sağa
    != ==							Soldan-Sağa
    &								Soldan-Sağa
    ^								Soldan-Sağa
    |								Soldan-Sağa
    &&								Soldan-Sağa
    ||								Soldan-Sağa
    ?:								Sağdan-Sola
    =, +=, /=, *=,...				Sağdan-Sola
    ,								Soldan-Sağa	 

    Burada sola ve sağa öteleme operatörlerinin aritmetik operatörlerle karşılaştırma operatörlerinin arasında bulunduğuna &, ^ ve | operatörlerinin 
    ise karşılaştırma operatörlerinden daha düşük öncelikte bulunduğuna dikkat ediniz. Bu durum C'de eleştirilmektedir. Örneğin Python'da &, | ^ operatörleri 
    karşılaştırma operatörlerinden daha önceliklidir. ~ operatörü tek operandlı bir operatör olduğu için öncelik tablosunun ikinci düzeyinde bulunmaktadır. 
    
    Bit operatörlerinin hepsini operand'ları tamsayı türlerine ilişkin olmak zorundadır. Yani bu operatörlerin operandları float ve double türden, bir adres
    türünden olamaz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir sayının en düşük anlamlı biti 0 olmak üzere her bitinin bir pozisyon numarası vardır. Örneğin:

    0101 1000

    Bu ikilik sistemdeki sayının bit pozisyon numaralerı şöyledir:
    
    7654 3210	
    0101 1000

    Biz aşağıdaki örnekllerde "sayının n numaralı biti" dediğimizde numaralandırmanın böyle yapılmış olduğunuı varsayınız.
 ----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    & operatörü iki operand'lı araek bit operatörüdür. Sayıların karşılıklı bitlerini AND işlmine sokar. Yine işlem öncesi otomatik tür dönüştürmesi
    (int türüne yükseltme kuralı da dahil olmak üzere) uygulanmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0x3F;     /* 0011 1111 */
    unsigned char b = 0x77;     /* 0111 0111 */
    unsigned char c;

    c = a & b;                  /* 0011 0111  = 0x37 */
    printf("%02X\n", c);        /* 37 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bit düzeyindeki AND işleminde 1'in etkisiz eleman olduğuna dikkat ediniz. 1 ile 0 AND yapılırsa 0, 1 ile 1 AND yapılırsa 
    1 elde edilmektedir. Benzer biçimde bit AND işleminde 0 yutn elemandır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Elimizde bir sayı olsun ve biz bu sayının n'inci bitinin durumunu belirlemeye çalışalım. Klasik yöntem şudur: Sayı tüm bitleri 0 olan n'inci biti 
    1 olan bir sayıyla bit AND işlemine sokulur. Bunun sonucunda 0 değeri elde edilirse n'inci bit 0, sıfır dışı bi değer elde edilirse n'inci bit 1'dir. 
     Tüm bitleri 0 olan yalnızca tek biti 1 olan bu tür değerlere halk arasında "bit mask değerler" ya da "one hot" değerler denilmektedir. 

    Aşağıdaki örnekte sayının 5'inci bitinin durumu elde edilmektedir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0x3F;     /* 0011 1111 */

    if (a & 0x20)
        printf("5. Bit 1\n");
    else
        printf("5. Bit 0\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir sayının diğer bitlerine dokunmadan n'inci bitini nasıl 0 yapabiliriz? Bunun klasik yolu şöyledir: Sayı tüm bitleri 1 olan n'inci biti 0 olan 
    bir sayıyla bit AND işlemine sokulur. 0 değeri AND işleminde yutan elemandır, 1 ise etkisiz elemandır. 

    Aşağıdaki örnekte a içerisindeki sayının diğer bitlerine dokunmadan 4'üncü biti 0 yapılmak istenmiştir. Burada a değeri 0xEF ile bit AND işlemine 
    sokulmuştur. 0xEF değeri 1110 1111 biçimindedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0x3F;     /* 0011 1111 */

    a = a & 0xEF;				/* 0xEF => 1110 1111 */
    printf("%02X\n", a);        /* 0010 1111 = 0x2F */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                    70. Ders - 28/02/2023 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    & ve | operatörlerinin "kısa devre (short circuit)" özelliği yoktur. Örneğin:

    a = foo() | bar() & tar();

    Burada her zaman bu üç fonksiyon da çağrılacaktır. (Halbuki mantıksal AND ve OR operatörlerinde bunun bir garantisi yoktur.)
    Burada önce her zaman & operatörü yapılır. Sonra buradan elde edilen değer | operatörğne sokulur. Yani işlemler şu sırada 
    yapılacaktır:

    İ1: foo() 
    İ2: bar()
    İ3: tar()
    İ4: İ2 & İ3
    İ5: İ1 | İ4
    İ6: İ5

    Aşağıda bit AND operatörü ile mantıksal AND operatörünün kısa devre davranışına ilişkin bir örnek verilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int foo(void)
{
    printf("foo\n");

    return 0;
}

int bar(void)
{
    printf("bar\n");

    return 0x3F;
}

int tar(void)
{
    printf("tar\n");

    return 0x7A;
}

int main(void)
{
    int result;

    result = foo() & bar() & tar();

    printf("%d\n", result);

    result = foo() && bar() && tar();

    printf("%d\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir tamsayının tek mi çift mi olduğunu anlamak için genellikle o sayının 2'ye bölümünden elde edilen kalana bakılmaktadır. Aslında işlemcilerin çoğunda
    çarpma, bölme ve bölümden elde edilen kalan işlemleri nano düzeyde bit işlemlerine göre daha fazla zaman almaktadır. Bir sayının tek ya da 
    çift olduğunu anlamanın diğer bir yolu da sayıyı 1 ile (1 sayısının tüm bitleri 0, en düşük anlamlı biti 1'dir) bit AND işlemine sokmaktır. Bunun sonucu
    ya 0 ya da 1 olarak elde edilir. Tabii bit operatörlerinin yalnızca tamsayı türleriyle kullanılabildiğine dikkat ediniz. Eğer örneğin biz bir double sayı
    için kontrol yapmak isteseydik ikiye bölümünden elde edilen kalan yöntemine başvururduk.

    Aşağıda bu yöntemin kullanımına bir örnek verilmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    printf("Bir sayi giriniz:");
    scanf("%d", &a);

    printf(a & 1 ? "tek\n" : "cift\n");

    return 0;
}


/*----------------------------------------------------------------------------------------------------------------------
    Bit OR işleminde 0 etkisiz elemandır. 1 ise yutan elemandır. Yani örneğin 0 biti ile 0 biti bir OR işlemine sokulursa 0 biti, 
    0 bti ile 1 biti bit OR işlemine sokulursa 1 biti elde edilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bit OR işlemi için | operatörü kullanılmaktadır. Bu operatör sayının karşılıklı bitlerini OR işlemine sokmaktadır. Örneğin:

    unsigned char a = 0xA2;         /* 1010 0010 */
    unsigned char b = 0x34;         /* 0011 0100 */
    unsigned char c;

    c = a | b;

    Bit OR işleminin de kısa devre özelliği yoktur.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0xA2;         /* 1010 0010 */
    unsigned char b = 0x34;         /* 0011 0100 */
    unsigned char c;

    c = a | b;

    printf("%02X\n", c);            /* 1011 0110 = 0xB6 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir sayının diğer bitlerine dokunmadan belli bir pozisyondaki bitini 1 nasıl yapabiliriz? Sayı bütün bitleri 0, ilgili biti 1 olan bir sayı ile
    bit düzeyinde OR işlemine sokulur. 

    Aşağıdaki örnekte a içerisindeki sayının diğer bitlerine dokunmadan 4'üncü biti 1 yapılmak istenmiştir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0xA7;     /* 1010 0111 */

    a = a | 0x10;				/* 0001 0000 */
    printf("%02X\n", a);        /* 1011 0111 = 0xB7 */

    return 0;
}

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıda da belirttiğimiz gibi bit AND operatörü bit OR peratöründen dagh yüksek önceliklidir. Örneğin:

    a = foo() | bar() & tar();

    Burada önce bit AND operatörü yapılır daha sonra bit OR operatörü yapılır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de karşılaştırma operatörlerinin bit AND, bit OR ve bit EXOR operatörlerinden daha öncelikli olduğuna dikkat ediniz. Bu durum maalesef programcılar tarafından 
    yanlış yorumlanıp hatalı kod oluşumuna yol açabilmektedir. Örneğin:

    if (a & 1 == 0)		/* dikkat! */
        printf("cift\n");
    else
        printf("tek\n");

    Burada a & 1 işleminin sonucu 0 iler karşılaştırılmaz.  Önce 1 ile 0 karşılaştırılıp bunun sonucu a ile AND işlemine sokulmaktadır. Halbuki programcı büyük olasılıkla
    a ile 1 değerini bi AND işlemine sokup sonucun 0 olup olmadığını kontrol etmek istemiştir. Burada paraztez kullanmak gerekir:

    if ((a & 1) == 0)
        printf("cift\n");
    else
        printf("tek\n");

    Pek çok C derleyicisi bu tür durumlarda bir mesajla programcıyı uyarmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a;

    printf("Bir sayi giriniz:");
    scanf("%d", &a);

    if ((a & 1) == 0)
        printf("cift\n");
    else
        printf("tek\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    EXOR işlemi operand'lar aynı ise 0 değerini, farklı ise 1 değerini veren işlemdir. (Exor sözcüğü "exclusive or" sözcüklerinden kısaltılarak uydurulmuşur. 
    "exclusive" dışlayan anlamına gelmektedir.) C'de EXOR işlemi ^ operatöryle temsil edilmektedir. Öneğin:

    0101 1011 
    0111 1101

    Bu iki sayıyı EXOR işlemine sokarsak şu binary dizilimi elde ederiz:

    0010 0110
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0x5B;     /* 0101 1011 */
    unsigned char b = 0x7D;     /* 0111 1101 */
    unsigned char c;

    c = a ^ b;                  /* 0010 0110 */

    printf("%02X\n", c);        /* 26 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    0 EXOR işleminde etkisiz elemandır. Ancak 1 karşı tarafın tersini verir. Örneğin 0 biti ile 0 biti EXOR işlemine sokulursa
    0 biti, 0 biti ile 1 biti EXOR işlemine sokulursa 1 biti elde edilir. 1 biti ile 0 biti EXOR işlemine sokulursa 1 biti
    1 biti ile 1 biti EXOR işlemine sokulursa 0 biti elde edilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi bir sayının diğer bitlerine dokunmadan onun belli bir bitini tersi ile nasıl yer değiştirirsiniz? İşte bu sayı 
    tüm bitleri 0 olan ilgili biti 1 olan bir sayı ile EXOR işlemine sokulur. Örneğin biz 0110 1101 sayısını diğer bitlerine 
    dokunmadan 4'üncü bitini tersi ile yer değiştirmek isteyelim. Bu sayıyı 0001 0000 sayısı ile EXOR işlemine sokarız. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0x6B;     /* 0110 1101 */
 
    a = a ^ 0x10;

    printf("%02X\n", a);        /* 0111 1011 = 0x7B */
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    EXOR işlemi geri dönüşümlü bir işlemdir. Yani a ^ b => c ise, c ^ a => b ve c ^ b => a olmaktadır. Örneğin (sembolik olarak):
    
    a = 0110 1101
    b = 1011 0010

    a ^ b = 1101 1111 => c
    c ^ a = 1011 0010 => b
    c ^ b = 0110 1101 => a
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0x6D;     /* 0110 1101 */
    unsigned char b = 0xB2;     /* 0110 1111 */
    unsigned char c, d, e;

    c = a ^ b;
    printf("%02X\n", c);        /* 1011 1111 = 0xDF */

    d = c ^ a;
    printf("%02X\n", d);        /* 0110 1111 = 0xB2 */

    e = c ^ b;
    printf("%02X\n", e);        /* 0110 1101 = 0x6D */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    EXOR işleminin geri dönüşümlü olması şifreleme gibi bazı alanlarda yaygın kullanılmasına yol açmıştır. Örneğin biz bir dosya içerisindeki byte'ları 
    bir anahtarla EXOR işlemine sokup bozalım. Sonra aynı anahtarla yeniden bozulmuş veriyi EXOR işlemine sokalım. Bu işlemden sonra orijinal içeriği elde ederiz. 
    Yani bir şeyi bozup düzeltmek için EXOR çok güzel bir işlemdir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de iki öteleme (shift) operatörü vardır: Sola öteleme (<< operatörü) ve sağa öteleme (>> operatörü). Her iki operatör de iki operand'lı arek operatörlerdir. 
    Öteleme operatörleri öncelik tablosunda aritmetik operatörler ile karşılaştırma operatörlerinin arasında bulunmaktadır. Öteleme operatörlerinde 
    soldaki operand ötelenecek değeri, sağdaki operand ötelenecek miktarı belirtmektedir. 

    Sola bir kez öteleme sırasında her bit bir sola kaydırılır, sayı en sağdan 0 ile beslenir. En soldaki bit yok olur. Örneğin:

    0101 0111

    Bu sayıyı bir kez sola öteleyelim:

    1010 1110

    Tabii biz ilk sayıyı iki kez de sola öteleyebiliriz. Bu durumda aynı işlem bir kez daha yapılacaktır:

    0101 1100

    Sağa öteleme bunun tersi bir işlemdir. Yani tüm bitler bir sağa kaydırılır, sayı en soldan 0 ile (bu konuda bazı ayrıntılardan bahsedeceğiz) beslenir,
    en sağdaki bit kaybolur. Örneğin:

    0101 0111

    Bu sayıyı 1 kez sağa öteleyelim:

    0010 1011

    elde edilir. 

    Öteleme operatörlerinde önce her iki operand üzerinde de "int türüne yükseltme kuralı" uygulanır. İşlemin sonucu,  sol taraftaki operand'ın int türüne yükseltme
    kuralı sonucunda elde edilmiş olan türdendir. Yani örneğin biz short bir değeri sola ya da sağa ötelersek sonuç short türden çıkmaz. int türden çıkar. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    unsigned char a = 0x6D;     /* 0110 1101 = 0x6D */
    unsigned char b;

    b = a << 1;                 /* 1101 1010 = 0xDA */
    printf("%02X\n", b);        /* 6A */

    b = a >> 1;                 /* 0011 0110 = 0x36 */
    printf("%02X\n", b);        /* 39 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Sayıyı bir kez sola ötelemek onu 2 ile çarpmak anlamına gelir. Örneğin:

    0000 0011

    Bu sayı 3'tür. Şimdi onu 1 kez sola öteleyelim:

    0000 0110

    Bu sayı 6'dır. Şimdi 3 sayısını 2 kez sola öteleyelim:

    0000 1100 

    Bu sayı 12'dir. O halde genelliştirirsek bir sayıyı n kez sola ötelediğimizde o sayıyı 2 üzeri n ile çarpmış oluruz. 

    Pekiyi sola öteleme sırasında bilgi kaybı söz konusu olabilir mi? Örneğin:

    int a = 0x7FFFFFFF;

    Burada a'nın içerisine en büyük pozitif sayı yerleştirilmiştir. Bu sayıyı a << 1 işlemi ile sola bir kez öteleyelim. Sola öteleme ikili çarpma anlamına geldiğine 
    göre sayıda bilgi kaybı oluşur. Burada bir taşma (overflow) söz konusu olduğu için işlem zaten tanımsız davranışa yol açacaktır.

    Sayıyı bir kez sağa ötelemek onu 2'ye tam bölmek anlamına gelir. Örneğin:

    0000 0110

    Bu sayı 6'dır. Şimdi bu sayıyı bir kez sağa öteleyelim:

    0000 0011

    Bu sayı 3'tür. Görüldüğü gibi sayı 2'ye bölünmüştür. Örneğin:

    0000 0101 

    Bu sayı 5'tir. Sayıyı bir kez sağa öteleyelim:

    0000 0010

    Bu sayı 2'dir. Bu biçimde 2'ye bölmede noktadan sonraki kısmın atıldığına dikkat ediniz. Sayıyı 2 kere sağa öteledeiğimizde sayıyı 2 ile değil 4 ile bölmüş 
    oluruz. O halde genel olarak sayıyı n defa sağa öteldiğimizde sayıyı 2 üzeri n'e bölmüş oluruz. Pekiyi sayıyı sağa ötelediğimizde işaret biti değişirse ne olur?
    Örneğin:

    int a = 0xFFFFFFFF;		

    Burada a'da -1 vardır. Biz şimdi a'yı sağa a >> 1 biçiminde bir kez ötelersek 0x7FFFFFFF değerini elde ederiz. Bu da en büyük pozitif değerdir. 
    İşte C standartlarında buradaki durumlar için şu kurallar oluşturulmuştur:

    1) Eğer sol taraftaki operand işaretsiz bir tamsayı türündense sola öteleme sırasında işlemler yukarıda belirtildiği gibi yapılır. Yani tüm bitler bir sola kaydırılır, 
    en soldaki bit yok edilir. Sayıya en sağdan 0 ile besleme yapılır. Sağa öteleme sırasında da tüm bitler bir sağa kaydırılır, en sağdaki bit yok edilir. 
    Sayı en soldan 0 ile beslenir. 

    2) Sola öteleme işleminde sol taraftaki operand işaretli bir tamsayı türündense ve pozitifse, sayı 2 ile çarpıldığında çarpımın sonucu hala bu işaretli tamsayı 
    türünün sınırları içerisinde kalıyorsa işlem sonucunda bu iki ile çarpım değeri elde edilir. Eğer işaretli tamsayı negatifse ya da pozitif olduğu halde 
    sayının iki ile çarpılması sonucunda elde edilen değer o işaretli tamsayı türünün sınırları içerisine girmiyorsa "tanımsız davranış (undefined behavior)" söz
    konusudur. Buradan çıkan sonuçlar şunlardır:

    a) Negatif bir tamsayıyı sola ötelemek tanımsız davranışa yol açar. 
    b) Sayı pozitifse ancak sola öteleme sonucunda sayı soldaki operand'ın yükseltilmiş türünün sınırları içerisinde kalmıyorsa (yani overflow oluşuyorsa)
    yine tanımsız davranış söz konusudur. 
    c) İşaretsiz tamsayı türlerininin sola ötelenmesinde hiçbir sakınca yoktur. 
    
    3) Sağa öteleme işleminde sol taraftaki operand işaertli bir tamsayı türündense ve pozitif ise işlem normal olarak her bitin sağa kaydırılması biçiminde
    yani 2'ye bölme biçiminde yapılır. Ancak sol taraftaki operand işaretli tamsayı türünden ve negatif ise bu durumda işaret bitinin korunup korunmayacağı 
    (yani en soldan 0'la mı 1'le mi besleneceği) derleyicileri yazanların isteğine bırakılmıştır. İşaretsiz bir sayının sağa ötelenmesi tamamen yukarıda anlattığımız 
    bir kaydırma ile yapılmaktadır. Buradan çıkan sonuçlar şunlardır:

    a) İşaretli pozitif bir sayının sağa ötelenmesinde hiçbir sakınca yoktur.
    b) İşaretli begatif bir sayının sağa ötelenmesinde en soldan besleme 1 ya da 0 ile yapılabilir. Beslemenin 1 ile yapılması aslında negatif bir sayının işareti korunarak
    2'ye bölündüğü anlamına gelir. Beslemenin 0 ile yapılması sayıyı büyük bir pozitif sayı haline getirir. Bu durun derleyicileri yazanların isteğine bırakılmıştır. 
    c) İşaretsiz tamsayı türlerinin sağa ötelenmesinde hiçbir sakınca yoktur.

    Sonuç olarak işaretsiz tamsayılar üzerinde öteleme işlemleri sorunsuz olarak yukarıda anlatıldığı gibi yapılmaktadır. Ancak işaretli sayılar üzerinde öteleme 
    işlemleri yapılırken dikkat edilmelidir. Programcılar genellikle öteleme işlemlerini şşaretsiz tamsayılar üzerinde yaparlar. 

    Yine standartlara göre sola öteleme ya da sağa öteleme işleminde sağ taraftaki operand negatif ise ya da soldaki operand'ın yükseltilmiş türünün 
    bit uzunluğunu aşıyorsa tanımsız davranış söz konusudur. Yani bir sayıyı -1 defa ötelemek istersek derleme aşamsından geçilir. Ancak programın çalışması 
    sırasında ne olacağı belli değildir. Benzer biçimde biz int türünün 32 bit olduğu bir sistemde unsigned int bir değeri örneğin 40 kere sola ya da sağa ötelersek
    bu durum da tanımsız davranış oluşturur. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    71. Ders - 02/03/2023 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Daha önce biz bir sayının n'inci bitinin 1 mi 0 mı olduğunu anlamak için bütün bitleri 0 olan ancak n'inci 1 olan bir sayı ile bit AND işlemi 
    uygulamıştık. Buna alternatif olarak aynı işlemi şöyle de yapabiliriz: Sayıyı n defa sağa öteleyip 1 ile bit AND işlemine sokarız. Sonuç ya 0 çıkar ya 
    da 1 çıkar. Örneğin:

    if (val >> n & 1) {
        /* n'inci bit 1 */
    }
    else {
        /* n'inci bit 0 */
    }

    Aşağıdaki örnekte bir sayını n'inci bitinin durumu bu yöntemler elde edilmiştir.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    uint32_t a = 0x12345678;
    int n, result;

    printf("Bit no:");
    scanf("%d", &n);

    result = a >> n & 1;
    printf("%d\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bazen sayının yan yana k bitinin durumu elde edilmek istenebilir. Bunun için o k bitin düşük anlamlı biti n defa sağa ötelenerek en sağ tarafa getirilir. 
    Sonra da k tane biti 1 olan bir sayıyla bit AND işlemi uygulanır. Örneğin a sayısının 7 ve 8 numaralı bitlerinin durumunu elde etmek isteyelim:

    result = a >> 7 & 3;

    Burada 3 değerinin bit olarak 000...011 biçiminde olduğuna dikkat ediniz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir sayının n'inci bitini sayının diğer bitlerine dokunmadan 1 yapmak isteyelim. Bu işlem de alternatif olarak şöyle gerçekleştirilebilir:

    a = a | 1u << n;

    Burada 1 sabitini 1u biçiminde ifade ettik. Böylece sola ötelemede n sayısı yüksek olduğunda "tanımsız davranıştan" sakınmak istedik. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Elimizde bir byte'lık işaretsiz bir tamsayı değer olsun. Bu değeri a ile temsil edelim. Biz bunun düşük anlamlı 4 bitini a & 0xF gibi bir işlemle
    elde edebiliriz. Yüksek anlamlı 4 bitini ise a >> 4 işlemiyle elde ederiz. Bu iki 4 biti (nibble) yer değiştirmek için ise şu işlemi yaparız:

    result = a << 4 | a >> 4;
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>

int main(void)
{
    uint8_t a = 0xAF;
    uint8_t result;

    result = a >> 4 | a << 4;
    printf("%02X\n", result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Elimizde iki byte'lık (word) işaretsiz bir tamsayı olsun Bunun düşük anlamlı ve yüksek byte'larını şöyle elde edebiliriz:

    #define LOBYTE(w)   ((w) & 0xFF)
    #define HIBYTE(w)   ((w) >> 8 & 0xFF)

    HIBYTE makrosunda 0xFF ile bit AND işlemi gereksiz gibidir. Ancak ne olursa olsun kişi yanlışlıkla 2 byte'tan daha büyük bir bilgiyi bu makroya verirse
    yüksek byte'ların maskelenmesi daha güvenli bir durum oluşturur. 

    Benzer biçimde 32 bitlik işaretsiz bir tamsayı için de onları word word ayrıştıran makroları şöyle yazabiliriz:

    #define LOWORD(dw)   ((dw) & 0xFFFF)
    #define HIWORD(dw)   ((dw) >> 16 & 0xFFFF)
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>

#define LOBYTE(w)   ((w) & 0xFF)
#define HIBYTE(w)   ((w) >> 8 & 0xFF)

int main(void)
{
    uint16_t w = 0x1234;
    uint8_t high, low;

    high = HIBYTE(w);
    low = LOBYTE(w);

    printf("%02X\n", high);
    printf("%02X\n", low);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bit NOT operatörü ~ ile temsil edilmektedir ve tek operand'lı önek bir operatördür. Bu operatör sayı içerisindeki 1'leri 0, 0'ları 1 yapar. 
    Bit NOT operatöründe operand üzerinde yine int türüne yükseltme kuralı uygulanır. Operatörün ürettiği değer bu yükseltilmiş türdendir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    int a = 0;

    a = ~a;

    printf("%d\n", a);      /* -1 */

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Klavyeden (stdin dosyasından) bir n değerinin okunduğunu kabul edelim. Düşük anlamlı n tane biti 1 olan geri kalan tüm bitleri 0 olan 32 bitlik 
    işaretsiz bir tamsayı değerini nasıl elde edebiliriz? Örneğin n değer 5 olsun. Elde edilecek sayı şyle olmalıdır:

    0000 0000 0000 0000 0000 0000 0001 1111 

    Aşağıdaki gibi bir işlemle bu yapılabilir:

    result = ~(~0u << n)

    Burada 0 sabitinin 0u biçiminde yazıldığına dikkat ediniz. Eğer biz bu sabiti ~0 biçiminde yazsaydık bu durumda ~0 ifadesinden elde edilecek değer int türden olurdu.
    Onun da sola ötelenmesi tanımsız davranış oluştururdu. Ancak ~0u ifadesinde 0u unsigned int türünden olduğu için artık ~ operatöründen elde edilen değer 
    unsigned int türünden olacaktır. Tabii aynı işlem şöyle yapılabilirdi:
    
    result = ~0u >> (32 - n);

    Burada da eğer ~0u yerine ~0 yazılırsa tüm bitleri 1 olan signed int bir değer elde edilir. İşaretli tamsayıların sağa ötelenmesinde
    işaret bitinin korunup korunmayacağının derleyicilere bağlı bir davrış olduğunu anımsayınız.

    2 üzeri n değerinden 1 çıkartmak da bir alternatif olabilir. Ancak kuvvet almak bit işlemlerine göre daha yavaş olacaktır:

    result = (uint32_t)pow(2, n) - 1;

    Tabii 2'nin n'inci kuvveti aslında 1U << n olduğu için aynı işlem şöyle yapılabilirdi:

    result = (1U << n) - 1;
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>

void disp_bits32(uint32_t val)
{
    for (int i = 31; i >= 0; --i)
        putchar((val >> i & 1) + '0');
    putchar('\n');
}

int main(void)
{
    uint32_t result;
    int n;

    printf("n: ");
    scanf("%d", &n);

    result = ~(~0u << n);
    disp_bits32(result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıdaki sorununun benzeri şöyle olabilir: Yüksek anlamlı n biti 1 olan geri kalan bitleri 0 olan bir sayı nasıl elde edilir? Bu işlem de benzer 
    biçimde şöyle yapıabilir:

    result = ~(~0u >> n);
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdint.h>

void disp_bits32(uint32_t val)
{
    for (int i = 31; i >= 0; --i)
        putchar((val >> i & 1) + '0');
    putchar('\n');
}

int main(void)
{
    uint32_t result;
    int n;

    printf("n: ");
    scanf("%d", &n);

    result = ~(~0u >> n);
    disp_bits32(result);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir sayının diğer bitlerine dokunmadan k'ıncı bitinden k + m'inci bitine kadar m tane bitini nasıl 0'layabiliriz? Örneğin daha somut olarak
    32 bitlik bir sayıda 17, 18 ve 19'uncu bitleri diğer bitlere dokunmadan nasıl sıfırlayabiliriz? Buradaki m değerinin ve k değerinin klavyeden girildiğini 
    varsayalım ve sayının a olduğunu kabul edelim:

    result = a & (~((~0u << m) << k))
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Anımsanacağı gibi C'de atama operatörüyle sağdan sola eşit öncelikli bir grup işlemli atama operatörü vardı. Örneğin a += b işlemi tamamen a = a + b
    ile aynı anlama geliyordu. İşte bit operatörlerinin işlemli atama verisyonları da vardır. Örneğin biz a = a & b yerine a &= b ifadesini kullanabiliriz. 
    İşlemli bit atama operatörlerinin listesi şöyledir:

    &=
    |=
    ^=
    <<=
    >>=

    Burada bir noktaya dikkatinizi çekmek istiyoruz: C'de mantıksal && ve || operatörlerinin işlemli atama karşılığı yoktur. Genel olarak tek operand'lı operatörerin de 
    işlemli karşılıkları yoktur. C'nin bütün işlemli atama operatörleri şunlardır:

    = *= /= %= += -= <<= >>= &= ^= |=

    O halde öncelik tablomuzun nihai durumu da aşağıdaki gibidir:

    
    () [] .	->								Soldan-Sağa
    + - ++ -- !	& *	(tür) sizeof ~			Sağdan-Sola
    * / %									Soldan-Sağa
    + -										Soldan-Sağa
    << >>									Soldan-Sağa
    <  > <= >=								Soldan-Sağa
    != ==									Soldan-Sağa
    &										Soldan-Sağa
    ^										Soldan-Sağa
    |										Soldan-Sağa
    &&										Soldan-Sağa
    ||										Soldan-Sağa
    ?:										Sağdan-Sola
    = *= /= %= += -= <<= >>= &= ^= |=		Sağdan-Sola
    ,	

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Son olarak yine bit operatörlerinin operand'larının tamsayı türlerine ilişkin olmak zorunda olduğunu anımsatmak istiyoruz. Örneğin elimizde bir adres
    bilgisi olsun biz de bu adres bilgisinin düşük anlamlı 4 bitini 0 yapmak isteyelim. Adres bilgilerini doğrudan bit operatörlerine sokamayız. O zaman
    adres bilgilerini önce tamsayı türlerine dönüştürüp sonra bit işlemini yapıp yeniden adres türüne dönüştürmek uygun olur. Örneğin a nesnesinin adresini alıp 
    onu sola 4 kez öteleyip bir göstericiye atamak isteyelim:

    int a;
    int *pi;

    pi = (int *)((uintptr_t)&a << 4);

    Burada biz önce &a ifadesini tamsayı türüne dönüştürdük sonra bu tamsayı değeri 4 kere sola öteledik. uintptr_t türünün <stdint.h> içerisinde o sistemdeki 
    adresi alacak genişlikte typedef edildiğini anımsayınız. Benzer biçimde biz ilgili sistemdeki adresin düşük anlamlı 4 bitini sıfırlamak isteyelim. 
    Bunu da taşınabilir bir biçimde şöyle yapabiliriz:

    pi = (int *)((uintptr_t)&a & (~(uintptr_t)0 << 4))

    Pekiyi bir double sayı üzerinde bit işlemi yapmanın bir anlamı olabilir mi? Aslında adresler üzerinde bit işlemleri bazı özel durumlarda gerekebilmektedir. 
    Ancak gerçek sayılar üzerinde genellikle böyle bir gereksinim ortaya çıkmaz. Gerçek sayılar ğzerinde bir işlemi yapmak için onu tamsayı türüe dönüştüremeyiz. 
    Çünkü o zaman saynın formatı değişir. O zaman mecburen gösterici kullanırız. Örneğin:

    double d = 12.3;

    result = *(uint64_t *)&d << 4;

    Burada biz double sayıyı formatını bozmadan aynı bit dizilimiyle sanki bir tamsayı gibi ifade ettik. Sonra onun üzerinde bit işlemi yaptık. 
    Tabii bu işlem sonucunda bir tamsayı elde etmiş oluruz. Bu tamsayı da aynı yöntemle yeniden double sayıya dönüştürülebilir. Ancak yukarıda da belirttiğimiz
    gibi genellikle böylesi işlemlere gereksinim duyulmamaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bit işelmleri işlemcilerde genel olarak diğer işlemlere göre daha hızlı olma eğilimindedir. Özellikle çarpma ve bölme işlemleri pek çok işlemcide 
    yavaş işlemler grubundadır. Bu nendenle sistem programcıları 2'nin kuvvetleriyle çarpma ya da bölöe yapmak yerine doğrudan öteleme işlemlerini 
    tercih edebilirler. Gerçi günümüzde derleyicilerin kod optimizasyonları bu tür optimizasyonları da zaten yapabilmektedir. Yani biz pek çok derleyici 2'nin 
    kuvvetleri söz konusu olduğnda çarpma ve bölme işlemi yerine öteleme işlemlerini kullanabilmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de bir yapının elemanı n bitlik nesnelerden oluşturulabilmektedir. Yapıların bu biçimdeki elemanlarına "bit alanı (bit field)" elemanları denilmektedir. 
    Yapının bit alanı elemanları dekleratörde ':' atomu ve bit uzunluğunu belirtien bir sabit ifadesi ile oluşturulmaktadır. Örneğin:

    struct SAMPLE {
        double a;
        int b;
        int c: 3;		/* bit alanı elemanı */
        int d: 5;		/* bit alanı elemanı */
    };

    Burada yapının a ve b elemanları sırasıyla double ve int türdendir. Ancak c elemanı 3 bitlik bir int nesneyi, d elemanı ise 5 bitlik bir int nesneyi belirtmektedir. 
    n bitlik nesneler ancak yapı elemanları olarak oluşturulabilmektedir. Yapının dışında böyle bir bildirim yapılamaz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        72. Ders - 07/03/2023 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Standartalara göre yapıların bit alanı elemanları int, unsigned int ya da _Bool türden olabilir. Ancak diğer türlerin bit alanı elemanı olarak 
    kullanılıp kullanılmayacağı derleyicileri yazanların isteğine bırakılmıştır. Bit alanı elemanı int (signed int) türdense yine ikiye tümleyen aritmetiği
    kullanılır. Örneğin şöyle bir bit alanı elemanı olsun:

    int a: 3;

    Burada a elemanı işaret biti dahil olmak üzere 3 bittir. 3 bit ile yazılabilecek işaretli tamsayı sınırı şöyledir:

    100		-4
    011		+3

    Bunun genel formününün bit sayısı n olmak üzere şöyle olduğunu biliyorsunuz:

    [-2^(n - 1), + 2^(n - 1) - 1]

    Bit alanı elemanı unsigned int türünden olsaydı bu durumda işaret biti söz konusu olmayacaktı. Örneğin:

    unisgned a: 3;

    Burada a bit alanı elemanının sayı sınırı şöyle olacaktır:

    000		0
    111		+7
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İşlemcilerde adresleme byte düzeyinde yapılmaktadır. Dolayısıyla bit alanı işlemleri işlemciler tarafından bitlere ilişkin makine komutlarıyla 
    gerçekleştirilmektedir. Şüphesiz aslında programcı da bit alanı işlemlerini kendisi bit operatörleriyle gerçekleştirebilir. Ancak bit alanları bunu
    biraz daha yüksek seviyeli bir biçimde sunmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Mademki en az byte katlarındaki nesnelerin adresleri alınabilmektedir o halde bit alanı elemanlarının adreslerinin alınması anlamsızdır. Dolayısıyla 
    C standartlarına göre bir alanı elemanlarının adresleri & operatörü ile alınmaz.  
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bit alanı elemanlarının yapı içerisindeki organizasyonları C standartlarında ana hatlarıyla belirtilmiştir. Organizasyon büyük ölçüde derleyicileri 
    yazanların isteğine bırakılmıştır. Standartlar bu konuda şunları söylemektedir:

    - Bir bit alanı elemanı yapı nesnesinin içerisndeki bir byte içerisinde konumlandırıldığında  eğer onu izleyen yapının bir bit alanı elemanı varsa
    izleyen bit alanı elemanı eğer o byte'ın içerisine sığacak durumdaysa kesinlikle o byte'ın içerisinde konumlandırılır. Ancak izleyen bit alanı elemanı
    o byte'ın içerisine sığmıyorsa bu durumda o byte ile sonraki byte içerisine konumlandırılabilir ya da sonraki byte'tan başlayarak konumlandırılabilir. 
    Bu konuda davranış derleyicileri yazanların isteğine bırakılmıştır. Örneğin:

    struct SAMPLE {
        int a;
        int b: 3;
        int c: 5;
        int d;
    };

    Burada yapıu nesnesinin b ve c bit alanı elemanları aynı byte içerisinde konumlandırılır. Çünkü b elemanı 3 bit olduğundan aynı byte'ta 5 bitlik 
    daha yer vardır ve c elemanı bu 5 bitlik yere sığmaktadır. Örneğin:

    struct SAMPLE {
        int a;
        int b: 3;
        int c: 7;
        int d;
    };

    Burada b bit alanı elemanı 3 bite yerleştirildikten sonra aynı byte içerisinde 5 bit kaldığından dolayı c elemanı için yeterli yer yoktur. 
    Bu durumda c elemanı bir kısmı o byte bir kısmı diğer byte'ta (overlapped) bir biçimde yerleştirilebileceği gibi tamamen sonraki byte'tan 
    itibaren de yerleştirilebilir. Bu durum	derleyicileri yazanların isteğine bırakılmıştır. Yani yukarıdaki yerleşim bir derleyicide aşağıdaki gibi olabilir:

    cccc cbbb
    ???? ??cc

    Ya da örneğin şöyle olabilir:

    ???? ?bbb
    ?ccc cccc
    
    Buradaki ?'leri derleyicinin yerleştirme yapmadığı boş bitleri temsil etmektedir. 

    Ayrıca standartlar byte içerisindeki bit alanı elemanlarının yerleşim bitlerinin yüksek anlamlı bitlerden düşük anlamlı bitlere doğru mu yoksa 
    düşük anlamlı bitlerden yüksek anlamlı bitlere doğru mu yapılacağını yine derleyicileri yazanların isteğine bırakmıştır. Örneğin:

    struct SAMPLE {
        int a;
        int b: 3;
        int c: 5;
        int d;
    };

    Burada b elemanının düşük anlamlı bitlerde, a elemanının yüksek anlamlı bitlerde olup olmayacağı derleyicileri yazanların isteğine bırakılmıştır. Yani dizilim
    aşağıdaki iki biçimdeki gibi de olabilir:

    cccc cbbb
    bbbc cccc
    
    Görüldüğü gibi bit alanı elemanlarının yapı nesnesi içerisindeki yerleşimleri konusunda derleyiciden derleyiciye değişebilecek 
    büyük bir belirsizlik vardır. Programcıların belli bir derleyicinin uyguladığı yerleşime güvenerek çıkarım yapmaması gerekir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bit alanı elemanları aslında yer kazancı sağlamak için kullanılmaktadır. Örneğin biz bir zaman bilgisini bir yapı içerisinde şöyle tutmak isteyelim:

    struct TIME {
        int hour;
        int minute;
        int second;
    };

    Bu yapı muhtemelen Windows ve UNIX/Linux sistemlerinde 12 byte yer kaplayacaktır. Halbuki aslında zamanın bu bileşenlerinin dörder byte yer kaplamasına 
    gerek yoktur. Saat bilgisi 5 bit ile, dakika ve saniye bilgileri 6 bit ile temsil edilebilir:

    struct TIME {
        unsigned hour: 5;
        unsigned minute: 6;
        unsigned second: 6;
    };

    Pekiyi bu yapı kaç byte yer kaplayacaktır? Bitleri saydığımızda 17 olduğunu görüyoruz. Ancak 17 bit biçiminde bir nesne oluşturulamaz. Üstelik derleyicinin 
    uyguladığı bir hizalama da vardır. Anımsanacağı gibi derleyiciler tüm yapı nesnesi için belli değerin katlarında yer ayırmaktadır. O zaman bu yapı muhtemelen bellekte
    4 byte yer kaplayacaktır (hizalama uygulanacağına dikkat ediniz). Bu yapyı aşağıdaki gibi pragma direktifleriyle BYTE hizalamasına getirsek bile derleyicilerin 
    çoğu bit alanı elemanları içeren yapı nesneleri için yine 4'ün katları kadar yer ayırmaktadır. Yani aşağıdaki gibi bir pragma direktifi muhtemelen derleyicinizde 
    bu yapının daha az yer kaplaması için yeterli olmayacaktır:

    #pragma pack(1)

    struct TIME {
        unsigned hour: 5;
        unsigned minute: 6;
        unsigned second: 6;
    };
        
    Bu konu sonraki paragrafta ele alınacaktır. Örneğin bir test sınavında bir sorunun yanıtı A, B, C, D, E şıklarından biri olabilir. Bu yanıtı biz 1 byte 
    içerisinde değil 3 bit içerisinde saklayabiliriz. 

    Örneğin bir satranç oyununda hamleler kaydedilebilmektedir. Kayıt için kullanılan en yaygın format bir taşın başlangıç ve bitiş karesinin yazılmasıdır. 
    Örneğin "e2-e4" hamlesi demek e2 karesindeki taşın e4 karesine oynanmış olması demektir. Satranç 8x8'lik bir tahtada oynandığına göre aslında harfler ve sayılar
    üçer bit ile temsil edilebilir. Böylece bir hamle 12 bit ile kaydedilebilir. 

    Bit alanı elemanları yer kazancı sağlamak için düşünülmüştür. Ancak sıradan nesneler için bu kazanaç genellikle önemli olmaz. Yani örneğin bir zaman bilgisinin
    int elemanlarla 12 byte ile kodlanması programlamada aslında hiç önemli değildir. Ancak milyonlarca zaman bilgisinin saklanacağı bir durumda bu önemli olabilmektedir. 
    Yani özetle tekil nesneler için bit alanı kullanarak yer kazancı sağlamaya çalışmak iyi bir teknik değildir. Ancak çok sayıda nesne söz konusu ise
    bit alanı elemanlarıyla yer kazancı sağlama iyi bir fikir haline gelmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıda da belirtildiği gibi bit alanları her ne kadar yer kazancı sağlamak için kullanılıyor olsa da bunun gerçekleşmesi bazı koşullara bağlıdır. 
    Örneğin bizim yapımız toplam 17 bit yer kaplıyorsa derleyiciler 17 bitlik bir nesne oluşturamadıklarından dolayı mecburen byte'ın katları kadar nesne 
    oluştururlar. Bu da en az 3 byte olur. Öte yandan derleyiciler hizalama da uyguladıklarından tüm nesne için belli sayının katları kadar yer ayırabilmektedir. 
    Örneğin:

    struct TEST_ANSWER {
        unsigned answer: 3;
        usnsigned result: 1;
    };

    Biz burada 1000000 TEST_ANSWER değerini bir yapı dizisi biçiminde tutmaya çalışalım:

    struct TEST_ANSWRE answers[1000000];

    Burada bir tane struct TEST_ANSWER nesnesi hizalama yüzünden 4 byte yer kaplayabilecektir. Bu durumda da bizim bit alanı elemanlarını kullanmamıza bir gerek
    kalmayacaktır. Tabii derleyicilerde daha önceden de belirttiğimiz gibi hizalama kontrol altına alınabilmektedir. Fakat bu taşınabilir bir durum değildir. 

    O halde yer kazancı sağlanmak isteniyorsa hizalama da dikkate alınarak yapının birden fazla aynı türden olguyu depolayacak biçimde oluşturulması uygun olur. 
    Örneğin:

    struct TEST_ANSWER {
        unsigned answer1: 3;
        usnsigned result1: 1;
        unsigned answer2: 3;
        usnsigned result2: 1;
        unsigned answer3: 3;
        usnsigned result3: 1;
        unsigned answer4: 3;
        usnsigned result4: 1;
    };

    Burada bu yapı hizalama yüzünden yine 4 byte yer kaplayacaktır. Ancak bir tane struct TEST_ANSWER nesnesi aslında idört farklı test yanıtını ve sonucunu tutmaktadır.

    Bit alanı elemanları kullanırken programcının yapının o derleyicideki sizeof değerine dikkat etmesi tavsiye edilir. Aksi takdirde bit alanı 
    elemanlarından bir yer kazancı sağlanmayacağı gibi hız bakımından bir dezavantaja da yol açılmış olabilir. Çünkü bit alanı elemanları aslında derleyici tarafından
    bitsel makine komutlarıyla yapay bir biçimde oluşturulmaktadır. 
    
    Bizim bu konudaki önerimiz şudur: Bit alanı elemanlarını çok önemli bir yer problemi olmadıkça kullanmayınız. Eğer kullanacaksanız bundan bir 
    kazanç sağlayıp sağlamayacağınızı test ederek görünüz. Ancak bu konudaki davranışın da derleyiciden derleyiciye değişebileceğini aklınızda bulundurunuz.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bit alanlarıyla ilgili son bir özellik daha vardır. Bir bit alanı elemanı için isim belirtilmeyebilir. Bu durumda bu eleman yine yer kaplar. Ancak
    o elemanın bir ismi olmadığı için kullanılamaz. Bu özelllik "padding" yapmak için düşünülmüştür. Örneğin:

    struct SAMPLE {
        int a: 4;
        int : 4;
        int b: 5;
        int : 3;
    };

    Burada a elemanından sonra ve b elemanından sonra padding amaçlı boşluklar bırakılmıştır. Buradaki "padding" terimi kullanılmayan ama yer kaplayan
    dolayısıyla sonraki elemanın diğer byte'tan başlamasını sağlayan boşlukları anlatmaktadır. Genellikle böyle padding gereksinimi seyrek bir biçimde
    ortaya çıkmaktadır.

    Eğer bit alanı elemanının uzunluğu 0 verilirse bu da özel bir anlama gelir. Bu durumda sonraki bit alanı elemanı aynı byte'ın içerisine yerleştirilmez,
    Sonraki byte'ın içine yerleştirilir. Örneğin:

    struct SAMPLE {
        int a: 5;
        int :0 ;
        int b: 7;
        /* ... */
    };

    Burada programcı a elemanından sonraki b elemanının aynı byte içerisinden başlanarak yerleştirilmesini istememiştir. Artık b elemanı kesinlikli sonraki byte'tan
    itibaren yerleştirilecektir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bütün anlattığımız durumlar eşliğinde bir alanları elemanları için şu tavsiyelerde bulunabiliriz: Bir alanı elemanlarının yerleşimi, bundan elde edilecek 
    yer kazancı derleyiciden derleyiciye değişebilmektedir. Bu konuda standartlar bir belirlemede bulunmamıştır. Eğer birtakım olguları az bit ile
    standart bir biçimde ifade edecekseniz char gibi int gibi türleri kullanıp bit işlemleriyle bitleri bu nesnelerin içerisine yerleştirmelisiniz. 
    Zaten derleyiciler de bit alanı elemanlarını bit operatörleriyle işleme sokmaktadır.Bütün bunların sonucu olarak C'de b,t alanı elemanlarının 
    kullanımı çok sınırlıdır. Bu nedenle programlarda bunlarla çok seyrek karşılaşırsınız.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İçerisinde byte'ların bulunduğu ikincil bellekleredeki bölgere "dosya (file)" denilmektedir. Kullanıcılar dosyalara bir isim ile erişirler. 
    Dosyaların bütün organizasyonu işletim sistemi tarafından sağlanmaktadır. Bir dosyanın ismi verildiğinde işletim sistemi onun ikincil bellekteki
    yerini belirleyebilmektedir. 

    Modern işletim sistemlerinde dosyalar "dizin (directory)" denilen kapların içerisinde bulunmaktadır. Bir dizinde hem dosyalar hem de başka dizinler 
    bulunabilir. Böylece dizinler bir ağaç oluştururlar. Bu ağacın en dışındaki dizine "kök dizin (root directory)" denilmektedir. Genel olarak bir dizin içerisinde
    aynı isimli bi,rden fazla dosya ya da dizin bulunamaz. Ancak farklı dizilerde aynı isimli dizin ya da dosyalar bulunabilir. O halde biz "test.txt" dosya
    ismini kullandığımızda pek çok dizin içerisinde bu isimli bir dosya var olabilir. İşte bir dosyanın hangi dizinin içerisinde olduğunu anlatan 
    yazısal ifadelere "yol ifadeleri (paths)" denilmektedir. Kullanıcı işletim sistemine yalnızca dosyanın ismini değil dizin bilgisini de içeren 
    yol ifadesini verir. 

    Windows işletim sisteminde her disk biriminin ayrı bir kökü vardır. Örneğin bu sistemlerde biz bir çıkartılabilir flash disk disk taktığımızda sistem 
    onu ayrı birim olarak görür. O birimin ayrı bir kökü vardır. Bu birimlere Windows sistemlerinde "sürücü (drive)" denilmektedir. UNIX/Linux sistemleri
    ve macOS sistemleri sürücü kavramını kullanmamaktadır. Bu sistemlerde tek bir ağaç vardır. 

    UNIX(Linux ve macOS sistemlerinde bir bellek birimi sisteme iliştirildiğinde sistem onu ağaç içerisinde bir dizinin altına monte etmektedir. 
    Bu işleme "mount" işlemi denilmektedir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    73. Ders - 09/03/2023 - Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yol ifadelerinde dizin geçişlerinde UNIX/Linux sistemleri ve macOS sistemleri "/" karakterini, Windows sistemleri ise "\" karakterini kullanmaktadır.
    Ancak Windows sistemleri dizin geçişlerinde UNIX/Linux uyumunu korumak için "/" karakterini de desteklemektedir. Ancak UNIX/Linux sistemleri genel olarak
    "\" karakterini desteklememektedir. Örneğin:

    /a/b/c.text			UNIX/Linux ve macOS sistemlerinde (ancak Windows sistemleri de destekliyor)
    \a\b\c.text			Windows sistemlerinde, ancak UNIX/Linux ve macOS sistemleri desteklemiyor

    Yol ifadeleri "mutlak (absolute)" ve "göreli (relative)" olmak üzere ikiye ayrılmaktadır. Yol ifadesinin ilk karakteri UNIX/Linux sistemlerinde "/",
    Windows sistemlerinde "\" ise böyle yol ifadelerine mutlak yol ifadeleri denilmektedir. Örneğin:


    /a.txt
    /a/b/c.txt
    \windows\notepad.exe

    Mutlak yol ifadeleri her zaman kök dizinden (root) yer belirtmektedir. Örneğin:

    /usr/include/stdio.h

    Burada kökün altında "usr" dizini, "usr" dizininin altında "include" dizini vardır ve bu "include" dizininin altındaki "stdio.h" dosyası belirtilmektedir.

    Eğer yol ifadelerinin ilk karakterleri UNIX/Linux ve macOS sistemlerinde "/" değilse, Windows sistemlerinde de "\" değilse böyle yol ifadeleri 
    göreli yol ifadeleridir. Örneğin:

    a/b/c.txt
    temp\test.txt
    test.txt

    Pekiyi göreli yol ifadeleri nereden itibaren yer belirtmektedir? İşte işletim sistemlerinde çalışmakta olan programlara "proses (process)" denilmektedir. 
    Her prosesin bir "çalışma dizini (current working directory)" vardır. Prosesin çalışma dizini proses yaratıldığında (yani program çalışmaya başladığında)
    belli bir dizindir. Ancak programın çalışması sırasında programcı tarafından değiştirilebilmektedir. Göreli yol ifadeleri prosesin çalışma dizininden 
    itibaren yer belirtir. Yani göreli yol ifadeleri için orijin noktası prosesin çalışma dizinidir. Örneğin programımızın çalışma dizini "c:\temp" olsun. 
    Biz de "a/b/c.txt" biçiminde göreli bir yol ifadesi belirtmiş olalım. İşte aslında biz bu göreli yol ifadesi ile "c:\temp\a\b\c.txt" yol ifadesini 
    belirtmiş olmaktayız. Aynı durumda "test.txt" yol ifadesini belirtirsek bu sefer aslında biz "c:\temp\test.txt" yol ifadesini belirtmiş oluruz. 

    İşletim sistemlerinde o anda çalışmakta olan her programın (yani proseslerin) çalışma dizinleri birbirinden farklı olabilmektedir. Pekiyi programın 
    çalışma dizini için başında (yani akış main fonksiyonuna girdiğinde) neresidir? İşte işletim sistemlerinde bir program başka bir program tarafından 
    çalıştırılmaktadır. Bir programın çalıştırdığı programa işletim sistemleri terminolojisinde "alt proses (child prosess)", çalıştıran programa 
    da "üst proses (parent prosess)" denilmektedir. Genel olarak işletim sistemlerinin büyük bölümünde üst prosesin çalışma dizini alt prosese aktarılmaktadır.
    Bu durumda özetle şunlar söylenebilir:

    - Eğer programı komut satırından çalıştırıyorsanız komut satırı programının çalışma dizini çalıştırdığınız programın çalışma dizini olacaktır.

    - Eğer programı IDE'den çalıştırıyorsanız. IDE'ler genellikle çalıştırdıkları programın çalışma dizinini proje dizini olarak ayarlamaktadır.
    Ancak bunun için IDE'nin dokümanlarına başvurabilirsiniz. Pek çok IDE'de zaten bu dizin ayarlanabilmektedir. 

    Prosesin çalışma dizinini alan ve onu set eden standart C fonksiyonları yoktur. Bu işlemler işletim sistemine bağlı olarak o işletim sistemine özgü
    fonksiyonlarla yapılabilmektedir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Windows sistemlerinde "sürücü (drive)" kavramı da olduğuna göre bu sistemlerdeki mutlak yol ifadeleri hangi sürücünün kökünü referans almaktadır?
    Windows sistemlerinde mutlak yol ifadelerine sürüvü bilgisi de eklenebilmektedir. Örneğin:

    d:\temp\test.txt

    Burada artık sürücü de belirtildiği için kök dizinin D sürücüsünün kök dizini olduğu anlaşılmaktadır. Sürücü de içeren yol ifadelerine Windows
    sistemlerinde "tam yol ifadeleri (full path names)" denilmektedir. Pekiyi aşağıdaki mutlak yol ifadesi hangü sürücünün kökündne itibaren yer belirtir?

    \a\b\c.txt

    İşte Windows sistemlerinde proses çalışma dizini sürücü bilgisi de içermektedir. Prosesin çalışma dizini hangi sürücüye ilişkinse sürücü belirtilmemiş 
    olan mutlak yol ifadeleri o sürücüsünün kökünden itibaren yer belirtmektedir. Örneğin Windows'ta prosesimiizn çalışma dizini "d:\ali" alsun. 
    Biz de "\a\b\c.txt" biçiminde sürücü içermeyen bir mutlak yol ifadesi belirtmiş olalım. Buradaki kök D sürücüsünün köküdür. 

    Windows sistemlerinde sürücü içeren göreli yol ifadeleri de oluşturulabilmektedir. Örneğin:

    C:a\b\c.txt

    Burada yol ifadesi hem görelidir hem de bir sürücü belirtilmiştir. İşte Windows sistemlerinde bu tür durumlarda prosesin bazı çevre değişkenlerine 
    bakılmaktadır. Bu çevre değişkenleri set edilmediyse yol ifadesi sanki mutlak yok ifadesi gibi ele alınmaktadır. Böylesi yol ifadelerini tercih 
    etmeyiniz.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Dosya ve dizin isimlendirmesinde kullanılabilecek karakterler Windows sistemlerinde ve UNIX/Linux ve macOS sistemlerinde farklılıklar içerebilmektedir. 
    Bu konu kullanılan kullanılan "dosya sistemi (file system)" ile ilgilidir. Ancak Windows dünyası ile diğer dünya arasında en önemli farklılıklardan biri 
    Windows dünyasında dosya ve dizin isimlerinin büyük harf küçük harf duyarlılığının olmaması ancak UNIX/Linux ve macOS sistemlerinde olmasıdır. 
    Örneğin Windows sistemlerinde aşağıdaki iki yol ifadesi arasında hiçbir farklılık yoktur. Ancak UNIX/Linux ve macOS sistemlerinde farklılık vardır:

    test.text
    Test.txt

    Dosya uzunatılarının (file extensions) işletim sistemi için bir önemi yoktur. Dosya uzantıları aslında insanların dosyanın neden oluşturulduğunu 
    anlamaları için kullanılmaktadır. Bir dosya ya da dizin ismi uzantıya sahip olmak zorunds değildir. Dizinler genellikle uzantıya sahip olmazlar. 
    Ancak istenirse dizinlere uzantı verilebilir. Bir dosya isminde genel olarak birden fazla "." karakteri kullanılabilemktedir. Windows sistemlerinde 
    Unicode karakterler dosya ismi olarak kullanılabilmektedir. Ancak UNIX/Linux sistemlerinde genel olarak kullanılamamaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C'de dosya işlemleri prototipleri <stdio.h> içerisinde olan standart C fonksiyonlarıyla yapılmaktadır. Bu dosya fonksiyonlarının hepsi "f" harfi ile
    başlatılarak isimlendirilmiştir. Örneğin:

    fopen, fclose, fread, fwrite, fgetc, ... 

    Aslında bütün dosya işlemleri işletim sisteminin kontrolü altındadır. Yani tüm dosya işelmleri aslında işletim sistemlerinin sistem fonksiyonları 
    tarafından yapılmaktadır. Programlama dili ne olursa olsun dosya işlemleri eninde sonunda işletim sisteminin içerisindeki fonksiyonların
    çağrılmasıyla gerçekleştirilmektedir. Yani dosya işlemleri aslında srandart C fonksiyonlarıyla değil bu fonksiyonların çağırdığı sistem fonksiyonlarıyla
    yapılmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir dosya üzerinde işlem yapabilmek için önce dosyanın açılması gerekir. Dosyanın açılması sırasında işletim sistemi birtakım hazırlık işlemleri 
    yapmaktadır. Dosyanın açılması için fopen isimli standart C fonksiyonu kullanılmaktadır. fopen fonksiyonunun prototipi şöyledir:

    FILE *fopen(const char *path, const char *mode);

    Fonksiyonun birinci parametresi açılacak dosyanın yol ifadesini belirtmektedir. Bu yol ifadesi mutlak ya da göreli olabilmektedir. İkinci parametre "açış 
    modunu" belirtir. Dosya açış modları aşağıdakilerden biri olabilir:

    Açış Modu						Anlamı
    ---------						------
    "r"								Olan dosyayı açar, yalnızca okuma yapılabilir.
    "r+"							Olan dosyayı açar, hem okuma hem de yazma yapılabilir.
    "w"								Dosya yoksa yaratır ve açar, dosya varsa içini sıfırlar (truncate eder) ve açar, yalnızca yazma yapılabilir.
    "w+"							Dosya yoksa yaratır ve açar, dosya varsa içini sıfırlar (truncate eder) ve açar, hem okuma hem de yazma yapılabilir.
    "a"								Dosya varsa olanı açar, yoksa yaratır ve açar, yalnızca yazma yapılabilir. Her yazılan sona eklenir.
                                    (Dosyanın sonunun dışında başka bir yerine yazma yapılamaz)
    "a+"							Dosya varsa olanı açar, yoksa yaratır ve açar, hem okuma hem yazma yapılabilir. Her yazılan sona eklenir. 
                                    Ancak herhangi bir yerden okuma yapılabilir.

    Bir dosya çeşitli nedenlerden dolayı açılamayabilir. Bu durumda fopen fonksiyonu başarısız olur. Örneğin dosyayı "r" modunda açmak istediğimizde 
    dosya mevcut değilse açık işlemi başarısız olur. İşletim sistemlerinde her prosesin açabileceği maksimum bir dosya sayısı vardır. Dosya ismi 
    geçersizse dosya açılamaz. Açış modu yukarıda belirtilenlerin dışındaysa yine fopen başarısz olur. Ayrıca işletim sistemleri dosyalar üzerinde 
    bazı erişim kontrolleri de uygulayabilemktedir. Yani biz her istediğimiz dosyayı her istediğimiz modda açamayabiliriz. Bu durumlarda da yine fopen 
    fonksiyonu başarısız olur. 

    fopen fonksiyonu başarılı olduğunda FILE isimli bir yapı nesnesinin adresiyle geri döner. fopen fonksiyonun geri döndürdüğü nesne güvenli bir biçimde
    tahsis edilmiştir. FILE bir typedef ismidir. <stdio.h> içerisinde aşağıdaki biçimde typedef edilmiştir:

    typedef struct {
        ....
    } FILE;

    Ancak C standartları bu FILE yapısının içeriği konusunda bir açıklama yapmamıştır. Dolayısıyla programcı bu yapının elemanlarıyla ilgilenmemelidir. 
    fopen fonksiyonun geri döndürdüğü FILE nesnesinin adresi diğer dosya fonksiyonlarına argüman olarak geçirilmektedir. Programcı bu FILE yapısının içeriği
    ile ilgili bilgi sahibi olmak zorunda değildir. fopen başarısız olursa NULL adrese geri döner. Programcı mutlaka fopen fonksiyonun başarısını
    kontrol etmelidir. 

    fopen fonksiyonun geri döndürdüğü FILE adresine İngilizce "stream" denilmektedir. Biz bu adrese Türkçe "dosya bilgi göstericisi" diyeceğiz. 

    O halde bir dosya tipik olarak şöyle açılır:

    FILE *f;

    if ((f = fopen("test.txt", "r")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXI T_FAILURE);
    }

    Aşağıdaki örnekte var olan bir dosya "r" modunda açılmak istenmiştir.

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;

    if ((f = fopen("test.txt", "r")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    printf("Ok\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Programcı bir dosyayla işlemlerini bitirince dosyayı kapatmalıdır. Dosyanın kapatılması ile açılması sırasında yapılan birtakım işlemler geri 
    alınmaktadır. Aslında dosya kapartılmasa bile en kötü olasılıkla program biterken exit işlemşyle birlikte zaten tüm açık dosyalar otomatik kapatılmaktadır.
    Yani aslında dosyaların kapatılması mutlak anlamda gerekli değildir. Ancak artık gereksinim duyulmayan kaynakların geri bırakılması iyi bir tekniktir.
    Dolayısıyla programcının programın bitmesini beklemeden artık kullanmayacağı dosyaları kapatması iyi bir tekniktir.

    Dosyayı kapatmak için fclose fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:

    int fclose(FILE *f);

    Fonksiyon fopen fonksiyuonundan elde edilen dosya bilgi göstericisini (stream) parametre olarak alıp dosyayı kapatmaktadır. Dosya başarılı bir biçimde
    kapatılmışsa fclose 0 değerine geri döner. Eğer dosya başarılı bir biçimde kapatılamadıysa fclose <stdio.h> içerisinde define edilmişl olan EOF değerine geri 
    dönmektedir. C standartları EOF sembolik sabitinin kaç oalrak define edileceği konusunda bir belirlemede bulunmamıştır. Ancak negatif int bir sabit ifadesi 
    olarak define edilmesi gerektiği standartlarda belirtilmiştir. Tipik olarak derleyicilerin hemen hepsi EOF sembolik sabitini -1 olarak define etmektedir:

    #define EOF		(-1)

    fclose fonksiyonun gheri dönüş değerinin kontrol edilmesine gerek yoktur. Programcı her şeyi doğru yaptıysa bu fonksiyon başarısız olamaz. 
    Ancak örneğin programcı fonksiyona yanlış bir adres geçtiyse ya da örneğin zaten kapatılmış bir dosyayı yeniden kapatmaya çalışmışsa 
    fonksiyon başarısız olabilir. Biz de örneklerimizde fonksiyonun başarısını kontrol etmeyeceğiz. 

    Aşağıdaki örnekte bir dosya açıldıktan sonra kapatılmıştır. Dosyanın programın sonunda kapatılmasının bir önemi yoktur. Çünkü nasıl olsa birazdan
    program bittiğinde dosya da fclose işlemi ile otomatik biçimde kapatılacaktır. Ancak ne olursa olsun programcıların çoğu okunabilirliği 
    artırmak için program sonunda bile olsa dosyaları fclose ile kapatmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;

    if ((f = fopen("test.txt", "r")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    printf("Ok\n");

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                                            74. Ders - 14/03/2023 - Salı
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    İşletim sistemleri bir dosya açıldığında dosya içerisindeki tüm byte'lara ilk byte 0 olmak üzere bir offset numarası karşı düşürmektedir. 
    Dosyanın ilk byte'ı 0'ıncı offset'tedir. Sonraki byte 1'incş offset'te olacak biçimde her byte'ın bir offset numarası vardır. 
    İşte işletim sistemi açık her dosya için "o andaki işlem pozisyonunu" belirten bir offset de tutmaktadır. Bu offset'e "dosya göstericisi (file pointer)"
    denilmektedir. Dosya göstericisi o anda o dosya üzerinde yapılacak okuma yazma işlemlerinin dosyanın neresinden itibaren yapılacağını belirtmektedir. 
    Dosya açıldığında dosya göstericisi 0'ıncı offset'tedir. Okuma ve yazma sırasında her zaman işlem dosya göstwricisinin gösterdiği offset'ten itibaren 
    yapılmaktadır ve dosya göstericisi okunan ya da yazılan miktar kadar otomatik olarak ilerletilmektedir. Örneğin:
,
    01234
    xxxxx

    Burada x'ler dosyadaki byte'ları gösteriyor olsun. Dosya göstericisi de 2'inci offset'te olsun. Biz bu offset'ten iki byte okursa 2'inci ve 3'üncü 
    offset'teki byte'ları okumuş oluruz. İki byte okuduğumuz için dosya göstericisi de 2 ilerletilecek ve artık 4'üncü offset'i gösterir duruma gelecektir. 
    Dosya göstericisi adeta "kalemin ucu" gibi bir imleç belirtmektedir. 

    Dosya göstericisinin dosyanın en son byte'ından onrakş olmayan byte'ı göstermesi durumuna EOF (End of File) durumu denilmektedir. Örneğin:

    012345
    xxxxx

    Burada dosya gösteicisi 4'üncü offset'te olsun. Biz de 1 byte okumuş olalım. Artık dosya göstericisi 5'inci offset'i gösterir durumdadır. 
    Ancak 5'inci offset'te herhangi bir byte yoktur. Dosya göstericisi artık EOF durumundadır. EOF durumundan okuma yapılamaz. Ancak yazma (eğer 
    açış modu da müsaitse) yapılabilir. EOF durumunda dosyaya yazma yapıldığında dosyaya byte eklenmiş olur. Dosya göstericisi EOF'ta değilse 
    yazım sırasında dosya büyütülmez. Dosya göstericisinin gösterdiği yerdeki byte'lar ezilerek yazım yapılır. Bir dosyanın uzunluğunu artırmanın tek yolu
    dosya göstericisini EOF'a çekip yazma yapmaktır. Tabii dosya göstericisi dosyanın sonund aolmasa bile biz dosyaya dosya göstericisinin gösterdiği yerden 
    itibaren dosyadaki byte sayısından daha fazla byte yazarsak dosya yine büyülür dosya göstericisi de EOF durumuna çekilir. Örn3ğin:

    012345
    xxxxx
    
    Burada dosya göstericisi 2'inci offset'te bulunuyor olsun. Biz burada dosyaya 5 byte yazarsak dosya 2 byte büyütülür:

    01234567
    xxyyyyy
    
    Dosya göstericisi artık 7'inci offset'te olacaktır. Bu da yine EOF durumudur.

    Dosya göstericisi EOF'ta olsun ve bir dosyaya 1 byte yazalım. Şimdi biz dosyaya 1 byte eklemiş oluruz. Ancak yine göstericisi EOF durumundadır.
    Dolayısıyla artık yeni yazılanlar da yine dosyanın sonuna eklenecektir. 

    Dosya göstericisi dosya başına bir tane değil her açık dosya için bir tanedir. Yani örneğin biz aynı dosyayı fopen ile iki kez açmışsak
    her iki açımın dosya göstericisi farklı olabilir:

    FILE *f1, *f2;

    if ((f1 = fopen("test.txt", "r") == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }
    if ((f2 = fopen("test.txt", "r") == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    Burada biz f1 dosya bilgi göstericisini kullanıp okuma yaparsak f2 dosya bilgi göstericisinin dosya göstericisi değişmez, f1 dosya 
    bilgi göstericisinin dosya göstericisi değişir. 

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    fgetc dosya göstericisinin gösterdiği yerdne itibaren 1 byte okuyup bize okuduğu byte'ı vermektedir. Fonksiyonun ptototipi şöyledir:

    int fgetc(FILE *f);

    Fonksiyonun geri dönüş değerinin int türdne olması kişilere tuhaf gelebilmektedir. Ancqak fgetc başarısız olabilirve başarısızlık durumunda 
    EOF değerine(tipik olarak -1) geri dönmektedir. Fonksiuonun geri dönüş değeri char ya da unsigned char olsaydı fonksiyonun başarısız mı olduğu 
    yoksa FF nmaralı byte'ı mı okuduğu anlaşılamazdı. Halbuki fonksiyonun geri dönüş değeri int olduğu için bu durum anlaşılabilmektedir. Fonksiyon 
    FF nmaralı byte'ı okursa 255 değerine (000000FF değerine) başarısız olursa -1 değerine (FFFFFFFF değerine) geri dönmektedir. 

    fgetc temelde iki nedenden dolayı başarısız olabilir: 

    1) Dosya göstericisi EOF durumundadır ve bu nedenle okuma yapılamamıştır.
    2) Disk ilgili ciddi ve patolojik bir problem oluşmuştur. 

    Diskle ilgili bu tür ciddi hatalara "IO hataları (IO error)" denilmektedir. IO hataları çok seyrek oluşabilecek hatalardır. EOF durumundna dolayı 
    okuma işleminin başarısız olması ise patolojik bir durum değildir. Bu durum normal bir başarısızlık durumudur.

    Aşağıda dosya sonuna kadar dosyadan byte byte okuma yapıp bunu yazdıran bir program örneği verilmiştir. Programın dönüsü şöyledir:

    while ((ch = fgetc(f)) != EOF)
        putchar(ch);

    Burada her byte okundukça dosya göstericisi bir ilerletilecek ve en sonunda dosya göstericisi EOF durumuna gelecektir. Böylece döngüden 
    çıkılacaktır. Tabii IO hatası oluşursa da fgetc EOF değeri ile geri dönmektedir. Bu da döngünün sonlanmasına yol açacaktır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;
    int ch;

    if ((f = fopen("test.txt", "r")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    while ((ch = fgetc(f)) != EOF)
        putchar(ch);

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Son yapılan işlemin nedne başarısız olduğunu veren iki standart C fonksiyonu bulundurulmuştur: ferror ve feof. Fonksiyonların prototipleri 
    şöyledir:

    int ferror(FILE *f);
    int feof(FILE *f);

    Fonksiyonlar fopen fonksiyonundna alınan dosya bilgi göstericisini parametre olarak almaktadır.

    ferror fonksiyonu son yapılan işlem IO hatasından dolayı başarısz olmuşsa sıfır dışı bir değere, IO hatasındna dolayı başarısz olmamışsa 0 değerine
    geri dönmektedir. feof fonksiyonu ise son yapılan işlem EOF durumundan dolayı başarısz olmuşsa sıfır dışı bir değere, EOF durumundan dolayı başarısız
    olmamışsa 0 değerine geri dönmektedir. Bu iki fonksiyon tipik olarak bir başarısızlık sonrasında başarısızlığın nedenini sorgulamak için kullanılmaktadır.
    Bir başarısızlık söz konusu olmadığı durumda bu fonksiyonlar 0 değerine geri döner. Özellikle feof fonksiyonunun işlevi yanlış anlaşılmaktadır. Örneğin:

    012345
    xxxxx

    Burada dosya gösteicisi 4'üncü offset'te olsun. Biz de fgetc fonksiyonu ile dosyadan 1 byte okumuş olalım. Burada biz feof fonksiyonu ile durumu sorgulamaya 
    çalışsak feof 0 değerini verecektir. Halbuki dosya göstericisi EOF durumundadır. Çünkü feof ve ferror "son yapılan işlemin başarısızlığının nedenini"
    tespit etmekte kullanılır. Son yapılan işlem başarısız değilse bu fonksiyonlar 0 ile geri döner. feof fonksiyonundaki yanlış anlaşılma şudur: 
    Yeni öğrenen kişiler sanki feof fonksiyonunun o andaki dosya göstericisinin konumuna göre durum tespiti yaptığını sanmaktadır. Halbuki feof 
    "son yapılan işlemin başarısızlığının nedeninin EOF nedeni ile mi olduğuna" bakmaktadır. Başka bir deyişle ferror ve feof ancak başarısızlık durumunda 
    anlamlı bir biçimde kullanılabilmektedir. 

    Pekiyi bu fonksiyonlar neden bu biçimde çalışmaktadır? İşte bu tasarım stream sisteminin tasarımı ile ilgilidir. FILE yapısı içerisinde iki 
    eleman vardır: feof bayrağı ve ferror bayrağı. Bu bayraklar başarısızlık durumunda başarısızlığa göre set edilmektedir. feof ve ferror fonksiyonları 
    aslında yalnızca bu bayrakların durumuna bakmaktadır. feof fonksiyonu dosya göstericisinin durumuna bakmaz. FILE yapısı içerisindeki feof bayrağının durumuna 
    bakar. Bu bayrağı da okuma fonksiyonları EOF durumundan dolayı başarısızlık durumunda set etmektedir. 

    Yeni öğrenenlerin sık yaptığı bir hata şudur:

    while (!feof(f)) {
        ch = fgetc(f);
        putchar(ch);
    }

    Burada programcı EOF'a gelinmediği sürece işlem yapmak istemiştir. Ancak bu kod parçası kusurludur. Dosyanın son byte'ının fgetc tarafından 
    okunduğunu varsayalım. Bu durumda putchar son byte'ı yazdıracaktır. Ancak feof hale sıfır değerini verecektir (çünkü son işlem başarısız olmamıştır).
    Bu durumda döngü yinelenecek fgetc bu sefer EOF'tan dolayı başarısız olacak putchar EOF değerini (-1 değeirni) yazdırmaya çalışacaktır. 
    Halbuki döngünün şöyle oluşturulması gerekir:

    while ((ch = fgetc(f)) != EOF)
        puthcar(ch);

    Tabii aşağıdaki gibi bir döngü de oluşturulabilir:

    for (;;) {
        ch = fgetc(f);
        if (feof(f))
            break;
        putchar(ch);
    }

    Tabii bu döngü güzel gözükmemektedir. Aşağıdaki döngüyü yeniden inceleyelim:

    while ((ch = fgetc(f)) != EOF)
        puthcar(ch);

    Bu döngüden patolojik bir IO hatası ile de çıkmış olabiliriz. Bu tür durumlarda programcının bu IO hatasını kullanıcıya bildirmesi 
    iyi bir tekniktir. Bunun bir yolu şudur:
    
    while ((ch = fgetc(f)) != EOF)
        puthcar(ch);

    if (ferror(f)) {
        fprintf(stderr, "Unexpected IO error!..n");
        exit(EXIT_FAILURE);
    }

    Tabii aynı kontrol aşağıdaki gibi de yapılabilrdi. Ancak bunu tavsiye etmiyoruz:

    while ((ch = fgetc(f)) != EOF)
        puthcar(ch);

    if (!feof(f)) {
        fprintf(stderr, "Unexpected IO error!..n");
        exit(EXIT_FAILURE);
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    fputc isimli standart C fonksiyonu dosya göstericisinin gösterdiği yere bir byte yazmak için kullanılmaktadır. Fonksiyonun prototipi şöyledir:

    int fputc(int ch, FILE *f);

    Fonksiyonun birinci parametresi yazılacak byte'ı belirtir. Bu parametre int türden olmasına karşın buradaki sayının en düşün anlamlı byte değeri 
    dosyaya yazılmaktadıır. Fonksiyonun ikinci parametresi yazılacak dosyaya ilişkin dosya bilgi göstericisini belirtir. Fonksiyon başarı durumunda 
    yazılan byte değerine başarısızlık duruymunda EOF değerine geri döner. Başarısızlığın nedeni IO hatası olabilir. (Tabii fonksiyon EOF'tan dolayı başarısz olmaz. 
    Çünkü EOF durumunda dosyaya yazma yapılırsa zaten bu durum dosyaya ekleme anlamına gelmektedir.) Fonksiyonb başarısız olduğunda ferrror bayrağı set edilir. 
    Dolayısıyla ferror fonksiyonu da sıfır dışı bir değere geri döner. 

    Yazma sırasında IO hatasının muhtemel nedenlerinden bazıları şunlar olabilmektedir:

    - Diskin bozuk olması
    - Çıkarılabilir (removable) bir diskin (örneğin flash bellekler) o anda çıkartılması
    - Diskin tamamen dolu olması
    - İşletim sistemlerinde dosyaya yazılabilecek maksimum byte sayısı sınırlı olabilmektedir. Bu sınır aşılmış olabilir.

    Aşağıdaki örnekte char türdne bir dizinin içerisindeki tüm byte'ler null karakter görülene kadar dosyaya yazılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;
    char s[] = "ankara";

    if ((f = fopen("test.txt", "w")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    for (size_t i = 0; s[i] != '\0'; ++i)
        if (fputc(s[i], f) == EOF) {
            fprintf(stderr, "Unexpected IO error!..\n");
            exit(EXIT_FAILURE);
        }

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İşletim sistemine göre dosya byte'lardan oluşmaktadır. Dosyanın içerisinde ne olduğunun bir önemi yoktur. Çünkü her şey byte'lardan oluşmaktadır.
    Ancak C'de işlemleri biraz kolaylaştırabilmek için dosyalar "text" ve "binary" olmak üzere ikiye ayrılmaktadır. İçerisinde yalnızca yazıların bulunduğu
    Örneğin Notepad'te oluşturduğumuz bir dosya text dosyadır. Ya da örneğin bir C programının akynak kodu bir text dosyadır. Ancak derleme ve link işlemi 
    sonucunda elde ettiğimiz ".obj", ".exe" dosyalarının içerisinde yazı yoktur. Bu dosyalar binary dosyalardır. Örneğin "jpeg" dosyaları, "bmp" dosyaları
    binary dosyalardır. Ancak "html" dosyaları text dosyalardır. Eğer bir dosyayı editöre çektiğimizde anlamlı şeyler görebiliyorsak bu bir text dosyadır.
    Örneğin biz bir exe dosyayı bir text editörle görüntülemek istesek bu text editör makine kodlarının byte'larını bize karakter olarak gösterecek ve 
    bunun sonucunda tuhaf karakterler görüntülenecektir. İşletim sistemi düzeyinde text dosya binary dosya diye bir ayrım yoktur.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                        75. Ders - 16/03/2023 - Persembe
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir yazıda bir karakterin aşağı satırın başında görüntülenmesi için UNXI/Linux ve macOS sistemlerinde '\n' karakteri kullanılmaktadır. Bu karaktere
    LF (Line Feed - 0A numaralı karakter) karakteri denilmektedir. Ancak Windows sistemlerinde (eskiden DOS sistemlerinde de böyleydi) bir karakterin 
    aşağı satırın başında görüntülenmesi için '\r' ve '\n' karakterlerinin bir arada peş peşe kullanılması gerekmektedir. '\r' karakterine 
    "Carriage Return (CR) - 0D numaralı karakter" denilmektedir. Yani Windows sistemlerinde "aşağı satırın başına geç" anlamında CR/LF çifti kullanılmaktadır. 

    C'de text dosyalarla işlemleri kolaylaştırabilmek için dosya açış modları "text mode" ve "binary mode" biçiminde ikiye ayrılmıştır. default açış modu
    text moddur. Binary modda açış için açış modunun sonuna "b" harfi eklenir. Örneğin "r" text mod anlamına gelir. Ancak "rb" binaey mod anlamına gelir.

    Text mode				Binary Mode

    "r"						"rb"
    "w"						"wb"
    "r+"					"r+b"
    "w+"					"w+b"
    "a"						"ab"
    "a+"					"a+b"

    Pekiyi dosyanın text mod ile binary mod açımı arasında ne farklılık vardır? 

    1) Dosya text modda açılmışsa dosyaya '\n' karakteri yazılmak istendiğinde UNIX/Linux ve macOS sistemlerinde dosyaya yalnızca bir byte'lık
    LF (0A) karakteri basılır. Ancak Windows sistemlerinde CR/LF (OD/0A) biçiminde iki karakter yazdırılır. Ancak dosya binary modda açılmışsa artık yazısal bir 
    anlam dikkate alınmayacağı için '\n' karakteri sistem ne olursa olsun LF (0A) byte'ı olarak yazıdırlacktır. Yani text modda '\n' karakterinin 
    dosyaya yazılması ilgili işletim sistemine göre o işletim sistemindeki tanıma göre yapılmaktadır. Ancak binary modda böyle bir durum
    söz konusu değildir. Bu durumda biz bir dosyayı Windows sistemlerinde "text" modda açarsak o dosyaya '\n' karakterini yazdığımızda aslında
    dosyaya \r\n karakterleri birlikte yazıdırılacaktır. Ancak binary modda dosyayı açarsak '\n' karakteri içimn yine '\n' karakteri basılacaktır. 
    Tabii UNIX/Linux ve macOS sistemlerinde bu anlamda "text mod" ile "binary mod" arasında bir farklılık söz konusu olmayacaktır. Ancak taşınabilirlik 
    için programcının yazısal içeriğe sahip olmayan dosyaları binary modda açması gerekir.

    2) Dosya text modda açılmışsa Windows sistemlerinde dosya göstericisi CR/LF çiftinin başını gösterir durumdaysa dosyadan bir karakter okunmak 
    istendiğinde her iki karakter de okunur. Ancak okumanın sonucu olarak LF (\n) karakteri verilir. Yani Windows'ta text modda LF ('\n') için CR/LF karakterleri
    yazdırıldığı için okunurken de ters işlem yapılmaktadır. Bu iki karakterin her ikisi de okunup sanki LF ('\n') karakteri okunmuş gibi davranılmaktadır. 
    Tabii UNIX/Linux ve macOS sistemlerinde dosya text modda açılmışsa dosya göstericisi CR/LF çifitini gösterdiğinde dosyadan bir byte okunacağı 
    zaman CR (\r) karakteri verilecektir. Sonra yine bir byte okunacağı zaman bu kez LF (\n) karakteri verilecektir. Windows'ta dosya binary modda açılmışsa 
    ve dosya göstericisi CR/LF çiftini gösteriyorsa dosyadan bir karakter okunduğunda CR karakteri okunur. Çünkü binary modda yazısal bir anlam uygulanmamaktadır.
    UNIX/Linux ve macOS sistemlerinde de okuma işleminde yine binary mod ya da text modda bir şey değişmemektedir. 

    O halde açış modu için şu tavsiyelerde bulunulabilir: Eğer bir dosyayı yazısal amaçlı açacaksanız text modda açmalısınız. Ancak yazısal olmayan bir dosya
    üzerinde işlem yapacaksanız dosyayı binary modda açmalısınız. 
    
    Aşağıdaki örnekte dosyay text ve binary modd açarak oluşan durumu gözlemleyiniz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;

    if ((f = fopen("test.txt", "w")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    fputc('a', f);
    fputc('\n', f);
    fputc('b', f);

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Ekranı temsil eden stdout dosyasının ve klavyeyi temsil eden stdin dosyasının "text modda "açaılmış olduğu varsayılmaktadır. Bu durumda biz Windows'ta
    imleci aşağı satırın başına geçirmek için ekrana yalnızca LF karakterini göndermeliyiz. Ekran CR/LF karakterlerini kullanarak imleci aşağı satırın 
    başına geçirecektir. Yinelemek gerekirse Windows'ta biz imleci aşağı satırın başına geçirmek için CR/LF karakterlerini ekrana göndermemeliyiz. 
    Yalnızca LF karakterini ekrana göndermeliyiz. Zaten bugüne kadar da imleci ekranda aşağı satırın başına geçirebilmek için yalnızca LF karakterini 
    kullandık.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Genel olarak Windows'ta oluşturulmuş olan bir text dosyanın UNIX/Linux ve macOS sistemlerinde okunup ekrana yazdırılmasında bir sorun oluşmaz. 
    Çünkü CR/LF çifti okunurken UNIX/Linux ve macOS sistemlerinde iki ayrı karakter olarak okunacaktır. Bu sistemlerde CR karakterş "imleci bulunulan 
    satırınb başına geçirdiği" için hemen arkasındaki LF ise "aşağı satıırn başına geçirdiği" için bir sorun oluşmayacaktır. Tabii text dosya daha fazla
    yer kaplar durumda olacaktır. Bunun tersi olan durum eskiden DOS ve Windows sistemlerinde bozuk bir görüntünün oluşmasına yol açıyordu. Çünkü eskiden 
    DOS ve Windows sistemlerinde CR karakteri "bulunulan satırın başına geç" ancak LF karakteri "ehemen aşağıdaki satıra geç (sütunu muhafaza ederek)" 
    anlamına geliyordu. Ancak Windows daha sonraları artık LF karakteri için de "aşağı satırın abşına geç" semantiği uygulamaya başlamıştır. Fakat bu sistemlerde
    geçmişe doğru uyumu korumak için yine aşağı satırın başına geçmek amacıyla CR/LF karakterleri kullanılmalıdır. Bu dönüşümğ yapan çeşitli utility 
    programlar bulunmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    fprintf fonksiyonu printf fonksiyonunun dosyaya yazan biçimidir. Tabii aslında ekran da bir dosya olarak ele alınmaktadır. Bu durumda ters ifade de 
    geçerlidir. Yani printf fonksiyonu aslında fprintf fonksiyonunun stdout dosyasına (ekrana) yazan biçmidir. fprintf fonksiyonun prototipi ile
    printf fonksiyonunun prototipini aşağıda veriyoruz:

    int fprintf(FILE *f, const char *format, ...);
    int printf(const char *format, ...);

    Görüldüğü gibi fonksiyonlar arasındaki tek fark fprintf fonksiyonunun bir fazla parametreye sahip olmasıdır. fprintf fonksiyonu tamamen printf 
    fonksiyonu gibi çalışmaktadır. Tek fark bu fonksiyonun ekran yerine dosyaya yazmasıdır. Tabii fprintf fonksiyonu ile dosyaya bir şeyler yazdıracaksak 
    dosyanın text modda açılmış olması uygundur. Her iki fonksiyon da yazıdırılan karakter sayısı ile geri dönmektedir. Fonksiyonların prototiplerinin 
    sonunda bulunan "... (ellipsis)" fonksiyonların çağrılırken istenildiği kadar çok argüman alabileceği anlamına gelir. 

    fprintf ve printf fonksiyonlarının geri dönüş değerleri genellikle programcı tarafından kullanılmaz. Ancak bazen faydalı birtakım işlemler için
    bu geri dönüş değerlerinden faydalanılmaktadır.

    fprintf fonksiyonu ile dosyaya yazdırılan şeylerin yazı gibi yazdırıldığına dikkat ediniz. 

    Aşağıdaki örnekte bir dosya text modda açılmış sonra da fprintf fonksiyonu ile oradan okuma yapılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;

    if ((f = fopen("test.txt", "w")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < 100; ++i)
        fprintf(f, "Number: %d\n", i);

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Daha önce ekranın bir dosya gibi davrandığını belirtmiştik. İşte ekran dosyasına ilişkin dosya bilgi göstericisi "stdout" ismiyle kullanılabilmektedir. 
    Örneğin:

    fprintf(stdout, "this is a tes\n");

    Burada ekrana (stdout dosyasın) yazı yazdıırılmıştır. O halde:

    printf(......);
    
    çağrısı ile aşağıdaki çağrı eşdeğerdir:

    fprintf(stdout, .....);

    Zaten standartlarda da printf ve fprintf fonksiyonları ayrı ayrı anlatılmamıştır. fprintf fonksiyonu açıklanmıştır. printf fonksiyonunun fprintf 
    fonksiyonunun stdout dosyasına yazan biçimi olduğu söylenmiştir. 

    Yani aslında printf gizli bir dosya fonksiyonudur. printf fonksiyonunun genel hali fprintf fonksiyonudur.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

int main(void)
{
    printf("this is a test\n");
    fprintf(stdout, "this is a test\n");

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    printf ile fprintf arasındaki scanf ile fscanf arasındaki ilişki ile benzerdir. scanf fonksiyonu stdin dosyasından (klavyeden) okuma yapmaktadır.
    fscanf ise herhangi bir dosyadan okuma yapmaktadır. İki fonksiyonun da prototiplerini aşağıda veriyoruz:
    
    int scanf(const char *format, ...);
    int fscanf(FILE *f, const char *format, ...);

    Bu fonksiyonlar başarı durumunda okunabilen parça sayısına geri dönmektedir. Eğer hiç okuma yapılamadan fonksiyonlar EOF ile karşılaşırsa EOF değerine
    geri dönmektedir. 

    Aşağıdaki örnekte "test.txt" dosyasının içeriği şöyledir:

    10 20

    fscanf ile bu dosyadan okuma yapılırken sanki bu içerik klavyeden girilmiş gibi bir etki söz konusu olacaktır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;
    int a, b;

    if ((f = fopen("test.txt", "r")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    fscanf(f, "%d%d", &a, &b);

    printf("a = %d, b = %d\n", a, b);

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    scanf ve fscanf fonksiyonları kabaca şöyle çalışmaktadır: Bu fonksiyonlar karakterleri tek okuyarak ilerler. Format karaktyeri ile uyuşmayan 
    bir karakterle karşılaşırlarsa işlemini sonlandırılar ve başarılı bir biçimde yerleştirilen nesne sayısına geri dönerler. Aşağıdaki gibi 
    bir program olsun:

    ,#include <stdio.h>

    int main(void)
    {
        int a = -1, b = -1;
        int result;

        result = scanf("%d%d", &a, &b);

        printf("a = %d, b = %d\n", a, b);
        printf("result = %d\n", result);

        return 0;
    }

    Biz klavyeden şu girişi yapmış olalım:

    10     20

    scanf girişler arasındaki boşluk karakterlerini dikkate almamaktadır. Dolayısıyla burada başarılı bir biçimde iki yerleşimi yaptığı için scanf 
    2 değeri ile geri dönecektir. Girişimiz şöyle olsun:

    10 ali

    Bu durumda scanf a için okumayı yapar. Ancak b için okumayı yapamaz ve işleminşi sonlandırır. 1 değeri ile geri döner. Şimdi girişin şöyle 
    yapıldığını düşünelim:

    ali veli

    scanf a için okumayı yapamayacaktır. Bu durumda işlemini sonlandırı ve 0 ile geri döner. Şimdi girişi şöyle yapmış olalım:

    10ankara izmir

    Burada scanf 10 değerini a'ya yerleştirir ve " değeri ile geri döner.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir dosya açıldığında dosya göstericisi 0'ıncı offset'tedir. Daha sonra okume ve yazma yapıldıkça dosya göstericisi otomatik olarak ilerletilmektedir. 
    Ancak biz dosyanın belirli bir yerinden bir şeyi okumak isteyebiliriz ya da belirli bir yerine bir şeyler yazmak isteyebiliriz. Bunun için dosya 
    göstericisini konumlnadırmamız gerekir. Bu işlem fseek isimli fonksiyonla yapılmaktadır. 

    fseek fonksiyonunun prototipi şöyledir:

    int fseek(FILE *f, long int offset, int whence);

    Fonksiyonun birinci parametresi dosya göstericisi konumlandırılacak dosyanın bilgi göstericisini almaktadır. İkinci parametre konumlandırma offset'ini
    belirtmektedir. Fonksiyonun ikinci parametresi konumlandırma offset'ini belirtmektedir. Üçücüncü parametre ise konumlandırmanın nerereye göre 
    yapılacağını başka bir deyişle konumlandırma orijinini belirtmektedir. Bu üçüncü parametre yalnızca 0, 1 ya da 2 değerini alabilir. 
    Diğer değerler geçirsizdir. 0 değeri konumlandırmanın dosyanın başından itibaren yapılacağı anlamına gelmektedir. Bu durumda ikinci parametrenin >= 0 
    olması gerekir. 1 değeri konumlandırmanın o anda dosya göstericisinin gösterdiği yerden itibaren yapılacağı anlamına gelmektedir. Bu duurmda ikinci
    parametre pozitif, negatif ya da 0 olabilir. Pozitif değer "bulunulan yerden n ileriye", negatif değer "bulunulan yerden n geriye" konumlandırma anlamına gelir. 
    Örneğin:

    fseek(f, -1, 1);

    Burada dosya göstericisi her neredeyse bir geriye konumlandırılmıştır. Örneğin:

    fseek(f, 10, 0);

    Burada dosya göstericisi 10'uncu offset'e konumlandırılmıştır. Üçüncü parametrenin 2 olması konumlandırmanın EOF pozisyonundan itibaren yapılacağı
    anlamına gelmektedir. Bu durumda ikinci parametrenin <= 0 olması gerekir. Örneğin:

    fseek(f, 0, 2);

    Burada konumlandırma EOF pozisyonuna yapılmaktadır. Örneğin:

    fseek(f, -1, 2);

    Burada konumlandırma dosyanın son byte'ına yapılmaktadır. Üçücnü parametre okunabilirliği artırmak için <stdio.h> içerisinde aşağıdaki 
    üç sembolik sabitle de define edilmiştir:

    #define SEEK_SET		0
    #define SEEK_CUR		1
    #define SEEK_END		2

    Örneğin:

    fseek(f, 10, SEEK_SET);
    fseek(f, 0, SEEK_END);

    fseek fonksiyonuna olmayan bir offset'i parametre oalrak geçrirsek ya da üçüncü parametreyi uygunsuz bir biçimde girersek fseek başarısız olabilir. 
    fseek başarı durumunda 0 değerine, başarısızlık durumunda sıfır dışı herhangi bir değere geri dönmektedir. Ancak programcılar genel olarak 
    fonksiyonun başarısını kontrol etmezler.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                            76. Ders - 31/03/2023 - Cuma
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte dosya göstericisi EOF durumuna çekilerek fprintf fonksiyonuyla dosyanın sonuna bir şeyler yazılmıştır.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;

    if ((f = fopen("test.txt", "r+")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    fseek(f, 0, SEEK_END);

    fprintf(f, "this is a test...\n");


    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C standartlarına göre dosya okuma ve yazma modunda açılmışsa okuma işleminden yazma işlemine, yazma işleminden de okuma işlemine geçilirken
    dosya göstericisinin konumlandırılması gerekmektedir. Örneğin dosyadaki byte'lar şunlar olsun:

    abc

    Dosya göstericisi de a byte'ını gösteriyor olsun. Aşağıdaki gibi bir işlem tanımsız davranışa (undefined behavior) yol açacaktır:

    ch = fegtc(f);
    fputc('x', f);

    Burada 'a' byte'ı okunur. Dosya göstericisi ilerletilir ve 'b' byte'ını gösteriyor duruma gelir. Ancak bu pozisyona yazma yapıldığında 
    tanımsız davranış oluşacaktır. Bu tür durumlarda mutlaka fseek fonksiyonuyla konumlandırma yapılması gerekir. Konumlandırmaya gereksinim olmasa bile
    fseek çağrısının yapılması gerekir. Örneğin:

    ch = fegtc(f);
    fseek(f, 0, 1);
    fputc('x', f);

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Dosya göstericisini konumlandırmak için rewind isminde de bir fonksiyon bulunmaktadır. Bu fonksiyon her zaman dosya göstericisini dosyanın başına (yani 0'ıncı 
    offset'e çeker). Prototoipi şöyledir:

    void rewind(FILE *f);

    Yani:

    rewind(f);

    çağrısı ile,

    fseek(f, 0, 0);

    çağrısı tamamen eşdeğerdir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bazen dosya göstericisinin o anki konumunu almak isteyebiliriz. Böylece daha sonra o offset'e fseek ile konumlandırma yapıp oradan bir şeyler okuyabilir
    ya da oraya bir şeyler yazabiliriz. Bunun için ftell fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:

    long ftell(FILE *f);

    Fonksiyonun offset belirten geri dönüş değerinin long olduğuna dikkat ediniz. Bu long değer her zaman dosyanın başından itibaren bir değer belirtmektedir. 

    Aşağıdaki örnekte dosya göçstericisi önce EOF pozisyonuna çekilmiş sonra da dosya göstericisinin değeri elde edilmiştir. Dolayısıyla aslında dosyanın 
    byte uzunluğu elde edilmiş olmaktadır. Tabii dosya uzunluğunu bu biçimde elde etmek aslında iyi bir teknik değildir. Ancak C'de standart fonksiyonlar kullanılarak 
    dosya uzunluğunu elde etmenin başka bir yolu da yoktur. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;
    long size;
    
    if ((f = fopen("test.txt", "r")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    fseek(f, 0, SEEK_END);
    size = ftell(f);
    printf("%ld\n", size);

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    fgets isimli standart C fonksiyonu text bir dosyadan bir satır okumak için kullanılmaktadır. Fonksiyonun prototipi şöyledir:

     char *fgets(char *s, int n, FILE *f);

     Fonksiyonun birinci parametresi satırdaki bilgilerin yerleştirileceği dizinin adresini belirtir. İkinci parametre birinci parametrede belirtilen dizinin 
     uzunluğunu belirtmektedir. Fonksiyon hiçbir zaman burada belirtilen sayıdan daha fazla byte'ı diziye yerleştirmeye çalışmaz. Son parametre ise 
     üzerinde işlem yapılacak dosyaya ilişkin dosya bilgi göstericisini belirtmektedir. fgets fonksiyonu şöyle çalışmaktadır:

     - Fonksiyon dosya göstericisinin gösterdiği yerden itibaren '\n' karakterini görene kadar ('\n' karakteri de dahil olmak üzere) ya da en fazla n - 1 
     kadar karakteri birinci parametresiyle belirtilen adresten itibaren diziye yerleştirir. Yazının sonuna da null karakteri ekler. Yani eğer 
     n değeri dosya göstericisinin gösterdiği yerden itibaren satır sonuna kadarki ('\n' karakteri de dahil) karakter sayısından büyükse bu durumda fonksiyon
     '\n' karakterini de diziye yerleştirir. '\n' karakterinden sonra null karakteri de diziye ekler ve işlemini sonlandırır. Eğer n değeri dosya göstericisinin 
     gösterdiği yerden itibaren satır sonuna kadarki ('\n karakteri dahil) karakter sayısına eşit ya da ondan daha küçük ise bu durumda fonksiyon n - 1 tane 
     karakteri diziye yerleştirir ve dizinin sonuna null karakteri de ekleyerek işlemini sonlnadırır. Örneğin satırda şunlar olsun:

     ankara\n

     biz de şu çağrıyı yapmış olalım:

     char buf[100];

     fgets(buf, 100, f);

     Burada buf dizisinin içerisinde şunlar olacaktır:

     ankara\n\0....

     Şimdi de şu çağrıyı yapmış olalım:

     char buf[5];

     fgets(buf, 5, f);

     bu durumda buf dizisinin içerisinde şunlar olacaktır:

     anka\0

     - Eğer fgets çağrıldığında '\n' karakteri daha görülmeden EOF ile karşılaşılmışsa bu durumda fgets okuyabildiği kadar karakteri okur yine dizinin 
     sonuna null karakteri yerleştirir ve işlemini sonlandırır. Örneğin:

     ankaraEOF

     Şimdi biz şu çağrıyı yapmış olalım:

     char buf[100];

     fgets(buf, 100, f);

     buf dizisine şunlar yerleştirilecektir:

     ankara\0

     fgets normal olarak birinci parametresiyle belirtilen adresin aynısıyla geri döner. Ancak fgets hiç okuma yapmadan EOF ile karşılaşırsa 
     NULL adresle geri dönmektedir. Bu durumda fgets diziye herhangi bir şey yerleştirmez. 

    Tabii Windows sistemlerinde alt satırın başına geçme işlemi CR/LF karakter çiftiyle yapılmaktadır. Dosya text modda açılmışsa (default durum)
    zaten bu durumda bu iki karakter okunur ancak diziye yalnızca LF ('\n') karakteri yerleştirilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir dosyayı satır satır ekrana (stdout dosyasına) yazdırmak istediğimizi düşünelim. Bunu nasıl yapabiliriz? fgets fonksiyonunun '\n' karakterini de 
    dizye yerleştirdiğine dikkat ediniz. Satırı yazdırırken ayrıcı '\n' karakterinin yazdırılmaması gerekir. Örneğin:

    while (fgets(buf, 4096, f) != NULL)
        printf("%s", buf);

    Burada printf fonksiyonunun formak karakyterlerinin sonunda '\n' karakterinin olmadığına dikkat ediniz. 

    Aşağıdaki örnekte dosya satır satır yazdırılmıştır. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;
    char buf[4096];
    
    if ((f = fopen("test.txt", "r")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    while (fgets(buf, 4096, f) != NULL)
        printf("%s", buf);

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aslında yukarıdaki programda biz dizi uzunluğunu küçk tutsak dosyayı satır satır okuyamayız ama dosyanın sonuna kadar her şeyi okuyup yazdırabiliriz. 
    n = 3 değeri ile programı yeniden aşağıdaki gibi çalıştırabilirizniz.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;
    char buf[3];
    
    if ((f = fopen("test.txt", "r")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    while (fgets(buf, 3, f) != NULL)
        printf("%s", buf);

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi biz bir satırın tamamını okuyabildiğimizi nasıl anlayabiliriz? Örneğin:

    fgets(buf, 1024, f);

    Burada satır 1023 karakterden daha büyük de olabilir. Bunu anlamanın tek yolu okunan bilgilerin sonunda '\n' karakterinin olup olmadığını kontrol etmektedir. 
    Örneğin:

    if (fgets(buf, 1024, f) != NULL) {
        if (buf[strlen(buf) - 1] != '\n'){			/* tam satır okunmamış */
            ....
        }
        ...
    }

    Pekiyi fgets ile en az kaç karakter okunabilir. fgets fonksiyonun ikinci parametresinin 1 olması anlamsızdır ama geçerlidir. Bu durumda null 
    karakter diziye yerleştirileceğine göre dizinin 1 elemanına null karakter yerleştirilir. Ancak dosyadan okuma yapılmayacağı için dosya göstericisi 
    de konum değiştirmez. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    gets fonksiyonunun 2011 yılında C standartlarından çıkartıldığını belirtmiştik. Bunun yerine standartlara "isteğe bağlı (optional)" bir gets_s fonksiyonu 
    eklendi. Ancak bu gets_s fonksiyonu UNIX/Linux'ta kullanılan gcc ve clang derleyicilerinin standart C kütüphanelerinde bulunmamaktadır. Bu durumda 
    C programcıları genellikle gets fonksiyonu yerine fgets fonksiyonunu kullanmaktadır. fgets fonksiyonun dosya bilgi göstericisi belirten üçüncü parametresi 
    "stdin" olarak geçilirse bu durumda stdin dosyasından okuma yapılır. Örneğin:

    char buf[1024];

    fgets(buf, 1024, stdin);

    Ancak bu kullanımın gets fonksiyonundan bir farkı vardır. gets fonksiyonu satırı sonlandırırken bastığımız ENTER tuşu yerine tampona yerleştirilen '\n'
    karakterini tampondan atar ancak diziyte yerleştirmez. Fakat fgets fonksiyonu bu '\n' karakterini de tıpkı dosyalarda olduğu gibi diziye yerleştirmektedir. 
    Örneğin:

    char buf[10];

    fgets(buf, 10, stdin);

    Burada biz klavyeden "ali" yazıp ENTER tuşuna basalım. fgtes diziye şunları yerleştirecektir:

    ali\n\0

    Halbuki burada gets olsaydı diziye şunlar yerleştirilirdi:

    ali\0

    O halde fgets aslında gets_s fonksiyonu gibi davranmamaktadır. İşte fgets fonksiyonunu gets gibi kullanmak için yazının sonundaki '\n' karakterini 
    bulup onun yerine null karakteri yerleştirmek gerekir. Örneğin:

    char buf[10];
    char *str;

    fgets(buf, 10, stdin);

    if ((str = strchr(buf, '\n')) != NULL)
        *str = '\0';

    Buradaki kontrol size anlamsız gelebilir. Çünkü siz her zaman yazının sonunda zaten '\n' olması gerekeceğini düşünülebilirsiniz. Halbuki satırdaki karakter sayısı 
    fazla ise fgets bu durumda '\n' karakterini yerleştirmeyecektir. Ayrıca burada henüz anlatmadığımız başka birtakım süreçler söz konusu olabilmektedir. 
    (Örneğin stdin dosyası başka bir dosyaya yönlendirilmiş olabilir ve bu dosyanın sonunda '\n' karakteri olmayabilir.) Aslında yine henüz 
    bilmediğimiz klavyedne EOF etkisi yaratma konusu nedeniyle fgets fonksiyonun da geri dönüş değerinin NULL adres olup olmadığının kontrol edilmesi gerekir. Örneğin:

    char buf[10];
    char *str;

    if (fgets(buf, 10, stdin) != NULL) {
        if ((str = strchr(buf, '\n')) != NULL)
            *str = '\0';
        ...
    }
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <string.h>

int main(void)
{
    char buf[10];
    char *str;

    if (fgets(buf, 10, stdin) != NULL) {
        if ((str = strchr(buf, '\n')) != NULL)
            *str = '\0';
        puts(buf);
    }
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    C'nin en önemli dosya fonksiyonlarından ikisi fread ve fwrite fonksiyonlarıdır. Bu fonksiyonlar genellikle "binary" dosyalarda bellek ile dosya 
    arasında transfer yapmak için kullanılmaktadır. Fonksiyonların prototipleri şöyledir:

    size_t fread(void *ptr, size_t size, size_t n, FILE *f);
    size_t fwrite(const void *ptr, size_t size, size_t n, FILE *f);

    Bu fonksiyonların birinci parametreleri bellek transfer adresini belirtir. Fonksiyonlar ikinci ve üçüncü parametrelerin çarpımı kadar byte transfer 
    etmektedir. Fonksiyonun son parametresi dosyaya ilişkin dosya bilgi göstericisini belirtir. Genelikle ikinci parametre dizinin bir elemanının byte uzunluğu 
    biçiminde üçüncü parametre ise dizinin uzunluğu biçiminde girilmektedir. İki değer çarpıldığında dizinin toplam byte sayısı elde edilir. 
    Fonksiyonlar üçüncü parametrede belirtilen okunan ya da yazılan parça sayısı ile geri dönmektedir. EOF durumunda fread fonksiyonu okuma yapamayacağı için 
    0 ile geri dönmektedir. Fonksiyonlar başarısız olduğunda da 0 ile geri dönmektedir. Bu durumda örneğin fread fonksiyonu 0 ile geri dönerse başarısızlığın 
    EOF'tan kaynaklanıp kaynaklanmadığını anlayabilmek için ferror ya da feof fonksiyonlarının kullanılması gerekir. 

    Aşağıdaki örnekte bir binary dosyaya 10 elemanlı int dizi tek hamlede fwrite fonksiyonuyla yazılmış ve sonra da yine tek hamlede fread fonksiyonuyla
    okunmuştur.
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(void)
{
    FILE *f;
    int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
    int b[10];

    if ((f = fopen("test.dat", "w+b")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    if (fwrite(a, sizeof(int), 10, f) != 10) {
        fprintf(stderr, "cannot write file!..\n");
        exit(EXIT_FAILURE);
    }

    rewind(f);

    if (fread(b, sizeof(int), 10, f) != 10) {
        fprintf(stderr, "cannot read file!..\n");
        exit(EXIT_FAILURE);
    }

    for (int i = 0; i < 10; ++i)
        printf("%d ", b[i]);
    printf("\n");

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    fread fonksiyonu dosya göstericisinin gösterdiği yerdne itibaren eğer ikinci ve üçüncü parametresiyle belirtilen miktarda byte dosyada yoksa 
    okuyabildiği kadar byte'ı okur okuyabildiği parçe sayısına geri döner. Yani biz fread ile talep ettiğimizden daha az byte okuyabiliriz. Bu durum 
    bir hata kabul edilmemektedir. Örneğin dosyada 20 byte olsun. Biz de aşağıdaki gibi bir okuma yapmak isteyelim:

    unsigned char buf[100];
    size_t result;

    result = fread(buf, 1, 100, f);

    Biz burada her bir parçası 1 byte olan 100 parçe okumak istedik. Halbuki dosyada 20 byte vardır. O zaman biz her bir parçası 1 byte olan 20 parça okuyabiliriz. 
    Bu durumda fread 20 değeri ile geri dönecektir. 

    fread ve fwrite fonksiyonlarının iki paremetresinin çarpımı kadar byte okumaya çalıştığına ancak üçüncü parametresinde belirtilen parça sayısına geri 
    döndüğüne dikkat ediniz. Bu tasarım biraz tuhaf ve kullanışsız gibidir. Ancak fonksiyonlar bu biçimde tasarlanmıştır. 

    fread fonksiyonu parçalı okuma yapabilir. Bu durumda okumanın yapıldığı dizinin son elemanı "kararsız (interminate)" bir durumda kalır. Örneğin dosyada
    10 byte olsun. int türünün uzunluğu da 4 byte olsun. Şöyle bir okuma yapmış olalım:

    int a[10];
    size_t result;

    result = fread(a, sizeof(int), 10, f);

    Burada fread fonksiyonu 10 byte'ın hepsini okur. Ancak int türü 4 byte olduğu için a dizisinin 2'incisli elemanı parçalı bir okumaya maruz kalmıştır 
    ve "kararsız (indeterminate)" bir durumdadır. fread fonksiyonu başarılı olarak okunan parça sayısına geri dönmektedir. Yani bu örnekte fread fonksiyonu 2 
    ile geri dönecektir. fread bu örnekte 10 byte okuduğu halde başarılı okuduğu parça sayısı 2 olduğu için 2 ile geri dönecektir. Maalesef bu tasarım 
    biraz gereksiz bir karmaşıklık oluşturmaktadır. Programcıların çoğu fonksiyonun ikinci parametresini 1 değerinde tutup açıkça byte okuması yapar. Örneğin:

    int a[10];
    size_t result;

    result = fread(a, sizeof(int), 10, f);

    Bu işlemin benzeri şöyledir:

    int a[10];
    size_t result;

    result = fread(a, 1, sizeof(int) * 10, f);

    Dosyada yeteri kadar byte olduğunu düşünelim. Birinci durumda fread 10 ile geri döner. Ancak ikinci durumda 40 ile geri dönecektir. 

    fread fonksiyonu hiçbir okuma yapamadan EOF ile karşılaşırsa ya da IO hatası oluşursa 0 ile geri döner. Bu durumun kontrol edilmesi gerekebilir. 

    Örneğin:

    result = fread(a, 1, sizeof(int) * 10, f);
    if (result == 0) {
        if (ferror(f)) {
            fprintf(stderr, "IO error!..\n);
            exit(EXIT_FAILURE);
        }
        /* end of file encountered */
    }
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    fread ve fwrite fonksiyonlarını kullanarak dosya kopyalaması yapabiliriz. Bunun için kaynak dosyadan belli bir büyülükte byte bir döngü içerisinde okunur.
    Hedef dosyaya yazılır. Döngünün yapısı şöyle olacaktır:

    char buf[BUFFER_SIZE];
    FILE *fs, *fd;
    ...

    while ((result = fread(buf, 1, BUFFER_SIZE, fs)) > 0)
        if (fwrite(buf, 1, result, fd) != result) {
            fprintf(stderr, "cannot write file!...\n");
            exit(EXIT_FAILURE);
        }

    if (ferror(fs)) {
        fprintf(stderr, "cannot read file!..\n");
        exit(EXIT_FAILURE);
    }

    Burada BUFFER_SIZE kopyalama sırasında kullanılacak çanak büyüklüğünü belirtmektedir. Bu döngüden iki nedenle çıkılabilir. Birincisi hiç byte okuyamadan 
    EOF ile karşılaşılması ikincisi ise IO hatasının olmasıdır. Bu nedenle döngü çıkışında fread fonksiyonunun neden 0 ile geri döndüğü sorgulanmıştır. 
    Burada BUFFER_SIZE değerinin 1024 olduğunu düşünelim. Kaynak dosya da 1020 byte uzunlupunda olsun. Döngüde fread önce 1024 byte'ı okur ve 1024 ile geri döner. 
    Sonra 1024 byte okumak ister ancak 6 byte okur ve 6 değeri ile geri döner. Sonra da EOF'tan dolayı okuma yapamaz ve 0 ile geri döner. Klasik dosya kopyalama
    algoritması bu biçimdedir. Aşağıda kodun tamamı verilmiştir. 

----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_PATH        1024
#define BUFFER_SIZE     1024

int main(void)
{
    FILE *fs, *fd;
    char spath[1024], dpath[1024], *str;
    size_t result;
    char buf[BUFFER_SIZE];

    printf("Source file path: ");
    fgets(spath, 1024, stdin);
    if ((str = strchr(spath, '\n')) != NULL)
        *str = '\0';
    else {
        fprintf(stderr, "path too long...\n");
        exit(EXIT_FAILURE);
    }

    printf("Target file path: ");
    fgets(dpath, 1024, stdin);
    if ((str = strchr(dpath, '\n')) != NULL)
        *str = '\0';
    else {
        fprintf(stderr, "path too long...\n");
        exit(EXIT_FAILURE);
    }

    if ((fs = fopen(spath, "rb")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    if ((fd = fopen(dpath, "wb")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    while ((result = fread(buf, 1, BUFFER_SIZE, fs)) > 0)
        if (fwrite(buf, 1, result, fd) != result) {
            fprintf(stderr, "cannot write file!...\n");
            exit(EXIT_FAILURE);
        }

    if (ferror(fs)) {
        fprintf(stderr, "cannot read file!..\n");
        exit(EXIT_FAILURE);
    }

    printf("file successfully copied...\n");

    fclose(fs);
    fclose(fd);
    
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
                                        77. (Son) Ders - 14/04/2023 - Cuma
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yapıların elemanları bellekte ardışıl bir biçimde bulunduğuna göre bir yapı nesnesi tek hamlede fwrite fonksiyonu ile dosyaya yazılabilir ve fread
    fonksiyonu ile dosyadan okunabilir. Örneğin:

    struct PERSON {
        char name[64];
        int no;
    };
    ...
    struct PERSON per = {"Kaan Aslan", 123};

    if (fwrite(&per, sizeof(struct PERSON), 1, f) != 1) {
        fprintf(stderr, "cannot write file!..\n");
        exit(EXIT_FAILURE);
    }

    Aşağıda örnekte bir döngü içerisinde kişilerin bilgileri alınıp dosyaya fwrite fonksiyonu ile yazdırılmıştır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct PERSON {
    char name[64];
    int no;
};

int main(void)
{
    FILE *f;
    struct PERSON per;
    char *str;
    
    if ((f = fopen("test.dat", "wb")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    for (;;) {
        printf("Adi soyadi:");
        fgets(per.name, 64, stdin);
        if ((str = strchr(per.name, '\n')) != NULL)
            *str = '\0';
        if (!strcmp(per.name, "quit"))
            break;
        printf("No:");
        scanf("%d", &per.no);
        while (getchar() != '\n')
            ;
        if (fwrite(&per, sizeof(struct PERSON), 1, f) != 1) {
            fprintf(stderr, "cannot write file!..\n");
            exit(EXIT_FAILURE);
        }
    }

    fclose(f);

    return 0;
}
        
/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki daha önceki örnekte dosyaya yazmış olduğumuz bilgileri döngü içerisinde fread fonksiyonu ile okuyoruz. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

struct PERSON {
    char name[64];
    int no;
};

int main(void)
{
    FILE *f;
    struct PERSON per;
 
    if ((f = fopen("test.dat", "rb")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    while (fread(&per, sizeof(struct PERSON), 1, f) == 1) 
        printf("%s, %d\n", per.name, per.no);
  
    if (ferror(f)) {
        fprintf(stderr, "cannot read file!..\n");
        exit(EXIT_FAILURE);
    }

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte belli bir kayıt isme göre "sıralı (sequential)" biçimde dosyada aranmıştır. Kayıt sayısı çok fazlaysa sıralama arama iyi bir 
    yöntem değildir. Bunun için özel algoritmik arama yöntemleri kullanılmaktaıdır. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

struct PERSON {
    char name[64];
    int no;
};

int main(void)
{
    FILE *f;
    struct PERSON per;
    char *str;
    char name[64];

    printf("Aranacak kisinin adi soyadini giriniz:");
    fgets(name, 64, stdin);
    if ((str = strchr(name, '\n')) != NULL)
        *str = '\0';

    if ((f = fopen("test.dat", "rb")) == NULL) {
        fprintf(stderr, "cannot open file!..\n");
        exit(EXIT_FAILURE);
    }

    while (fread(&per, sizeof(struct PERSON), 1, f) == 1) 
        if (!strcmp(per.name, name)) {
            printf("record found : %s, %d\n", per.name, per.no);
            break;
        }
       
    if (feof(f))
        printf("record not found!..\n");

    if (ferror(f)) {
        fprintf(stderr, "cannot read file!..\n");
        exit(EXIT_FAILURE);
    }

    fclose(f);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    İşletim sistemlerinde ekran ve klavye birer "aygıt sürücü (device driver)" tarafından yönetilmektedir. Aygıt sürücüler ise dosyalar gibi işleme sokulmaktadır.
    Ekran ve klavyeyi kontrol eden aygıt sürücülere "terminal aygıt sürücüleri" denilmektedir. C'de "stdout" değişkeni FILE * türünden bir dosya bilgi 
    göstericisi belirtir. Benzer biçimde "stdin" isimli değişken de yine FILE * türünden bir dosya bilgi göstericisi belirtmektedir. Her iki değişken de 
    <stdio.h> içerisinde bildirilmiştir. 

    C standartlarında "ekran" ve "klavye" lafı edilmemektedir. Çünkü bir bilgisayar sisteminde ekran ve klavye olmak zorunda değildir. C standartlarında 
    ekran ve klavye yerine "stdout dosyası" ve "stdin dosyası" lafları edilmektedir. Örneğin standratlara göre printf fonksiyonu "ekrana değil stdout dosyasına"
    yazma yapmaktadır. Benzer biçimde scanf fonksiyonu da klavyeden değil "stdin dosyasından" okuma yapmaktadır. C standratlarında stdout aslında çıktının 
    bir biçimde gönderileceği bir aygırı temsil etmektedir. stdout ilgili sistemde ekran aygıt sürücüsü olabileceği gibi, printer'a ilişkin ya da başka bir 
    aygıta ilişkin aygıt sürücüsü olabilir. Tabii stdout bir dosya olarak normak bir disk dosyasını da temsil edebilir. Benzer biçimde yanı durum stdin dosyası 
    için de geçerlidir. Tabii klasik bilgisayar sistemlerinde default durumda stdout ekranı, stdin klavyeyi temsil etmektedir. 

    printf fonksiyonu aslında fprintf fonksiyonunun stdout dosyasına yazan biçimidir. Yani aşağıdaki iki çağrı eşdeğerdir:

    printf(...);
    fprintf(stdout, ...);

    Benzer biçimde scanf fonksiyonu da aslında fscanf fonksiyonunun stdin dosyasından okuma yapan biçimdir. Aşağıdaki iki çağrı eşdeğerdir:

    scanf(...);
    fscanf(stdin, ...);

    stdin, stdout ve stderr dosyaları program çalışmaya başladığında otomatik olarak açılmış kabul edilmektedir. Bunlar programcı tarafından açılmazlar 
    ve dolayısıyla programcı tarafından kapatılmazlar. Programcı tarafından bu dosyalar doğrudan kullanılabilirler. stdin dosyasının "read only text modda"
    açıldığı, stdout ve stderr dosyalarının ise "write only text modda açıldığı" kabul edilmektedir. Yani biz stdin dosyasına yazma yapamayız. stdout ve stderr 
    dosyalarından da okuma yapamayız. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    stdin, stdout ve stderr dosyaları başka aygırlara ve diskteki başka dosyalara yönlendirilebilmektedir. Bu konuya "IO yönlendirmesi (IO redireciton)"
    denilmektedir. IO yönlendirmesinin işletim sistemine özgü ayrıntıları vardır. Windows, UNIX/Linux ve macOS sistemlerinde IO yönlendirmesi 
    > ve < sembolleriyle yapılmaktadır. > sembolü stdout dosyasının yönlendirilmesi için < sembolü ise stdin dosyasının yönlendirimesi için kullanılmaktadır. 
    Örneğin:

    #include <stdio.h>

    int main(void)
    {
        for (int i = 0; i < 10; ++i)
            printf("%d\n", i);

        return 0;
    }

    Bu program 1'den 10'a kadar sayıları stdout dosyasına yazdırmaktadır. Bu program çalıştırıldığında stdout dosyası default oalrak terminal aygıt 
    sürücüsünü belirttiği için yazılanlar ekrana çıkacaktır. Ancak biz komut satırında > sembolü ile stdout dosyasını bir disk dosyasına yönledirebiliriz. 
    Örneğin:

    ./sample > test.txt

    Artık ekrana yazılacak şeyle "test.txt" dosyasının ieçrisine yazılacaktır. IO yönlendirmesi IDE'lerde de hiç komut satırına geçmeden yapılabilmektedir. 
    Örneğin Visual Studio IDE'sinde proje seçeneklerinden "Debugging/Command Line Arguments" kısmımında > sembolü ile yönlendirme yapılabilemktedir.

    Benzer biçimde klavyeden okunan değerler bir dosyadan okunuyormuş etkisi de yaratılabilir. Bunun için stdin dosyasını < sembolü ile yönlendirmek gerekir. 
    Örneğin:

    ./sample < x.txt

    Burada stdin dosyasından okunacaklar x.txt dosyasından okunacaktır. Pekiyi EOF etkisi klavyede nasıl sağlanmaktadır? İşte UNIX/Linux sistemlerinde 
    klavyeden Ctrl+d tuşlarına basıldığında sanki stdin dosyasında EOF ile karşılaşılmış etkisi yaratılmaktadır. Windows sistemlerinde ise bu etki Ctrl+z 
    tuşuyla yapılmaktadır. Tabii biz bu özel tuşlara bastığımızda klavyeye ilişkin dosyasını kapatmış olmayız. O defalık EOF etkisi yaratılmış olur. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        double val;

        for (;;) {
            if (scanf("%lf", &val) == EOF)
                break;
            
            printf("%f\n", val * val);
        }

            return 0;
        }
    }

    Burada scanf EOF ile karşılaştığında EOF değeri ile geri dönmektedir. Biz de döngüden çıkmaktayız. İşte bu programı çalıştırırken 
    EOF etkisinin yaratılması için Ctrl+d ya da Ctrl+z tuşları kullanılmaktadır. Biz bu proramı aşağıdaki gibi çalıştırırsak prtogram klavyeden okuma 
    yapmak yerine "x.txt" dosyasından okuma yapacaktır:

    ./sample < x.txt

    Tabii stdin dosyası ile stdout dosyası birlikte de yönlendirilebilir. Örneğin:

    ./sample < x.txt > y.txt

    (Windows 11'deki Visual Studio'nun son versiyonlarında scanf fonksiyonunda EOF etkisi yaratma konusunda muhtemel bir bug bulunmaktadır.)
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Pekiyi stderr dosyası nedir? stderr dosyası hata mesajlarının yazdırılması için düşünülmüş bir dosyadır. İşletim sistemlerinde default durumda stderr
    dosyası da stdout ile aynı biçimde terminal sürücüsüne (yani ekrana) yönlendirilmiş durumdadır. Yani default durumda stderr dosyasına yazılanlar yine
    ekrana çıkacaktır. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        printf("stdout\n");
        fprintf(stderr, "stderr\n");

        return 0;
    }

    Her iki yazı da ekrana çıkacaktır. Pekiyi o zaman stderr dosyasına ne gerek vardır? 
    
    Biz hata mesajlarını print ile stdout dosyasına değil, fprintf fonksiyonu ile stderr dosyasına yazdırmalıyız. Zaten bugüne kdara da böyle yapmıştık. 
    Örneğin:

    char *str;

    if ((s = (char *)malloc(1024)) == NULL) {
        fprintf(stderr, "cannot allocate memory!..\n");
        exit(EXIT_FAILURE);
    }

    Her ne kadar default durumda stdout da stderr de yazıların ekrana çıkmasına yol açıyorsa da IO yönlendirmesi yoluyla bu iki dosyanın hedefleri değiştirilebilir. 

    Yukarıdaki programı yeniden yazmış olalım:

    #include <stdio.h>

    int main(void)
    {
        printf("stdout\n");
        fprintf(stderr, "stderr\n");

        return 0;
    }

    Şimdi programı şöyle çalıştırmış olalım:

    ./sample > x.txt

    Burada ekranda yalnızca "stderr" yazısını göreceğiz. Çünkü > semboli yalnızca stdout dosyasını yönlnedirmektedir. Eğer stderr dosyası yönlendirilmek 
    istenirse bu durumda 2> sembolünün kullanılması gerekir. Örneğin:

    ./sample 2> x.txt

    Burada da ekran yalnızca "stdout" yazısını göreceğiz. Çünkü biz bu örnekte yalnızca stderr dosyasını yönlendirmiş olduk. İşte programcıo eğer 
    hata mesajlarını stderr dosyasına yazarsa kullanıcı programın normal mesajlarıyla hata mesajlarını birbirinden ayırabilir. Örneğin:

    find / -name "sample.c"

    Bu komut kökten itibaren her dizinde "sample.c" dosyasını aramaktadır. Ancak yekisiz dizinlerle karşılaştığında stderr dosyasına hata mesajları yazmaktadır. 
    İşin başında stdout ile stderr ekrana yönlendirildiği için programın normal mesajları ile hata mesajları birlikte ekranda görünecektir. Karışık bir 
    görüntü oluşacaktır. Halbuki biz bu programı çalıştırırken stderr dosyasını başka bir dosyay yönlendirirsek kafamızın karışmasını engellemiş oluruz. 
    Örneğin:

    find / -name "sample.c" 2> err.txt

    Artık programın hata mesajları ekrana değil "err.txt" dosyasına yazdırılacaktır. İşte kullanıcıya bu ayırma olanağını vermek için hata mesajlarının 
    stderr dosyasına yazdırılması gerekmektedir. Linux sistemlerinde /dev/null isimli özel bir dosya vardır. Bu dosyaya yazılan her şey atılmaktadır. 
    Dolayısıyla yönlendirme bu dosyaya yapılırsa hata mesajlarından tamamen kurtulabiliriz. Örneğin:

    find / -name "sample.c" 2> /dev/null

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir program komut satırından çalıştırılırken argümanlar girilebilmektedir. Bu argümanlara programın "komut satırı argümanları (command line arguments)"
    denilmektedir. Örneğin:

    ./sample ali veli selami

    Buradaki "ali veli selami" çalıştırdığımız sample programının komut satırı argümanlarıdır. Komut satırı argümanları olmasaydı pek çok programı istediğimiz
    işleri yapacak biçimde çalıştıramzdık. Örneğin:

    find / -name "sample.c"

    Burada find programı komut satırı argümanlarını alıp ne yaapacağına karar vermektedir. Örneğin:

    ls -l 

    Burada ls programı görüntülemenin nasıl yapılacağına "-l" argümanı ile karar vermektedir. 

    Komut satırı argümanları kabul programlar tarafından alınır ve C programına iletilir. C programcısı da bunları alarak kullanır. Standartlara göre 
    main fonksiyonu iki parametrik yapıya sahip olabilmektedir:

    int main(void)
    {
        ...
    }

    int main(int argc, char *argv[])
    {
        ...
    }

    Ancak standartlar o derleyiciye ve işletim sistemine özgü biçimde main fonksiyonun başka parametrelerinin de olabileceğini belirtmiştir. Fakat taşınabilirlik 
    bakımından bu iki parametrik yapı kullanılmaktadır. main fonksiyonun aşağıdaki biçimi komut satırı argümanlarını almak için kullanılmaktadır:

    int main(int argc, char *argv[])
    {
        ...
    }

    Burada parametre değişkenlerinin isimleri argc ve argv olmak zoruda değildir. Ancak geleneksel olarak programcılar bu isimleri kullanmaktadır. 
    main fonksiyonun ikinci parametresi göstericiyi gösteren göstericidir. Yani yukarıdaki yazım ile aşağıdaki yazım tamamen eşdeğerdir:

    int main(int argc, char **argv)
    {
        ...
    }

    Ancak geleneksel olarak programcılar her nedense önceki yazımı tercih etmektedir. Pekiyi bu argc ve argv ne anlama gelmektedir? Programın aşağıdaki gibi 
    çaşıltırıldığını varsayalım:

    ./sample ali veli selami

    Burada argc parametresine program ismi dahil olmak üzere boşluklarla ayrılmış yazıların sayısı geçirilmektedir. Yani örneğimizde argc 4 değerini alacaktır. 
    argv ise bir göstericiyi gösteren göstericidir. Yani bir gösterici dizisini göstermektedir. İşte bu gösterici dizisi program ismi dahil olmak üzere
    komut satırı argümanlarını gösterir. Her komut satırı argümanın sonunda yine null karakter bulunmaktadır. Standartlar argv dizisinin sonundaki son elemanın 
    NULL adres içereceğini garanti etmektedir. Yukarıdaki çalıştırmada argv dizisinin ieçriği şöyle olacaktır:

    argv ---> [0] ---> ./sample\0
              [1] ---> ali\0
              [2] ---> veli\0
              [3] ---> selami\0
              [4] NULL

    Örneğin "sample.c" programı şöyle olsun:

    #include <stdio.h>

    int main(int argc, char *argv[])
    {
        printf("%d\n", argc);

        for (int i = 0; i < argc; ++i)
            puts(argv[i]);
    
        return 0;
    }

    Biz de UNIX/Linux sistemlerinde programı çeşitli biçimlerde çalıştıralım:

    kaan@kaan-virtual-machine:~/Study/C$ ./sample ali veli selami
    4
    ./sample
    ali
    veli
    selami
    kaan@kaan-virtual-machine:~/Study/C$ ./sample 10 20
    3
    ./sample
    10
    20
    kaan@kaan-virtual-machine:~/Study/C$ ./sample
    1
    ./sample
    kaan@kaan-virtual-machine:~/Study/C$ ./sample ankara izmir
    3
    ./sample
    ankara
    izmir
    
    Mademki programın ismi de komut satırı argümanlarında dahildir. O halde argc hiçbir zaman 0 olamaz. En azından 1 olmak zorundadır. 

    argc ve argv parametrelerinden komut satırı argümanlarını alıp kullanmak programcının görevidir. İşletim sistemi yalnızca bu argümanları main 
    fonksiyonuna geçirmektedir. Başka bir şeye karışmamaktadır. Tabi programcı main fonksiyonunun parametresini void yaparsa bu argümanları elde edemez. 
    Yani programcı main fonksiyonun paraöetresini void yaparsa komut satırından argüman girmek bir soruna yol açmaz. Ancak bu durumda programcı 
    bu argümanları kullanamaz. Burada bir kez daha "argc" ve "argv" isimlerinin zorunlu olmadığını ancak geleneksel oladuğunu vurgulamak istiyoruz. 
    Yani program aşağıdaki gibi yazılsaydı da hiçbir sorun oluşmazdı:

    #include <stdio.h>

    int main(int a, char **b)
    {
        printf("%d\n", a);

        for (int i = 0; i < a; ++i)
            puts(b[i]);

        return 0;
    }

    "argc" ismi "argument counter" sözcüklerinden "argv" ismi ise "argument vector" isminden (burada "vector" dizi anlamında kullanılmıştır) 
    ya da "argument variable list" sçzcüklerinden kısaltmadır.----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
                                                    78. Ders 28/04/2023 - Cuma
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıdaki örnekte komut satırından alınan değerlerin toplamı ekrana yazdırılmıştır. Programının isminin "sample" olduğunu varsayalım:

    ./sample 10 20 30
    60.000000
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    double total;

    if (argc < 2) {
        fprintf(stderr, "wrong number of arguments!..\n");
        exit(EXIT_FAILURE);
    }

    total = 0;
    for (int i = 1; i < argc; ++i)
        total += atof(argv[i]);

    printf("%f\n", total);
  
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Aşağıda örnekte bir komut satırı argümanı biçiminde alınan yol ifadesi ile belirtilen dosyanın içeriğini yazdıran örnek bir C programı
    görülmektedir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])
{
    FILE *f;
    int ch;

    if (argc != 2) {
        fprintf(stderr, "wronh number of arguments!..\n");
        exit(EXIT_FAILURE);
    }

    if ((f = fopen(argv[1], "r")) == NULL) {
        fprintf(stderr, "cannot open file: %s\n", argv[1]);
        exit(EXIT_FAILURE);
    }

    while ((ch = fgetc(f)) != EOF)
        putchar(ch);

    if (ferror(f)) {
        fprintf(stderr, "cannot read file!..\n");
        exit(EXIT_FAILURE);
    }

    fclose(f);
  
    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    Bir program komut satırı argümanlarını kullanmıyor olsa bile (yani örneğin main fonksiyonunun parametresi void olsa bile) programın çalıştırılması sırasında
    komut satırı argümanı verilmesi genel olarak bir soruna yol açmamaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Biz şimdiye kadar önişlemci komutları olarak yalnızca #include ve #define komutlarını gördük. Şimdi çok kullanılan bazı önişlemci komutlarını 
    göreceğiz. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    #if komutu adeta if deyiminin önişlem aşamasında işlem gören biçimi gibidir. #if komutunun genel biçimi şöyledir:

    #if <sabit_ifadesi>
        ...
    #else
        ...
    #endif

    Komutta #else kısmı hiç olmayabilir. Ancak #if ve #endif kısımları olmak zorundadır. #else kısmı olmayan #if komutu şu biçimdedir:

    #if <sabit_ifadesi>
    ...
    #endif

    Sabit ifadesi önişlemci aşamsında net sayısal değeri hesaplanacak bir ifadeden oluşmalıdır. Örneğin:

    #if 1
        ...
    #else
        ...
    #endif

    Örneğin:

    #if MAX > 10
    ...
    #else
    ...
    #endif

    Bu #if komutunun geçerli olabilmesi için MAX değişkenin de #define ile oluşturulmuş bir sembolik sabit olması gerekmektedir. 

    #if komutu şöyle çalışır: Önişlemci #if komutunun yanındaki ifadenin sayısal değerini hesaplar. Bu değer sıfır dışı bir değer ise #if ile #else
    arasındaki kısım, 0 ise #else ile #endif arasındaki kısım derleme modülüne verilir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
    #if 1
        printf("Yes\n")
    #else
        printf("No\n");
    #endif

        return 0;
    }

    Bu kod derleme modülüne geldiğinde derleme modülü kodu şöyle görecektir:

    <stdio.h dosyasının içeriği>

    int main(void)
    {
        printf("Yes\n");

        return 0;
    }

    #if komutunu normal if deyimiyle karıştırmayınız. Normal if deyiminde if deyiminin doğruysa kısmı da yanlışsa kısmı da kodda bulunmaktadır. 
    Akış programın çalışma zamanı sırasında sapar. Halbuki #if komutunda derleme modülüne hangi kısmın verileceği belirlenmektedir. Yani çalışan programda
    bir if deyimi yoktur. 

    Pekiyi #if komutunun ne anlamı vardır? İşte bazen çeşitli durumlarda çeşitli bazı kod parçalarının atılıp bazı kod parçalarının derleme modülüne 
    verilmesi istenebilir. Örneğin bir kütüphanenin 3 versiyonundan sonra bazı şeylerin değişmiş olduğunu varsayalım. Bu durumda biz kodumuzu hem 3 versiyonundan
    sonraki versiyonlarda hem de önceki versiyonlarda çalışacak biçimde düzenlemek isteyebiliriz. Bu ayarlamayı da tek yerden yapabiliriz:

    #define VERSION		3.4
    ...

    #if VERSION > 3
        ...
    #else
        ...
    #endif

    Bazen programcılar kodun bir bölümünü derleme modülünden çıkartmak için yorum satırı kullanmak yerine #if komutunu tercih ederler. Örneğin:

    #if 1

    int main(vodid)
    {
        ...
    }

    #endif

    #if 0

    int main(vodid)
    {
        ...
    }

    #endif

    Yorum satırı kodun bir bölümünün derleme dışında bırakılmasının çeşitli problemleri vardır. Aşağıdaki gibi fonksiyon olsun:


    void foo(void)
    {
        /* this function performs ... */
        ...
    }

    Biz şimdi bu fonksiyonu yorum satırı içerisine alarak derleme dışı bırakmak isteyelim:

    /*
    void foo(void)
    {
        /* this function performs ... */
        ...
    }
    */

    Görüldüğü gibi burada içi içe yorum satırları oluşmuş durumdadır. İç içe yorum satırlarının düzgün bir biçimde ele alınıp alınmayacağı
    derleyicileri yazanların isteğine bırakılmıştır. Bazı derleyicilerde yukarıdaki kod error ile sonuçlanabilir. Halbuki aşağıdaki gibi 
    bir devre dışı bırakmada bir sorun yoktur:

    #if 0

    void foo(void)
    {
        /* this function performs ... */
        ...
    }
    
    #endif

    #if komutunda iç içe #if'ler için  #elif (else if anlamında) bir parça da eklenmiştir. Örneğin:

    #if VERSION == 1
        ....
    #else
        #if VERSION == 2
            ...
        #else 
            #if VERSION == 3
                ...
            #endif
        #endif
    #endif

    Böylesi iç içe #if'lerin oluşturulması oldukça zordur. Bunun yerine aynı iişlemler #elif kullanılarak daha kolay gerçekleştirilebilir. Örneğin:

    #if VERSION == 1
        ....
    #elif VERSION == 2
        ...
    #elif VERSION == 3
        ...
    #endif
    
    #elif anahtar sözcüğünün yanında yine bir sabit ifadesinin olması gerektiğine ve her #elif kısmının #endif ile kapatılmadığına toplamda 
    yalnızca bir tane #endif kullanıldığına dikkat ediniz. #elif merdivenin sonunda #else de kullanılabilmektedir. Örneğin:

    #if VERSION == 1
        ....
    #elif VERSION == 2
        ...
    #elif VERSION == 3
        ...
    #else
        ....
    #endif

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    #ifdef komutu en çok kullanılan önişlemci komutlarından biridir. Komutun genel biçimi şöyledir:

    #ifdef <sembolik_sabit>
        ...
    #else
        ...
    #endif

    #ifdef anahtar sözcüğünün yanında bir ifade değil bir sembolik sabit ismi olur. Komut eğer bu sembolik sabit define edilmişse #else'e kadar olan kısmı
    derleme modülüne verir, eğer bu sembolik sabit define edilmemişse #else ile #endif arasındaki kısmı derleme modülüne verir. #else kısmı yine 
    olmak zorunda değildir. Buradaki sembolik sabitin ne olarak define edildiğinin bir önemi yoktur. Asıl önemli olan bu sembolik sabitin define edilip
    edilmediğidir. Örneğin:

    #include <stdio.h>

    #define MAX 

    int main(void)
    {
    #ifdef MAX
        printf("Yes\n");
    #else
        printf("No\n");
    #endif

        return 0;
    }

    #ifndef komutu da tamamen #ifdef komutunun tersini yapmaktadır. Genel biçimi şöyledir:

    #ifndef <sembolik_sabit>
        ...
    #else
        ...
    #endif

    Önişemci eğer #ifndef komutunun yanındaki sambolik sabit define edilmemişse #else'e kadar olan kısmı, edilmişse #else ile #endif arasındaki kısmı 
    derleme modülüne vermektedir. Yine komutun #else kısmı bulunmayabilir. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    #if, #ifdef ve #ifndef komutları en fazla kullanılan önişlemci komutlarıdır. Pek çok proje derlenmeden önce "konfigüre"
    edilmektedir. Konfigürasyon projedeki çeşitli öğelerin belirlenmesi anlamına gelmektedir. Konfigürasyon aşamasında
    kullanıcının belirlemelerinden hareketle çeşitli sembolik sabitler define edilmekte ve projenin kodu da #if, #ifdef 
    ve #ifndef komutları ile bu sembolik sabitlere başvurularak yazılmaktadır. Örneğin Linux işletim sisteminin çekirdeğini
    yeniden derlemek isteyelim. Ancak çekirdeğin pek çok parametresini kendimize göre değiştirmek isteyelim. İşte bunun 
    için önce çekirdek konfigüre edilmektedir. Bu konfigürasyon sonucunda çeşirli sembolik sabitler bir başlık dosyası 
    biçiminde oluşturulmaktadır. Zaten çekirdeğin kaynak kodları da bu sembolik sabitlere dayalı biçimde yazıldığı için
    uygulamacının belirlemekleri çekirdek kodlarına yanısıtılmış olmaktadır.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    #ifndef komutu "include korumaları (include guard)" için sıkça kullanılmaktadır. include koruması "bir dosyanın doğrudan ya da dolaylı olarak 
    birden fazla kez include edilmesi durumunda sorun çıkmaması için" oluşturulan yapıya denilmektedir. Bir dosyanın ikinci kez include edilmesi her 
    zaman olmasa da genellikle sorunlara yol açabilen bir durum oluşturmaktadır. Örneğin "sample.h" dosyasının içeriği şöyle olsun:

    /* sample.h */

    #define MAX     10

    struct POINT {
        int x, y;
    };

    void draw_line(const struct POINT *pt1, const struct POINT *pt2);

    Biz bu dosyayı aşağıdaki gibi iki kez include etmiş olalım:

    #include "sample.h"
    #include "sample.h"

    Aynı yapının aynı isimle ikinci kez bildirilmesi derleme sırasında error oluşmasına yol açacaktır. 

    #ifndef komutu ile include koruması şöyle oluşturulmaktadır:

    #ifndef <sembolik_sabit_ismi>
    #define <sembolik_sabit_ismi>

    <dosya içeriği>	

    #endif

    Burada #ifndef'in yanındaki makro ismi dosya isminden hareketle uydurulmuş herhangi bir isimdir. Örneğin include dosyasının "sample.h" 
    isminde olduğunu varsayalım. Biz bu ismi SAMPLE_H_ biçiminde uydurabiliriz:

    #ifndef SAMPLE_H_
    #define SAMPLE_H_
    
    <dosya içeriği>
    
    #endif

    include koruması şöyle çalışmaktadır: Önişlemci dosyayı ilk kez gördüğünde SAMPLE_H_ isimli makronun define edilmemiş olduğunu tespit edecek ve 
    böylece dosyanın içeriğini derleme modülüne verecektir. Ancak bu sırada ilgili makro define edilmiş olmaktadır. Bu durumda artık önişlemci dosyanın 
    içeriğini ikinci kez derleme modülüne vermeyecektir. Programcının kendi oluşturduğu başlık dosyalarında yukarıdaki gibi include koruması uygulaması gerekir.
    Örneğin:

    #ifndef SAMPLE_H_
    #define SAMPLE_H_

    #define MAX     10

    struct POINT {
        int x, y;
    };

    void draw_line(const struct POINT *pt1, const struct POINT *pt2);

    #endif

    Şüphesiz programcılar bir include dosyasını açıkça aşağıdaki gibi include etmezler:

    #include "sample.h"
    #include "sample.h"

    Ancak include dosyaları dolaylı bir biçimde birden fazla kez include edilebilmektedir. Örneğin projemizde "a.h" ve "b.h" isimli iki başlık dosyası olsun. 
    Biz de bu iki başlık dosyasını aşağıdaki gibi include etmiş olalım:

    #include "a.h"
    #include "b.h"

    Bu başlık dosyaları kendi içerisinde aynı başlık dosyalarını mecburen include ediyor olabilir. Örneğin:

    /* a.h */

    #include <stddef.h>

    struct SAMPLE {
        size_t a;
        size_t b;
    };

    /* b.h */

    struct MAMPLE {
        size_t x;
        size_t y;
    };

    Burada görüldüğü gibi <stddef.h> dosyası iki kez önişlemci modülü tarafından dolaylı bir biçimde görülecektir. Tabii <stddef.h> içerisinde ve C'nin 
    standart başlık dosyaları içerisinde zaten include koruması yapılmış durumdadır. Dolaylı include işlemiyle bir dosyanın birden fazla kez include edilmesi 
    durumlarıyla sık karşılaşılmaktadır. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C standartlarında bir grup "önceden tanımlanmış (predefined)" makrolar vardır. Bu makrolar genel olarak __XXX__ biçiminde isimlendirilmiştir. 
    Bu önceden tanımlanmış makrolar herhangi bir yerde define edilmiş değildir. Bunların kullanılması için herhangi bir başlık dosyasının include edilmesine 
    de gerek yoktur. Bunların önemli olanları şunlardır:

    __FILE__: Bu makroyu önişlemci gördüğünde dosyanın ismini (bu isim yol ifadesi de içerebilir) iki tırnak içerisinde makronun bulunduğu yere yerleştirmektedir. 
    Dosya isminin yalnızca bir isim mi yoksa yol ifadesi ile birlikte bir isim mi olacağı derleyiciden derleyiciye değişebilmektedir. Böylece biz bir program 
    içerisinde program dosyasının ismini elde edebiliriz. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        char fname[] = __FILE__;

        printf("%s\n", fname);

        return 0;
    }

    __FILE__ makrosu özellikle hata mesajlarının iletilmesinde kullanılmaktadır. 

    __LINE__: Önişlemci bu makroyu gördüğünde makronun bulunduğu satırın numarasını int bir sabit olarak makronun bulunduğu yere yerleştirmektedir. 
    Böylece biz dosyanın belli bir satırının satır numarasını program içerisinden elde edebiliriz. 

    #include <stdio.h>

    int main(void)
    {
        printf("%d\n", __LINE__);
        printf("%d\n", __LINE__);

        return 0;
    }

    __DATE__ ve __TIME__: Önişlemci bu makroları gördüğünde o anki tarih ve zaman bilgilerini iki tırnak içerisinde kod yerleştirmektedir. Örneğin:

    #include <stdio.h>

    int main(void)
    {
        printf("%s\n", __DATE__);
        printf("%s\n", __TIME__);

        return 0;
    }

    __cplusplus: Bu makro eğer derleme C'de yapılıyorsa define edilmemiş, C++'ta yapılıyorsa define edilmiş kabul edilmektedir. Dolayısıyla programcılar
    C kodlarının C++ derleyicileri tarafından derlenmesi durumunda iki dilin uymsuzluklarını gidermek için bu makroyu kullanabilirler. Örneğin C'deki bazı 
    özellikler C++'ta geçerli olmayabilmektedir. Bu durumda programcı her ne kadar C dilinde yazıyorsa da C++'ın kabaca C'yi kapsadığı fikriyle
    kodunun C++ derleyicisi ile derlenmesini de sağlayabilir. Örneğin "restrict göstericiler" C99 ile birlikte C'ye eklenmiştir. Ancak C++ bu özelliği 
    desteklememektedir. Bu durumda biz aşağıdaki gibi bir prototipi C++ uyumlu hale getirebiliriz:

    #ifdef __cplusplus
    void foo(void * p);
    #else
    void foo(void * restrict p);
    #endif

    __STDC_VERSION__: Önişlemci bu makroyu gördüğünde  201ymmL biçiminde long bir sabit yerleştirmektedir. Bu sayede biz C'nin hangi versiyonunda 
    çalıştırığımızı	derleme sırasında anlayabiliriz. Bu makro C99 ile birlikte C'ye eklenmiştir. Bazı derleyiciler (örneğin Microspft C derleyicileri)
    hala bu makroyu desteklememktedir.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Yukarıda ele almış olduğumuz önceden tanımlanmış standart makroların dışında pek çok derleyicinin kendine özgü "önceden tanımlanmış (predifined)" 
    makroları da vardır. Örneğin gcc derleyicileri __GNUC__ makrosunun define edildiğini varsaymaktadır. Böylece gcc derleyicisine özgü birtakım işlemleri 
    derleme sırasında koda dahil edebiliriz. Makroyu aşağıdaki test koduyla test edebilirsiniz:

    #include <stdio.h>

    int main(void)
    {
    #ifdef __GNUC__
        printf("gcc\n");
    #else
        printf("not gcc\n");
    #endif

        return 0;
    }

    Örneğin UNIX/Linux sistemlerindeki C derleyicileri standart olmasa da __unix__ isimli bir makronun define edilmiş olduğunu varsaymaktadır. Böylece biz 
    kodumuzda UNIX/Linux sistemlerine özgü işlemleri derleme aşamasında koda dahil edip çıkartabiliriz. Benzer biçimde Microsoft C derleyicilerinde de 
    32 bit Windows sistemleri için _WIN32 makrosu, 64 bit Windows sistemleri için _WIN64 makrosu define edilmiş kabul edilmektedir. Örneğin:

    #include <stdio.h>
    #include <stdlib.h>

    #if defined(__unix__)
    #include <sys/stat.h>
    #elif defined(_WIN32) || defined(_WIN64)
    #include <windows.h>
    #endif

    #define DIR_NAME       "xxx"

    int main(void)
    {
    #if defined(__unix__)
        if (mkdir(DIR_NAME, 0777) == -1) {
            fprintf(stderr, "cannot create directory!..\n");
            exit(EXIT_FAILURE);
        }
    #elif defined(_WIN32) || defined(_WIN64)
        if (!CreateDirectory(DIR_NAME, NULL)) {
            fprintf(stderr, "cannot create directory!..\n");
            exit(EXIT_FAILURE);
        }
    #else
        #error operating system not supported
    #endif

        return 0;
    }

    Bu kodda dizin yaratılmaya çalışılmıştır. Ancak dizin yaratma işlemi UNIX/Linux sistemlerinde ve Windows sistemlerinde farklı fonksiyonlarla yapılmaktadır. 
    Programcı da yukarıdaki örnekte o anda hangi sistemde çalışılıyorsa o sistemdeki fonksiyonu çağırmıştır. Yukarıdaki kodd ahenüz görmediğimiz 
    birkaç özelliği de kullanmış olduk.
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    #error önişlemci komutu o anda derleme (yani önişlem) bir ölümcül hata mesajıyla derleme işlemini kesmektedir. Komutun kullanımı şöyledir:

    #error <mesaj yazısı>

    Mesaj yazısının iki tırnak içerisinde olmadığına dikkat ediniz. Örneğin biz bir programı UNIX/Linux sistemleri için yazmış olabiliriz. Bu programı
    birisi Windows'ta derlemeye çalışırsa hata mesajıyla derlemenin kesilmesini isteyebiliriz:

    #ifndef __unix__
        #error operatating system is not UNIX/linux
    #endif
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    defined isimli önişlemci operatörü belli bir makro ya da sembolik sabit define edilmişse 1 değerini define edilmemişse 0 değerini vermektedir. 
    Bu önişlemci operatörü makro ya da sembolik sabitlere ilişkin karmaşık koşulları oluşturmak için kullanılmaktadır. Örneğin SIZE ve COUNT define edilmişse 
    bir kod parçasını derleme modülüne vermek isteyelim. Bunu şöyle yapmaya çalışabiliriz:

    #ifdef SIZE
        #ifdef COUNT
            ...
        #endif
    #endif

    Ancak defined operatörü sayesinde bu işlem basit bir biçimde şöyle de yapılabilir:

    #if defined(SIZE) && defined(COUNT)
        ....
    #endif
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define SIZE        10
#define COUNT       20

int main(void)
{

#if defined(SIZE) && defined(COUNT)
    printf("yes\n");
#else
    printf("no\n");
#endif

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    #define komutunda bir makro parametresi komutun STR2 kısmında başına # önişlemci operatörü getirilerek kullanılırsa bu parametre 
    önişlemci tarafından iki tırnak içerisine alınmaltadır. Örneğin:

    #define NAME(name)		#name

    Burada biz kod içerisinde NAME(x) gibi bir ifade kullandığımızda bunun yerine önişlemci "x" yerleştirecektir. 
----------------------------------------------------------------------------------------------------------------------*/

#include <stdio.h>

#define MSG(msg)		puts(#msg)

int main(void)
{
    MSG(this is a test);

    return 0;
}

/*----------------------------------------------------------------------------------------------------------------------
    #define komutunda komutun STR2 kısmında ## önişlemci operatörü kullanılırsa bu işleme "atom yapıştırma (token pasting)" denilmektedir. Örneğin:

    #define WSTR(msg)		L##msg

    Burada biz WSTR makrosuna iki tırnaklı bir yazı verdiğimizde makro bunun başına L harfi iliştirecektir. Örneğin:

    #include <stdio.h>
    #include <wchar.h>

    #define WSTR(msg)		L##msg

    int main(void)
    {
        wprintf(L"%s\n", WSTR("test"));

        return 0;
    }

    Örneğin:

    #include <stdio.h>

    #define Unicode

    #ifdef Unicode
        #define STR(str)	L##str
    #else
        #define STR(str)		str
    #endif

    #ifdef Unicode
        #define _tprintf		wprintf
    #else
        #define _tprintf	printf
    #endif

    int main(void)
    {
        _tprintf(STR("%s\n"), STR("this is a test\n"));

        return 0;
    }

    Burada Unicode sembolik sabiti deine edilmişse _tprintf ismi wprintf ile define edilmemişse printf ile yer değiştirmektedir. Programcı tüm 
    string'leri STR makrosuyla kullanırsa bu sayede Unicode ile ASCII arasında kodunu büyük ölüçüde uyumluı tutabilir. 

    Örneğin:

    #include <stdio.h>

    #define TEST(a, b)		a##b

    int main(void)
    {
        int TEST(x, y);

        xy = 10;
        printf("%d\n", xy);

        return 0;
    }

    Örneğin:

    #include <stdio.h>

    #define PREFIXED_NAME(name)	sys_##name

    int main(void)
    {
        int PREFIXED_NAME(info);

        sys_info = 10;

        printf("%d\n", sys_info);

        return 0;
    }

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Biz daha önce küçük kod parçalarının "fonksion çağrılarını elimine etmek" için makro biçiminde yazılabileceğini söylemiştik. Örneğin bir sayının 
    karesini alan bir fonksiyon söz konusu olsun:

    int square(int a)
    {
        return a * a;
    }
    
    Biz de bu fonksiyonu şöyle çağırmış olalım:

    result = square(++x);

    Burada bir sorun yoktur. Çünkü önce argümanın değeri hesaplanmakta daha sonra parametre değişkenine kopyalama yapılmaktadır. Şimdi de bu 
    fonksiyonu makro biçiminde yazalım:

    #define square(a)		((a) * (a))

    Artık makronun aşağıdaki gibi kullanımı "tanımsız davranış (undefined behavior)" oluşturacaktır:

    result = square(++x);


    İşte fonksiyonları fonksiyon çağrılarını elimine etmek için makro biçiminde yazmak şu nedenlerden dolayı sorunlu bir konudur. 

    - Makro yazımı zordur. Makro parametrelerinin parantez içerisine alınması, makronun en dıştan paranteze alınması aokunabilirliği zorlaştırmaktadır.

    - Makro yazımının birden fazla satıra yaydırılması zordur.

    - Makro açımı önişlemci tarafından yapıldığı için çeşitli kontroller makrolar üzerinde sağlanamamaktadır. 

    - Makroların çağrılması sırasında tanımsız davranışların oluşmaması için dikkat edilmesi gerekmektedir.

    - Makrolar içerisinde bloklu işlemleri oluşturmak zordur. 

    İşte fonksiyon çağrılarını elimine etmek makrolara daha iyi bir alternatif oluşturan ""inline fonksiyonlar" denilen bir fonksiyon çeşidi düünülmüştür. 
    inline fonksiyonlar C'ye resmi olarak C99 ile birlikte sokulmuştur. Ancak C++'ta ilk standartlardan beri (C++98) inline fonksiyon bulunmaktadır. 
    Her ne kadar inline fonksiyonlar C'ye C99 ile resmi olarak sokulmuşsa da aslında derleyicilerin önemli bir bölümü bir "eklenti (extension)" biçiminde 
    inline fonksiyonları destekliyordu. Ancak standart öncesinde derleyicilerin inline fonksiyon semantikleri arasında küçük farklılıklar bulunabiliyordu. 
    Her ne kadar C++ Programlama Dili C programlama Dilini kapsıyorsa da C++'ın inline fonksiyon semantiği ile C99'un inline fonksiyon semantiği arasında
    farklılıklar bulunmaktadır. Biz burada C99 ile C'ye eklenmiş olan inline fonksiyon semantiği üzerinde duracağız. 
----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    Bir fonksiyonu inline yapmak için tek yapılacak şey fonksiyonun başına aşağıdaki belirleyicileri getirmektir:

    inline
    static inline (ya da inline static)
    extern inline (ya da inline extern)

    inline anahtar sözcüğü C99'da "fonksiyon belirleyicisi (function specifier)" denilen bir gruba dahil edilmiştir. 

    inline bir fonksiyon çağrıldığında derleyici makine kodlarına fonksiyon çağırma kodunu (CALL komutu) eklemek yerine doğrudan kodun iç kısmını 
    tıpkı bir makroymuş gibi çağrılma yerine enjekte edebilmektedir. Örneğin:

    inline int square(int a)
    {
        return a * a;
    }

    Burada biz fonksiyonu şöyle çağırmış olalım:

    result = square(1 + 2);

    Derleyici square fonksiyonunu çağırmak yerine fonksiyonun iç kodunu çağırma yerine aşağıdaki gibi enjekte edebilmektedir:

    result = 3 * 3;

    Böylece daha önce görmüş olduğumuz makrolara benzer bir etki yaratılmış olur. Ancak bu etki önişlemci tarafından değil bizzat derleme modülü tarafından 
    sağlanmaktadır. 

    inline fonksiyonlar normal bir fonksiyon gibi yazılmaktadır. Derleyici inline fonksiyonlar üzerinde normal fonksiyonlarda yaptığı bütün kontrolleri 
    yapmaktadır. Ancak bir inline fonksiyon çağrıldığında derleyici onu bir fonksiyon gibi çağırmak yerine bizzat onun iç kodunu çağırma yerine enjekte edebilmektedir. 
    Böylece C99 ve sonrasında artık fonksiyon çağırma işlemini elimine etmek için küçük fonksiyonların makro yerine inline fonksiyon biçiminde yazılması 
    tavsiye edilmektedir. Zaten inline fonksiyonlar makroların yukarıda sıraladığımız olumsuzluklarını gidermek amacıyla dile sokulmuştur. 

    Ancak C'de inline fonksiyonların bazı ayrıntıları vardır. Öncelikle "inline" belirleyicisi "bir emir değil rica" niteliğindedir. Yani biz bir 
    fonksiyonu inline yaptığımız zaman derleyici o fonksiyonu inline olarak açmayabilir. inline fonksiyonların derleyiciler tarafından inline olarak 
    açılmaları zorunlu değildir. Pek çok derleyicide "inline açım (inline expansion)" derleyicinin optimizasyon seçenekleriyle ilişkilendirilmiştir. 
    Yani derleyicilerin optimizasyon seçenekleri açılmazsa genellikle derleyiciler inline açım yapmamaktadır. Microsoft derleyicilerinin inline açım yapması 
    için en azından /O2 optimizasyon seçeneğinin kullanılması gcc ve clang derleyicilerinde de en azından -O2 seçeneğinin kullanılması gerekmektedir. Örneğin:
    
    cl /O2 sample.c					(Microsoft)
    gcc -O2 -o sample sample.c		(gcc)

    Tabii programcı bu derleyicilerde optimizasyon seçeneklerini açmış olsa bile derleyici yine de inline açımı yapmayabilir. Derleyiciler genellikle inline 
    açım yapmadıkları durumunda herhangi bir uyarı vermezler. Programcı da inline açımın yapılıp yapılmadığını ancak derleyicinin ürettiği kodu sembolik makine 
    kodlarını inceleyerek anlayabilmektedir. 

    Pekiyi derleyici neden fonksiyonu inline olarak açmayabilmektedir? Bunun çeşitli nedenleri olabilir. Örneğin:

    - Özyinelemeli fonksiyonların inline açımları mümkün değildir. 
    - İçlerinde karmaşı deyimler olan fonksiyonalar (örneğin iç içe if deyimleri gibi) inline açılamayabilir. 
    - Çalışması uzun zaman alan kodların inline olarak açılması uygun değldir. Örneğin bir fonksiyonun içerisinde 1000000 kere dönen büyük 
    bir döngü olsun. Böyle bir fonksiyonun inline açılmasının bir faydası olmayacağı gibi kodu büyütebilmesi gibi zararları söz konusu olabilmektedir. 
    - Fonksiyonun adresi alınıp kod içerisinde kullanılıyorsa derleyici fonksiyonu inline olarak açmak istemeyebilir. 
    - Fonksiyon çok fazla satırdan oluşuyorsa inline açım kodu ciddi biçimde büyütebilecektir. Derleyiciler bu durumda inline açım yapmak istemeyebilirler.

    Tersten gidersek "basit, birkaç satırlık, uzun döngüler ve karmaşık if deyimleri gibi deyimleri içermeyen" fonksiyonlar inline olarak açılmaya aday fonksiyonlardır. 

    Pekiyi derleyici inline fonksiyonu inline olarak açmazsa ne yapacaktır? İşte bu durumda C99 ve sonrasında fonksiyonun nasıl inline tanımlandığna göre 
    derleyicinin davranışı farklılaşmaktadır. Yukarıda da belirttiğimiz gibi C'de inline fonksiyonlar üç biçimde olabilmektedir:

    inline
    static inline
    extern inline

    C++'ta böyle bir ayrım yoktur. Ancak C'de yukarıdaki üç inline tanımlama farklı anlamlara gelmektedir. 

    Eğer C'de inline fonksiyon static ya da extern anahtar sözcüğü olmadan yalnızca inline anahtar sözcüğü ile tanımlanmışsa bu durumda derleyici fonksiyonu
    inline açarsa sorun olmaz. Ancak derleyici fonksiyonu inline olarak açmazsa fonksiyonun tanımlamasını amaç kod içerisine yerleştirmez. Yani sanki fonksiyon 
    hiç tanımlanmamış gibi işlem yapar. Fakat fonksiyonu CALL makine komutuyla çağırır. İşte bu durumda eğer inline açım yapılamadıysa ve başka bir modülde de 
    aynı isimli bir fonksiyon yoksa muhtemelen build işlemi link aşamasında çağrılan fonksiyonu linker'ın bulamaması biçiminde error ile sonuçlanacaktır. 
    Örneğin:

    /* sample.c  */

    #include <stdio.h>

    inline int square(int a)
    {
        return a * a;
    }

    int main(void)
    {
        int result;

        result = square(3);

        printf("%d\n", result);

        return 0;
    }

    Burada square fonksiyonu static ya da extern belirleyicisi olmadan yalnızca inline belirleyicisi ile tanımlanmıştır. Eğer fonksiyon inline açılamazsa
    link aşamasında error olulacaktır. Örneğin biz gcc ve clang derleycilende bu fonksiyonu optimizasyon seçeneklerini açmadan aşağıdaki gibi derlemek isteyelim:

    gcc -o sample sample.c

    Bu durumda link aşamasında şöyle bir hata alırız:

    /usr/bin/ld: /tmp/ccWaZ5zm.o: in function `main':
    sample.c:(.text+0x1d): undefined reference to `square'
    collect2: error: ld returned 1 exit status

    Ancak biz program- -O2 seçeneği ile derlersek derleyici muhtemelen inline açım yapacağı için her ne kadar square fonksiyonunu amaç koda yerleştirmeyecekse de 
    programın derlenip çalışmasında muhtemelen bir hata oluşmayacaktır:

    gcc -O2 -o sample sample.c

    Yalnızca inline belirleyicisi ile fonksiyonu tanımladığımızda derleyici inline açımı yapsın ya da yapmasın fonksiyon kodunu amaç koda yazmadığına dikkat ediniz.

    static inline fonksiyonlar eğer derleyici tarafından inline olarak açılmazlarsa static biçimde amaç dosyaya yerleştirilmektedir. Böylece derleyici CALL makine komutu 
    ile bu static fonksiyonu çağırmış olmaktadır. Yani bu durumda derleyici inline fonksiyonu açsa da açmasa da programın derlenip çalışmasında bir sorun oluşmayacaktır. Örneğin:

    static inline int square(int a)
    {
        return a a a;
    }

    Tabii static inline fonksiyonlarda derleyici inline açımı yaparsa aslında fonksiyonu hiç amaç dosyaya yazmayabilir. (Nasıl olsa fonksiyonun iç kodu 
    enjekte edişlmiştir ve fonksiyon da başka modülden zaten kullanılamamaktadır.)	Ancak inline açımı yapamazsa static	düzeyde fonksiyonu amaç dosyaya 
    yazacaktır. Tabii fonksiyon static olduğu için başka bir modülden çağrılması mümkün olmayacaktır. 

    static inline fonksiyonlar C'de en çok kullanılan inline fonksiyonalrdır. Ancak bunların da en önemli dezavantajı birden fazla modülde aynı static inline 
    fonksiyonun kullanılması durumunda eğer inline açım yapılamazsa bu fonksiyonların kodlarının ayrı ayrı bu modüllerde bulunması zorunluluğudur. 

    Normal bir fonksiyon zaten extern biçimdedir. Yani onun tanımlamasının önüne extern getirilip getirilmemesinin bir farkı yoktur. Örneğin:

    extern void foo(void)
    {
        ...
    }

    Burada extern belirleyicisi gereksiz kullanılmıştır. Fonksiyonlar zaten default external linkage'a sahiptir. Ancak extern anahtar sözcüğü ile 
    inline anahtar sözcüğü bir arada kullanılırsa bu başka bir anlam ifade etmektedir. extern inline fonksiyonlar derleyici tarafından inline açılsalar da açılmasalar da 
    her zaman object dosyaya extern linkage biçiminde yazılırlar. Örneğin:

    extern inline int square(int a)
    {
        return a * a;
    }

    Burada fonksiyon extern ilnine olarak tanımlanmıştır. Derleyici çağrı sırasında bu fonksiyonu inline olarak açsa da açmasa da kod yine bu fonksiyonu 
    normal external linkage'a sahip bir fonksiyon olarak yazar. Bu durumun dezavatajı şudur: Biz extern inline bir fonksiyonu birden fazla modülde kullanırsak 
    birden fazla extern tanımalası modüllerde bulunur. Bu da link aşamasında soruna yol açar. Özetle üç inline fonksiyon arasındaki farlılıklar şunlardır:

    1) inline: Derleyici inline açım yaparsa sorun olmaz, ancak yapamazsa CALL makine komutunu yerleştirir. Başka modülde bu fonksiyon yoksa 
    link aşamasında error oluşur. 

    2) static inline: Derleyici inline açım yapsa da yapmasa da sorun oluşmaz. Ancak fonksiyonu inline açamazsa static linkage'a sahip biçimde amaç dosyaya 
    yazar ve fonksiyonu CALL makine komutuyla çağırır.

    3) extern inline: Derleyici inline açım yaparsa sorun olmaz. Açamazsa CALL makine komutunu kullanır. Ancak her zaman derleyici fonksiyonu 
    amaç dosyaya extern linkage'a sahip olacak biçimde yazar. Dolayısıyla birden fazla modülde bu fonksiyon kullanılırsa error oluşur. 

    Burada programcı nasıl bir yol izlemeidir? Eğer programcı inline fonksiyon derleyici tarafından inline açılamıyorsa bir sorun oluşmasın istiyorsa
    ya static inline tanımlamasını tercih etmelidir ya da inline tanımlamasını tercih edip ayrıca fonksiyonun static olmayan bir tanımlamasını başka bir modülde 
    bulundurmalıdır. C'de inline fonksiyonlar için en çok tercih edilen biçim "static inline" biçimidir. 

    inline fonksiyonlar her derleme işleminde derleyici tarafındna görülmek zorundadır. Dolayısıyla bu fonksiyonlar kütüphanelere yerleştirilemezler. 
    Birden fazla modülle çalışırken biz tipik olarak inline fonksiyonları bir başlık dosyasına yerleştirir. Bu başlık dosyasını da birden fazla modülde
    include edebiliriz. Örneğin:

    /* project.h */

    #ifndef PROJECT_H_
    #define PROJECT_H_

    static inline foo()
    {
        ...
    }

    #endif

    /* sample.c */

    #include <stdio.h>
    #include "project.h"
    ...

    /* mample.c */

    #include <stdio.h>
    #include "project.h"
    ...

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------
    C99 ile birlikte standartlara "restrict" isimli bir tür niteleyicisi daha eklenmiştir. C++ C'yi temel aldığı halde bu restrict niteleyicisini bünyesine katmamıştır. 
    restrict niteleyicisi yalnızca göstericilerle "üst düzey (top level)" biçimde kullanılablir. Yani sıradan bir nesne restrict olamaz. Yalnızca göstericiler restrict olabilir. 
    Göstericilerin gösterdiği yer restrict olamaz yalnızca kendileri restrict olablir. Örneğin:

    restrict int a;				/* geçersiz */
    restrict int *pi;			/* geçersiz! */
    char * restrict pc;			/* geçerli */

    restrict anahtar sözcüğü "pointer aliasing" denilen bir optimizasyonu mümkün hale getirebilmek için düşünülmüştür. Bir göstericinin gösterdiği yere erişim en az
    iki makine komutu ile yapılmaktadır. Örneğin:

    a = *pi;

    Bunun muhtemel makine kaomutları şöyle olacaktır:

    mov reg1, pi
    mov reg2, [reg1]
    mov a, reg2

    Pekiyi bir kod içerisinde *p gibi bir kullanım birden fazla yerde varsa derleyici daha önceden elde ettiği *p değerini opimizasyon amaçlı saklayıp kullanabilir mi?
    Yanıt kullanabilir. Ancak derleyicinin o göstericinin gösterdiği yerdeki nesnenin değişmemiş olduğunu garanti etmesi gerekir. Örneğin:

    for (;;) {
        a = *pi;
        foo();
    }

    Derleyici burada foo fonksiyonun pi'nin gösterdiği yerdeki nesneyi değiştirmiş olabileceğini dikkate almak zorundadır. Dolayısıyla *pi'ye yeniden başvuru yapabilir. 
    Tabii derleyici foo'nun içini görürse ve *pi'nin yerel bir nesneyi gösterdiğini bilirse yani ""bazı koşullar altında"" foo'nun *pi'yi değiştirmeyeceğini anlayabilir. 
    Bu durumda optimizasyon yapabilir. Ancak bu karmaşık bir durumdur. Bazen bunu anlamak çok zor bazen de mümkün olmayabilir. İşte restrict anahtar sözcüğü 
    bu tarz optimizasyonları derleyici yapabilsin diye uydurulmuştur. restrict anahtar sözcüğü derleyiciye şunu demektedir: "Derleyici bu göstericinin gösteridiği 
    yerdeki nesneye ben yalnızca bu gösterici ile erişiyorum. Başka bir fonksiyon ya da başka bir yolla ona erişmeyeceğime söz veriyorum". Böylece restrict 
    göstericilerde derleyici o göstericilerin gösterdiği yerdeki nesnelerin erişiminde optimizasyonlar yapabilmektedir. Pekiyi biz bir göstericiyi restrict yaptığımız halde 
    o göstericinin gösterdiği yere başka bir nesne ya da gösterici yoluyla erişirsek ne olur? Bu durum C'de tanımsız davranışa yol açmaktadır. Çünkü burada 
    restrict ile verilen söz programcı tarafından tutulmamıştır. 

    restrict göstericiler C99'da fonksiyon prototiplerinde de sıkça karşımıza çıkmaktadır. Örneğin:

    char *strcpy(char * restrict dest, const char * restrict source);

    Burada restrict gösterici gördüğümüzde ne anlamalıyız? İşte mademki restrict göstricilerin gösterdiği yere yalnızca o gösterciler yoluyla erişilir o halde
    burada fonksiyona verdiğimiz iki adres çakışık olmamalıdır. Eğer bu iki adres çakışık olursa "tanımsız davranış (undefined behavior)" oluşur. Örneğin:

    void *memcpy(void * restrict s1, const void * restrict s2, size_t n);

    Burada da blokların çakışık olmaması gerekir. Ancak örneğin:

    void *memmove(void *s1, const void *s2, size_t n);

    Burada restrict gösterici kullanılmadığına göre bloklar çakışık olabilir. Şüphesiz göstericinin gösterdiği yerde bir güncelleme yapmayan fonksiyonlarının 
    prototiplerinde restrict gösterici bulundurmanın bir anlamı yoktur. Örneğin:

    int strcmp(const char *s1, const char *s2);

    Bazı işlemcilerde blok kopyalaması yapan makine komutları bulunmaktadır. Örneğin böyle bir işlemci söz konusu olsun. Biz de aşağıdaki gibi 
    bir fonksiyonu yazmış olalım:

    void reverse_copy(void *dest, void *source, size_t n)
    {
        ...
    }
    
    Bu fonksiyon ikinci parametresiyle verilen diziyi birinci parametresiyle verilen diziye tersten kopyalacak olsun. Bir işlemci bunu tek bir makine komutuyla 
    önce source'dan n kadar byte kendi içine çekip sonra bunu dest'e kopyalayabilir. Ancak burada bloklar çalıkışıksa fonksiyonun iç kodunun yapma çalıştığı 
    şey bu makine komutuyla yapılamayacaktır. Bu durumda biz derleyiciye bir güvence verebiliriz:

    void reverse_copy(void * restrict dest, void * restrict source, size_t n)
    {
        ...
    }
    
    Şimdi artık biz derleyiciye şunu demekteyiz: "Derleyici ben hiçbir zaman source ve dest adreslerinin gösterdiği yerlere başka bitr yolla erişmeyeceğim.
    Dolayısıyla benim adresini geçtiğim diziler çakışık olmayacaktır. Sen bunların çakışık olmayacağı garantisiyle istediğin optimizasyonu yap".

    C++'ta restrict göstericiler olmadığı için eğer kodun C++ uyumu olması isteniyorsa restrict göstericiler kullanılmamalıdır. 
----------------------------------------------------------------------------------------------------------------------------------*/

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/

/*----------------------------------------------------------------------------------------------------------------------

----------------------------------------------------------------------------------------------------------------------*/




