CSDKursNotlari/C-OzetNotlar-Ornekler.txt
C ve Sistem Programcıları Derneği a5169917d8 New version
2024-12-09 22:06:31 +03:00

28019 lines
1.2 MiB
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/*----------------------------------------------------------------------------------------------------------------------
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: 27/11/2024 - Perşembe
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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
ı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 beleklere
"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 dilin daha uygun 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ı için 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ından dilleri tipik olarak aşağıdaki gibi sınıflara ayırabiliriz:
- Prosedürel Diller (Procedural Languages): Bunlar altprogramların birbirlerini çağırması ile program yazma tekniğini desteklerler. 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 sınıflarla program yazma modelini
desteklemektedir.
- Fonksiyonel Diller (Functional Languages): Bıunlar adeta formül yazar gibi program yazmaya olanak sağlayan dillerdir. Aslında kendi aralarında bir spektrum
oluştururlar. 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 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 ya da el kol hareketleriyle program akışının oluşturulduğu çok yüksek seviyeli dillerdir.
Programlama eğitiminde kullanılan SCratch gibi.
- Çok Modelli Diller(Multiparadigm Languages): Yukarıdaki modellerden birden fazlasını belli ölçülerde destekleyen dillerdir. Örneğin C++2ta biz C gibi
kod yazabiliriz. Ama sınıflar kullanarak da kod yazabiliriz. Fonksiyonel bazı dil özelliklerini de C++'ta kullanabiliriz. O zaman C++ çok modelli
bir programlama dilidir. UYeni tasarlanan diller zaten genel olarak çok modelli olma eğilimindedir. Örneğin 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 kullanır. Derleyiciler genel olarak komut satırında çalışan GUI arayüzü olmayan programlardır.
C için ö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
kullanılabilmektedir. Mac OS ve Linux sistemlerinde iyi bir alternatifit.
- Eclipse IDE'si. Bu IDE bir Java IDE'si olarka çı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 bir IDE'dir. Kullanımı biraz dah zahmetli olsa da
çok az yer kaplamaktadır (light weight IDE).
- 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 diiş 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. 2'lik sistemde sayıları ifade etmek için 2 sembol kullanılmaktadır:
0
1
2'lik sistemde sayının her bir basamağına "bit (binary digit)" denilmektedir. 2'lik 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. Kile diğer bilimlerde "1000 katı" anlamına gelmektedir. Ancak bilgisayarlar 2'lik sistemi kullandığj için 1000 katı iyi bir
kat değildir. Bu nedenle genel olarak Kilo byte için 2'nin 102uncu 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 milyan kat anlamına gelmektedir. Ancak bilgisayar bilimlerinde genel olarak 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.
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ümleyeb (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 bulunmaktadır. Bir sayının bire tümleyeni sayıdaki
o'ların 1, 1'lerin 0 yapılmasıyla bulunur. 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:
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 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
---------------------
1 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 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?
CEVAP: En soldaki bit 0 yapılıp n sayısı 2'lik 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.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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 manris 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 bir 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 2'lik sistemde sayılar biçiminde tutulmaktadır. Bir yazıyı oluşturan elemanlara "karakter" denilmektedir. İşte
bir yazıda her bir karakter 2'lik 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.
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 2'lik 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
2'lik 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
Bilgisayar dünyasında 162lık sistem aslında 2'lik sistemin yoğun bir gösterimi olarak kullanılmaktadır. Yani 2'lik sistem çok yer kapladığı için kişiler
2'lik sistem yerine 16'lık sistemi kullanırlar. Bu nedenle belleği, dosyayı gösteren programlar bunları 2'lik sistem yerine 16'lık sistemde gösterirler.
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 bir ile ifade edilebilir:
0 000
1 001
2 010
3 011
4 100
5 101
6 110
7 111
Bu durumda bir octal sayı 2'lik sisteme kolay bir biçimde dönüştürülebilir:
476 100 111 110
741 111 100 001
Benzer biçimde 2'lik 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 2'lik 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 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 C
Derleyicisi kullanılmaktadır. UNIX/Linux ve MAC OS 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 port'una "mingw" denilmektedir.
C programını C derleyicisi ile derledikten sonra eğer hiçbir hata yoksa derleyici bize "relocatable object module" denilmektedir. Bu dosyaya biz Türkçe
"amaç kod dosyası" da diyeceğiz. Amaç kod dosyası (relocatable object module) daha sonra "bağlayıcı (linker)" denilen bir programa sokulur. Bu linker programı
"çalıştırılabilir (executable)" dosyaysı üretir. Biz de nihayetinde bu dosyayı çalıştırırz.
.c -----> C Derleyicisi -----> Object file -----> Bağlayıcı (Linker) -----> Çalıştırılabilir (executable)
Bağlayıcı (linker da diyeceğiz) aslında bir grup amaç dosyayı alıp tek bir çalıştırılabilir dosya oluşturmaktadır. 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 ağırklıklı olarak "GNU ld" isimli
bağlayıcı ya da "clang ldd bağlayıcısı" kullanılır.
Derleyicinin ürettiği amaç dosyanın uzantısı Windows sistemlerinde ".obj" biçimindedir. UNIX/Linux ve MAC OS sistemlerinde derleyicinin ürettiği amaç dosya
".o" uzantılı olur.
Bağlayıcnın ürettiği "çalıştırılabilir" dosya ise Windows sistemlerinde ".exe" uzantılıdır. UNIX/Linux ve MAC OS sistemlerinde dosyanın çalıştırılabilir olup
olmadığı uzantı ile değil dosya özellikleri (attributes) 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ı derlemeyi yaptıktan sonra zaten "linker" programını kendisi çalıştırmaktadır.
4) Artık cl.exe derleme işlemini yapıp bağlayıcı programı da (link.exe) ç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ı programı da bağımsız olarka ç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.
Derleme işlemi için şu komut uygulanı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çimd eoluşturualn ç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 dısyanın yalnızca isminin yazılması yeterlidir. Ancak UNIX/Linux ve MAC OS 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ılı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 Visuak 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 "solution" denilen kapların içerisindedir. O halde aslında bir
proje yaratmak için bir solution da yaratılmaktadır. Bir solution 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 "solution" da yaratılmış olur. Solution'ı idare etmek için
"Solution Explorer" denilen pencereden faydalanılı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ü
ı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 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 link 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 link edilir.
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 (solution'u) 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" denieln 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. 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 "hata mesajları" bu sorunları programcıya iletirler. Derleyicilerin verdiği
hata mesajları üçe ayrılmaktadır:
1) Uyarılar (Warnings)
2) Gerçek 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ı hatalara dikkat çekmek için uyarı vermektedir. Gerçek hatalar object dosyanın oluşunu engellecek 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 derleme işleminin bile devam ettirilmesini engelleyecek biçimde hatalardır. Normal olarak gerçek bir hata ile karşılaşıldığında tüm hataları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 etmek için gereken
RAM'in bulunmaması gibi durumlar tipik ölümcül hata gerekçeleridir.
Derleyiciler genellikle hata mesajlarında hatanın kaynak kodun neresinde eolduğunu belirtirler. Pek çok derleyici hata mesajlarına içsel birer numara
vermektedir. Bu numara yoluyla hata mesajı hakkında daha fazla bilgi elde edilebilmektedir. Hata mesajları standart mesajlar değildir. Derleyiciden derleyiciye
hata mesajları değişebilmktedir.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
Bir programlama dilinde kendi başına anlamlı olan en küçük birime "atom (token)" denilmektedir. Örneğinaşağıdaki gibi bir C kodu olsun:
if (a > 10)
x = 10;
else
y = 20;
Bu kodu şöyle atomlarına ayırabiliriz:
if
(
a
>
10
)
x
=
10
;
else
y
=
20
;
M3rhaba 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 parçalara 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.
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 olmaısı için derleyicinin
onu gördüğünden değişkendne farklı bir işlem uygulaması gerekir.
3) Sabitler (Literals/Constants): Bir sayı ya bir değişkenin içerisindedir ya da program içerisinde doğrudan yazılmıştır. İşte programda 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.
6) Ayıraçlar (Delimiters/Punctuators): Yukarıdaki grupların dışında kalan ifadeleri birbirindne 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ı 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 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 edielmiş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 parantex-köşeli parantez" tekniğini kullanacağız. Bu teknikte
ı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 aynı pozisyonda bulundurulması gerekir. Örneğin if deyimi şöyle ifade edilebilir:
if (<ifade>)
<deyim>
[
else
<deyim>
]
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
9. Ders 21/06/2022-Salı
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
Boşluk duygusu 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ıl 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şiki 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
geri dönüş değerinin türünün yazılması 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" yazılabilir. Tanımlama sırasında boş bırakmakla "void" yazmak 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 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 sol üst köşededir. printf fonksiyonunda iki tırnak içerisindeki "\n"
"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ımşlanmış)
biçimde bulunan fonksiyonlardır. maşn fonksiyonun sonundaki 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 ve C17 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. Aşağıda temel türleri açıklanmaktadır:
[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 MAC Os 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 bir ve
64 bit UNIX/Linux ve Mac OS 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
Mac OS sistemlerindeki derleyicilerde long türü 4 byte iken, 64 bir 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özcleri yer değiştirerek de ifade
edebiliriz.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
Hocam 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 MAC OS 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ıfı [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 (yan 16 bit) olmak zorundadır. 32 bit ve 64 bit Windows ve UNIX/Linux ve MAC OS 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" lafı 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 sonderece 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ü sistemlerin %99.9'unda 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 bit 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. Dolısıyla C90 uyumlu eski C derleyicilerinde bu türü kullanamayabilrsiniz.
long long tütü 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 syaı 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ıı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 prograöcı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.
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" sembolü 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 atarı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 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ısı bir değişkenine içerisine küçük tamsayı değerleri yerleştirecek olsa 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 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 derleyiciyte 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 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 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ı zamandabellekte yer ayırmaktadır.
Her tanımalam bir bildirimdir ancak her bildirim bir tanımlama değildir. Tabii bildirim olup da tanımlama olmayan durumlar seyrektir. Biz aksi söylenmediği sürece
"bildirim" dediğimizde bildirilen değişken için yer de ayrıldığını varsayacağız. Bildirim olup datanımlama olmayan durumları özel olarak konular içerisinde
vurgulayacağız. Ö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ülmedir */
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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 işlem bir ilkdeğer verme değildir. İlkdeğer verme bildirim sırasında değer atama anlamına gelmeketedir.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
Aslında C standartlarında "ekran" ve "klavye" lafları 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
fonksiyonun yazdıkları seri porta aktarılır. Aynı durum klavye için de geçerlidir. Aslında klavyeden okumak diye bir şey yoktur. stdin dosyasından okumak
diye bir şey vardır. stdin dosyası da klasik bilgisayar sistemlerinde genellikle klavyeye yönlendirilmiştir. Ancak örnğ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 kaynakları 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. Solay dayalı yazdırmak için bu genişlik beliritlen sayının
önüne ayrıca bir de '-' karakteri eklenir. Özellikle sütunsal hizalamalar için %-nd gibi (buarada n yerine bir sayı geitirilmelidir) format karakterleri
kullanılmaktadır. Eğer geniş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 ouble 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)" deni,lmektedir.
----------------------------------------------------------------------------------------------------------------------*/
#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 ekü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ı 2'lik sistemde tutulmaktadır.
C'de bir tamsayıyı 2'lik 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;
}
/*----------------------------------------------------------------------------------------------------------------------
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 sayısı 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
"sanki ters bölü karakter sabitlerinden birisini yazmak istiyormuşuz da onu yazamamışız gibi" durumu 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 "sanki tek tırnağın içerisine bir
şey yazılmamış gibi" durumu 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şti.
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 UTF16 ya da UNICODE
UTF32 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ürdne 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 karakterlerin hangi karakter kodlamasına
göre kodlandığını kabul ettiği kümedir. Ö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 başka
tek byte'lı karakter kodlamaları da vardır. Örneğin Micsosoft 1254, ISO 8859-9, OEM 857 gibi. Çalıştırma karakter
kümesi ise derleyicinin kaynak karakter kümesinde kabul ettiği karakterlerin nasıl depolandığına ilişkin karakter
kümesidir. Derleyicilerde genellikle default durumda bu iki karakter kümesi aynıdır. Örneğin:
int ch;
ch = 'ş';
Buradaki program parçasında biz UNICODE UTF-8 editörünü kullanmışsak bu editör buradaki 'ş' karakteri için iki
byte'lık bir sayı yazacaktır. Eğer derleyimizin kaynak karakter kümesi örneğin Microsoft 1254 ise derleyici bu iki
karakteri 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 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 byte'ları kodlayacaktır.
Durumun böyle olduğunu varsayalım. Şimdi 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 ekran 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 bizimle 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ı olarak Türkçe karakterleri oluşturursak 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 sağlayabilemiz
için kaynak dosyanın "UNICODE UTF8" olarak kodlanması, derleyicinin kaynak ve çalıştırma karakter kmelerinin de "UNICODE
UTF-8" olarak ayarlanması gerekir. Microsoft C derleyicilerinde kaynak ve çalıştırma karakter kümeleri "/source-charset:utf-8
/execution-charset:utf-8" komut satırı seçenekleri UTF-8 olarak ayarlanabilmektedir. Aynı ayar gcc ve clang
derleyicilerinde
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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 çağrıldığında klavyeden (stdin dosyasından)
bir karaktere basılıp ENTER tuşuna basılır. 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. Bunun verdiği değeri 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ıl 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 cgetchar tampon boş olduğu için klavyedne okuma
isteyecektir.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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ıı 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;
ı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ıı 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 Mac OS 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 MAC OS sistemlerinde
ise ".so" (shared object sözcüklerinden kısaltma) biçimindedir. Bugün Windows, UNIX/Linux ve Mac OS 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ığıı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
ı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ıı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ıı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ıı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ı
ı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 Mac OS 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ıı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ı
Ö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ıı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ıı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şlemcidden işlemciye değişebilmektedir. Buna işlemcinin "endian'lığı (endianness)"
denilmektedir. Intel işlemcvilerinde 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 anlamı byte değeri düşük adreste tutulur. Örneğin:
int a = 0x12345678;
Intel işlemcilerinde bu int 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" denilmektedir. Sayının yüksek anlamlı byte değerinin düşük adreste tutulmasına ise
"big endian" denir. 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ı olduğunu 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 organiasyon bakımından farklı olan bir veri yapısıdır. Bir birlik union anatar sözcüğü kullanılarak
aşağıdaki genel bbiç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.
Bir union türünden nesnelerin tanımlanması da benzer biçimde yapılmaktadır. Birlik türleri "union" anahtar sözcüğü ile birlik isminden oluşmaktadır.
Ö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 ilkdeğer verilebilir. Ancak birden fazla elemana yine ilk değer verilemez.z
Ö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ği her elemanı aynı adresten itibaren çakışık bir biçimde yerleştirilir. Bu nedenle bir birlik
nesnesi için birliği en büyük elemanı kadar yer ayrılmaktadı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("%zd\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 "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
buunması yeterlidir. Pekiyi biz per nesnesi içerisindeki cinfo nesnesinin hangi elemanın dolu olduğunu nasıl anlayabiliriz? Genellikle 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 tutmal 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;
};
struct PERSON per = {"Ali Serce", 123, CINFO_EMAIL, {.email = "aslank@csystem.org"}};
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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 dw;
Burada DWORD isimli birliğin iki elemanı vardır: dword ve bytes. Bunlar çakışık bir biçimde yerleştirilecektir. O halde biz dw.dword elemanına 4 byte
yerleştirdiğimizde bunların parçalarını dw.bytes[0], dw.bytes[1], dw.bytes[2] ve dw.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 dw;
dw.dword = 0x12345678;
printf("%02x %02X %02X %02X\n", dw.bytes[0], dw.bytes[1], dw.bytes[2], dw.bytes[3]); /* 78 56 34 12 */
for (int i = 0; i < 4; ++i)
printf("%02X ", dw.bytes[i]);
printf("\n");
dw.bytes[0] = 0x10;
dw.bytes[1] = 0x20;
dw.bytes[2] = 0x30;
dw.bytes[3] = 0x40;
printf("%08lX\n", (unsigned long)dw.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 dw;
Burada DWORD dw 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 dw.dword elemanına değer atadığımızda onun parçalarını dw.bytes[0], dw.bytes[1], dw.bytes[2] ve
dw.bytes[3] 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 dw;
dw.dword = 0x12345678;
printf("%02x %02X %02X %02X\n", dw.bytes.b0, dw.bytes.b1, dw.bytes.b2, dw.bytes.b3); /* 78 56 34 12 */
dw.bytes.b0 = 0x10;
dw.bytes.b1 = 0x20;
dw.bytes.b2 = 0x30;
dw.bytes.b3 = 0x40;
printf("%08lx\n", (unsigned long)dw.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 özellikle
yapılar ve dizilerin ifade içerisinde o anda oluşturulmasına olanak sağlamak için dile eklenmiştir. Örneğin ekranda 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 görünüm oluşturacaktır. İşte bileşik sabitler bunun için düşünülmüştür.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
Bileşik sabit oluşturmanın genel biçimi şöyledir
(<tür>) { <eleman_listesi>}
Aslında işlem bir tür dönüştürme işlemi gibidir. Ancak daha önce gördüğümüz tür dönüştürme operatöründen farkı yanında küma 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ündür. Ö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.
Biz bir bileşik sabitin adresini hemen 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. Ö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:
pi = (int[]){10, 20, 30};
*pi = 100; /* geçerli */
Bileşik sabitler eğer global düzeyde oluşturulmuşsa statik ömürlü olurlar. Yani program sonlanana kadar yaşamaya devam ederler. eğer bir bloğun
içerisinde oluşturulmuşlarsa ömürleri oluşturuldukları yerden blok sonuna kadarki akış içerisinde devam eder. Yani adeta bileşik sabitler o noktada
isimsiz bir global değişkenin ya da yerel değişkenin yaratılması gibi işlem görmektedir.
----------------------------------------------------------------------------------------------------------------------*/
#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;
}
/*----------------------------------------------------------------------------------------------------------------------
Bugün kullandığımız pekk ç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 C'de değişkeni yeniden yaratılıp yok edilecek gibi bir semantik tanımanmıştır. Ancak yukarıda da belirttiğimiz gibi bugünkü mimarilerde
genellikle (her zaman değil) derleyiciler aslında bu nesnesi ana blokta yaratıp fonksiyon sonlanana kadar muhafaza ederler. Dolayısıyla aşağıdaki kodd
ekrana hep aynı adresin yazıldığına şaşırmayınız:
for (int i = 0; i < 10; ++i) {
int a = 10;
printf("%d\n", &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 bileşik sabitler her faaliyet alanı için toplamda bir kez yaratılmaktadır. Örneğin:
{
int *pi;
REPEAT:
pi = (int[]) {10, 20, 30};
...
goto REPEAT;
}
Burada aynı faaliyet alanı içerisinde goto deyimi ile yukarıya gidilmiştir. Ancak bu durum her goto işleminde yeni bir nesnenin yaratılcağı anlamına
gelmemktedir. Fajkat örneğin:
for (int i = 0; i < 10; ++i) {
pi = (int[]) {10, 20, 30};
...
}
Burada her defasında faaliyet alanı dışına çıkılmıştır. Tabii yukarıda da belirtildiği gibi derleyiciler genel olarak zaten iç bloktaki nesneler
optimizasyon amacıyla tekrar tekrar yaratım yapmamaktadır.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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şturğuna
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.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
& operatörü iki operand'lı araek bit operatörüdür. Sayıların karşılıklı bitlerini AND işlmeine 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;
}
/*----------------------------------------------------------------------------------------------------------------------
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.
Bir 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.)
Tüm btleri 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 ediilmektedir. (Bir sayının en düşük anlamlı bit 0 olmak üzere her bitinin bir pozisyon numarası vardır.)
----------------------------------------------------------------------------------------------------------------------*/
#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'layabiliriz? 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 sokulur.
0xEF değeri 1110 1111 biçimindedir.
----------------------------------------------------------------------------------------------------------------------*/
#include <stdio.h>
int main(void)
{
unsigned char a = 0x3F; /* 0011 1111 */
a = a & 0xEF;
printf("%02X\n", a); /* 0010 1111 = 0x2F */
return 0;
}
/*----------------------------------------------------------------------------------------------------------------------
70. Ders - 28/02/2023 - Salı
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
Bit AND ve OR 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. (Halbuli mantıksal AND ve OR operatörlerinde bunun bir garantisi yoktur.)
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ş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 deiğ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;
}
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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, OR ve 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)
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 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 edilmiştir. Ö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şı tarfın tersini verir. Pekiyi bir sayının diğer bitlerine dokunmadan onun belli bitini
tersi ile nasıl yer değiştirirsiniz? Bunun 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ısı 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 sokarsak 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 ö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 öteleyebilirdi. Bu durumda aynı işlem bir kez daha yapılmaktadı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 türü 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 çerisine en büyük pozitif sayı yerleştirilmiştir. Bu sayıyı sola bir a << 1 işlemi ile öteleyelim. Sola öteleme ikili çarpma anlamına geldiğine
göre sayıda bilgi kaybı oluşur.
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 yukarıda belirtildiği gibi yapılır. Yani tüm bitler bir sola kaydırılır,
en soldaki bit yok edilir. Sayı 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 bilgi kaybı 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şartli 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. Buıradan çı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. 1 ile yapılması aslında negatif bir sayının negatif olarak 2'ye
bölündüğü anlamına gelir. 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 şiş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ındna 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ç y 0 çıkar ya
da 1 çıkar.
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 bit 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 yüksek 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'tab 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 ~0 biçiminde yazsaydık bu duurmda ~0 ifadesindne 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);
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;
----------------------------------------------------------------------------------------------------------------------*/
#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öryle 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 bir 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 dönüştürmek uygun olur. Örneğin a nesnesinin adresini alıp
onusola 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 bir işlemleri bazı özel durumlarda gerekebilmektedir.
Ancak gerçek sayılar üzerinde genellikle böyle bişr 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 bir 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. Ancal 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ğı derleyileri 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 olnak üzere şöyle olduğunu biliyorsunuz:
[- iki üzeri (n - 1), + iki üzeri (n - 1) - 1]
Bit alanı elemanı unsigned int türündne 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çeirisndeki 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 ybu durumda o byteile sonraki byte içerisine konumlandırılabilir ya da sonraki byte'tan başlayarak konumlandırılabilir.
Bu konuda davranış derleyicileri yazanların istedğ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'ya 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ı çakışık (overlap) 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:
bbbc cccc
cccx xxxx
Ya da örneğin şöyle olabilir:
bbbx xxxx
cccc cccx
Buradaki x'ler 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 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:
bbbc cccc
cccc cbbb
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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 tutmakl isteyelim:
struct TIME {
int hour;
int minute;
int second;
};
Bu yapı muhtemele2 Windows ve UNIX/Linux sistemlerinde 12 byte yer kaplayacaktır. Halbuki aslında zamanını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 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ı kullanrak 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.
Bizim yapımız toplam 17 bir 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_ANSWRE 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 kıonudaki ö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.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
İç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.
s
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. s
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
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:
ış 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ıı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
ış 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"
ı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
...
#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 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çekeltirilebilir. Örneğin:
#if VERSION == 1
....
#elif VERSION == 2
...
#elif VERSION == 3
...
#endif
#elif kısmının yanında yine bir sabit ifadesinin olması gerektiğine dikkat ediniz. Her #elif kısmının #endif ile kapatılmadığına toplamda yalnızca bir tane
#endif kullanıldığına dikkat ediniz. #elseif 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.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
#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 edildiğinde sorun çıkmaması için" oluşturulan yapıya denilmektedir. Bir dosyanın ikinci kez include edilmesi her zmana olmasa da
genellikle soruna 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 bildirilmesi derleme sırasında error oluşmasına yol açacaktır.
#ifndef komutu ile include koruması şöyle yapılmaktadı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:
#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ıı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
gerek yoktur. Bunların önemli olanları şunlardır:
__FILE__: Bu makroyu önişlemci gördüğünde dosyanın ismini 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;
}
__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 bazı
C'deki ö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++ derleyici ile derlenmesini de isteyebilir. Ö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.
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
Yukarıda ele almış olduğumuz 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 şö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 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 fonksiyon çağrıldığında derleyici koda fonksiyon çağırma kodu (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 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
ı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
ım yapmaması durumunda herhangi bir uyarı vermezler. Programcı inline açımın yapılıp yapılmadığını ancak derleyicinin ürettiği kodu inceleyerek anlayabilmektedir.
Pekiyi derleyici neden fonksiyonu inline açmak istemeyebilmektedir? 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 eror olulacaktır. Örneğin biz gcc derleyicisinde 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 programo -O2 seçeneği ile derlersek derleyici 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 yazmamaktadır.
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 yazöayabilir. Ancak inline açımı yapamazsa static
düzeyde fonksiyonu amaç dosyaya yazacaktır. Tabii fonksiyon static olduğu için fonksiyonun 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 yazmaktadır. Bu durumun dezavatajı şudur: Biz extern inline fonksiyonu birden fazla modülde kullanırsak
ve bu fonksiyon inline açılmazsa fonksiyonun 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 ayapamazsa 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ı naısl 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 derleyic tarafındna görülmek zorundadır. Dolayısıyla bu fonksiyonlar kütüphanelere yerleştirilemezler.
Birden fazla modülle çalışırken biz inline fonksiyonları ortak bir başlık dosyasına yerleştirebiliriz. Ö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.
----------------------------------------------------------------------------------------------------------------------------------*/
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/
/*----------------------------------------------------------------------------------------------------------------------
----------------------------------------------------------------------------------------------------------------------*/