28374 lines
1.2 MiB
28374 lines
1.2 MiB
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
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: 31/03/2025 - Pazartesi
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C Programlama Dili 1971-1972 yıllarında Dennis Ritchie tarafından AT&T Bell Lab'ta UNIX işletim sisteminin bir yan
|
||
ürünü olarak geliştirilmişir. AT&T Bell Lab. Ken Thompson, Brian Kernighan gibi önemli kişilerle UNIX isimli işletim
|
||
sistemini geliştiriyordu. UNIX işletim sistemi o zamanın DEC PDP-8 makineleri için yazılıyordu. İlk UNIX sistemleri
|
||
ağırlıklı sembolik makine dilinde yazılmıştır. Ancak yazımı kolaylaştırmak için Ken Thompson'ın B ismini verdiği
|
||
programlama dilinden (dilciğinen de diyebiliriz) de faydalanılmıştır. İşte Dennis Ritchie bu B programlama Dilini
|
||
geliştirerek C haline getirmiştir. UNIX işletim sistemi 1973 yılında tamamen C kullanılarak yeniden yazılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
1978 yılında Dennis Ricthie ve Brian Kernighan C'yi tanıtan "The C Programming Laanguage" isimli bir kitap yazdılar.
|
||
Daha sonra bu kitabın 1987 yılında 2. Baskısı (Second Edition) oluşturuldu. Nihayet 1989 yılında C Programlama Dili
|
||
ANSI (American National Standard Institute) tarafıında standardize edildi. C'nin ANSI standartları "ANSI C" olarak
|
||
bilinmektedir ve kısaca bu standartlara C89 denilmektedir. C'de 1989 öncesi devire "standart öncesi devir" denir.
|
||
1990 yılında ISO kurumu C'nin ANSI standratlarını alarak bölüm numaralandırmalarını değiştirip tasdik etmiştir. Bu
|
||
standartlar "ANSI/ISO 9899:1990" kod numarasıyla basılmıştır. Bu standarda halk arasında C90 denilmektedir. C Programlama
|
||
Dili 90'lı yıllarda dünyanın en popüler ve yaygın programlama dili haline gelmiştir. ISO 1999 yılında C'ya bazı kurallar
|
||
ekleyerek yeni bir standart oluşturdu. Bu standartlar da "ISO/IEC 9899:1999" kod numarasını verdi. Bu standartlar da
|
||
halk arasında kısaca C99 olarak ifade edilmektedir. Daha sonra ISO yine C'ya bazı eklemeler yaparak 2011 standartlarını
|
||
oluşturdu. Bu standartların kod numarası "ISO/IEC 9899:2011" biçimindedir. Bu standartlar da kalk arasında C11 olarak
|
||
ifade edilmektedir. Nihayet ISO 2017 yılında yeni bir standart oluşturdu. Ancak bu standart C11'in bir düzeltmesi
|
||
biçimindedir. Şu anda C'nin en son standardı "ISO/IEC 9899:2017" kod numarasıyla tasdik edilen biçimidir. Bu da halk
|
||
arasında C17 olarak ifade edilmektedir. C'nin son standardı olan C23 üzerinde çalışılmaktadır. Halen tam olarak tasdik
|
||
edilmemiştir. Bu standartlar 2023 yılında basılamadığı için artık C2Y olarak isimlendirilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C++ Programlama Dİli C Programlama Dİlinin nesne yönelimli bir biçimi olarak düşünülebilir. Tamamen olmasa da C++ C'yi
|
||
kapsamaktadır ve fazlalıkları vardır. C++'a bu fazlalıklar "Nesne Yönelimli Programlama Tekniğini (NYPT)" uygulamak
|
||
için eklenmiştir. Derneğimizde C++ Programlama Dilinin eğitimi C bilenlere yönelik olarak verilmektedir.
|
||
|
||
C++ Programlama Dilinin standart gelişimi de şöyledir:
|
||
|
||
- ISO/IEC 14882: 1998 (C++98)
|
||
- ISO/IEC 14882 :2003 (C++03)
|
||
- ISO/IEC 14882 :2011 (C++11)
|
||
- ISO/IEC 14882 :2014 (C++14)
|
||
- ISO/IEC 14882 :2017 (C++17)
|
||
- ISO/IEC 14882 :2020 (C++20)
|
||
- ISO/IEC 14882 :2020 (C++23 Henüz basılmadı)
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bilgisayar donanımını yöneten, donanım ile kullanıcı arasında arayüz oluşturan temel sistem programlarına "işletim
|
||
sistemi (operating system)" denilmektedir. İşletim sistemleri iki katman olarak düşünülebilir. Çekirdek (kernel)
|
||
işletim sisteminin donanımı yöneten motor kısmıdır. Kabuk (shell) ise kullanıcı ile arayüz oluşturan kısmıdır. İşletim
|
||
sistemleri çeşitli bakımlardan sınıflandırılabilir. Örneğin:
|
||
|
||
- Tek prosesli işletim sistemleri
|
||
- Çok prosesli işletim sistemleri
|
||
- Gerçek zamanlı işletim sistemleri
|
||
- Masaüstü (desktop) ve mobil işletim sistemleri
|
||
- Server işletim sistemleri
|
||
|
||
Bugün masaüstü işletim sistemlerinin en yaygın kullanılanı Windows sistemleridir (75 civarı). Bunu macOS izlemektedir
|
||
(22 civarı) bunu da Linux izlemektedir. (%2.5 civarı). Mobil işletim sistemlerinin en yaygın olanı ise %70 civarında
|
||
kullanıma sahip olan Android sistemleridir. Bunu %25 civarlarında kullanıma sahip olan IOS sistemleri izlemektedir.
|
||
Diğer mobil işletim sistemleri %1'in oldukça altındadır.
|
||
|
||
Linux sistemleri Server dünyasında en yaygın kullanılan işletim sistemleridir. Server sistemlerinin %70 civarı Linux
|
||
makinelerden oluşmaktadır. Artık pek gömülü sistem projelerinde de Linux işletim sistemi kullanılmaktadır.
|
||
|
||
Bazı işletim sistemleri sıfırdan yazılmıştır. Bazı işletim sistemleri ise mevcut işletim sistemlerinin kodlarıdan
|
||
faydalanılarak oluşturulmuştur. Örneğin Android büyük ölçüde Linux çekirdeğinin kodlarına sahiptir. Windows, Linux
|
||
özgün kod temeline sahip olan işletim sistemleridir. Eskiden BSD sistemleri özgün değildi. Sonra tamamen sıfırdan
|
||
yeniden yazıldı. Benzer biçimde Solaris gibi sistemlerde sıfırdan yazılmıştır. Anak macOS sistemleri böyle değildir.
|
||
macOS sistemleri FreeBSD ve Mach isimli çekirdeklerin birleşimiyle oluşturulmuştur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir bilgisayar sisteminde en önemli üç birim "CPU", "RAM" ve "Disk" birimleridir. Bütün işlemler CPU (Central Processing
|
||
Unit) tarafından yapılır. CPU işlemlerin yapıldığı kavramsal birimdir. Bunun entegre devre biçiminde üretilmiş haline
|
||
"mikroişlemci (microprocessor)" denilmektedir. CPU'nun elektiriksel olarak bağlantılı olduğu belleklere "ana bellek
|
||
(main memory)", ya da "Birincil bellek (primary memory)" denilmektedir. Ana belleklere ise halk arasında RAM denilmektedir.
|
||
Programlama dillerindeki değişkenler program çalışırken RAM'de bulunurlar. Ancak işlemler CPU tarafından yapılır.
|
||
Örneğin:
|
||
|
||
a = b + c;
|
||
|
||
gibi bir işlemde aslında a, b, ve c RAM'de bulunmaktadır. Bu işlem yapılırken b ve c CPU'ya çekilir. CPU içerisindeki
|
||
elektrik devreleri toplama işlemini yapar. Sonuç RAM'deki a'ya aktarılır. Bilgisayarın güç kaynağı kapatıldığında
|
||
RAM'deki bilgiler silinmektedir. Bunun için bu bilgilerin daha kalıcı bir bellekte saklanması gerekir. Bu tür 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 dillerin daha uygun
|
||
bir araç olacağı" ile ilgilidir. Bu bakımdan pek çok alt sınıflandırma yapılabilmektedir. Aşağıda birkaç önemli alt
|
||
sınıf verilmiştir:
|
||
|
||
- Bilimsel ve Mühendislik Diller (Scientific and Enginnering Languages): Fortran, C, C++, Pascal, Java, C#, ...
|
||
- Veritabanı Dilleri (Database Languages): SQL, Clipper, ...
|
||
- Web Dilleri (Web Languages): Java Script, PHP, Ruby, Java, C#, Python, ...
|
||
- Yapay Zeka Dilleri (Artificial Intelligence Langauges): Lisp, Prolog, Python, C, C++,...
|
||
- Görsel ve Animasyon Dilleri (Visual and Animation Languages): Action Script, ...
|
||
- Sistem Proramlama Dilleri (System Programming Languages): C, C++, Sembolik Makine Dilleri, Rust, Go
|
||
- Genel Amaçlı Diller (General Purpose Languages): C, C++, Java, C#, Pascal, ...
|
||
|
||
C Programlama Dİli bilimsel ve mühendislik alanlarda kullanılan, genel amaçlı, uzmanlığı sistem programlama olan bir
|
||
dildir.
|
||
|
||
Sistem programlama "bilgisayar donanımı ile arayüz oluşturan, uygulama programlarına çeşitli bakımlardan hizmet veren,
|
||
aşağı seviyeli temel yazılımların oluşturulması amacıyla yapılan programlama faaliyetlerine denilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Programlama modeli (programming paradigm) programlama yaparken kullandığımız genel yöntemleri ve biçimleri anlatan bir
|
||
kavramdır. Programlama dilleri belli programlama modellerini uygulayabilmek için özel tasarlanmıştır. Bu bakımdan dilleri
|
||
tipik olarak aşağıdaki gibi sınıflara ayırabiliriz:
|
||
|
||
- Prosedürel Diller (Procedural Languages): Bu dillerde programlar altprogramların (bunlara fonksiyon, prosedür ya da
|
||
"subroutine" de denilmektedir) birbirlerini çağırması ile oluşturulmaktadır. Fortran, C, Basic, Pascal gibi 90 öncesi
|
||
klasik programlama dillerinin büyük bölümü böyledir.
|
||
|
||
- Nesne Yönelimli ve Nesne Tabanlı Diller (Object Oriented Languages): İçerisinde sınıf kavramı geçen diller. Bunlar
|
||
programların sınıflar kullanılarak yazılmabilmesine olanak sağlamaktadır.
|
||
|
||
- Fonksiyonel Diller (Functional Languages): Bunlar adeta "formül yazar gibi program yazmaya olanak sağlayan" dillerdir.
|
||
Aslında bu diller de kendi aralarında bir spektrum oluşturmaktadır. Fonksiyonel dil demekle genellikle yüksek oranda
|
||
fonksiyonal olan (pure functional) diller kastedilmektedir.
|
||
|
||
- Imperative Diller (Imperative Languages): Programların deyim deyim çalıştırıldığı mantıksal, görsel ve fonksiyonel
|
||
dillerin dışındaki diller genel olarak imperative diller olarak da bilinmektedir.
|
||
|
||
- Mantıksal Diller (Logical Languages): Mantıksal ifadelerin ve sonuç çıkartma işlemlerinin yoğun kullanıldığı dillerdir.
|
||
Lisp ve Prolog gibi.
|
||
|
||
- Görsel Diller (Visual Languages): Fare hareketleriyle tamamne görsel biçimde program akışının oluşturulduğu çok
|
||
yüksek seviyeli dillerdir. Programlama eğitiminde kullanılan Scratch buna örnek verilebilir.
|
||
|
||
- Çok Modelli Diller(Multiparadigm Languages): Yukarıdaki modellerden birden fazlasını belli ölçülerde destekleyen
|
||
dillerdir. Örneğin C++'ta biz C gibi kod yazabiliriz. Ama sınıflar kullanarak da kod yazabiliriz. Fonksiyonel bazı
|
||
dil özelliklerini C++11'den sonra C++'ta kullanabilmekteyiz. O zaman C++ çok modelli bir programlama dilidir. Yeni
|
||
tasarlanan diller zaten genel olarak çok modelli olma eğilimindedir. Bu yeni diller hem nesne yönelimli hem de
|
||
fonksiyonel özellikleri bünyesinde barındırmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C Programalama Dili orta seviyeli, prosedürel, imperative, genel amaçlı, bilimsel ve mühendislik çalışmalarda kullanılan
|
||
ancak uzmanlık alanı "sistem programlama" olan bir dildir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Uygulama geliştirmeyi kolaylaştıran, kendi içerisinde editörü olan, menüleri olan, genellikle debugger'ları olan, başka
|
||
birtakım araçları bulunan yazılımlara "IDE (Integrated Developmen Environment)" denilmektedir. IDE derleyici değildir.
|
||
IDE derleyiciyi barındırmaz. IDE'de derleme işlemi yapılırken IDE derleyiciyi dışarıdan çalıştırmaktadır. Derleyiciler
|
||
genel olarak komut satırında çalışan GUI arayüzü olmayan araçlar biçiminde oluşturulmaktadır:
|
||
|
||
C için kullanılabilecek önemli IDE'ler şunlardır:
|
||
|
||
- Windows sistemleri için Microsoft'un "Visual Studio" IDE'si. Kursumuzda bunun "Community-2022" parasız versiyonunu
|
||
kullanacağız.
|
||
|
||
- QtCreator IDE'si. Cross platform bir IDE'dir. Aslında "Qt" denilen framework için yazılmıştır. Ancak genel amaçlı
|
||
C/C++ IDE'si olarak da kullanılabilmektedir. macOS ve Linux sistemlerinde iyi bir alternatiftir.
|
||
|
||
- Eclipse IDE'si. Bu IDE bir Java IDE'si olarak çıkmıştı. Ondan sonra pek çok programlama dili için "plugin" yöntemiyle
|
||
kullanılabilmeye başlandı. Bu IDE'nin C/C++ versiyonu doğrudan indirilebilir.
|
||
|
||
- CLion IDE'si. Bu JetBrains firmasının C/C++ IDE'sidir. Ancak paralıdır ve community versiyonu yoktur.
|
||
|
||
- Visual Studio Code IDE'si Aslında bu yazılım IDE ile editör arasında bir yerdedir. Ancak cross platform özelliği
|
||
vardır. Kullanımı biraz dah zahmetli olsa da çok az yer kaplamaktadır (lightweight IDE). Plug-in'lerle (extensions)
|
||
işlevleri genişletilebilmektedir.
|
||
|
||
- XCode IDE'si. Bu IDE Apple firmasının temel IDE'sidir. Dolayısıyla adeta Visual Studio IDE'sinin Apple versiyonu
|
||
gibi düşünülebilir. Her ne kadar Apple'ın temel dili Swift ve Objective C olsa da XCode C/C++ çalışmasını
|
||
desteklemektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
5. Ders - 07/06/2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Biz 10'luk sistemi (decimal system) kullanmaktayız. 10'luk sistemde sayıları ifade etmek için 10 sembol vardır:
|
||
|
||
0
|
||
1
|
||
2
|
||
3
|
||
4
|
||
5
|
||
6
|
||
7
|
||
8
|
||
9
|
||
|
||
10'luk sistemde sayının her bir basamağı 10'nun kuvvetleriyle çarpılıp toplanmaktadır. Örneğin:
|
||
|
||
123.25 = 3 * 10^0 + 2 * 10^1 + 1 * 10^2 + 2 * 10^-1 + 5 * 10^-2
|
||
|
||
Halbuki bilgisayarlar 'lik sistemi (binary system) kullanmaktadır. ikilik sistemde sayıları ifade etmek için 2 sembol
|
||
kullanılmaktadır:
|
||
|
||
0
|
||
1
|
||
|
||
ikilik sistemde sayının her bir basamağına "bit ("binary digit" sözcüklerinden kısaltma)" denilmektedir. ikilik sistemde
|
||
sayının her basamağı 2'nin kuvvetiyle çarpılarak sayı elde edilir. Bit en küçük bellek birimidir. 8 bite 1 byte denilmektedir.
|
||
Genellikle bitler 4'erli gruplanarak yazılırlar. Örneğin:
|
||
|
||
1010 0010
|
||
|
||
Burada 1 byte'lık bir bilgi vardır. Byte temel bellek birimidir.
|
||
|
||
Byte da küçük bir birimdir. Kilo diğer bilimlerde "1000 katı" anlamına gelmektedir. Ancak bilgisayarlar ikilik sistemi
|
||
kullandığj için 1000 katı iyi bir kat değildir. Bu nedenle genellikle olarak Kilo byte için 2'nin 10'uncu kuvveti olan
|
||
1024 kat kullanılır. Yani 1KB (kısaca 1K) 1024 byte'tır. Mega diğer bilimlerde kilonun 1000 katıdır. Dolayısıyla milyar
|
||
kat anlamına gelmektedir. Ancak bilgisayar bilimlerinde genellikle mega kilonun 1024 katı olarak alınır. Bu durumda
|
||
1 MB = 1020 * 1024 (2^20) KB'dir. Giga ise meganın 1024 katıdır. Bu durumda 1 GB = 1024 * 1024 * 1024 byte'tır ( 2^30).
|
||
Giga'dan sonra tera, tera'dan sonra peta, ondan sonra da exa gelmektedir. Bazı kaynaklar kilo, mega ve giga byte
|
||
terimlerini diğer bilimlerde olduğu gibi 10^3, 10^6 ve 10^9 biçiminde kullanmaktadır. Bu konudaki karışıklık daha
|
||
sonraları "International System of Units (SI)" tarafından giderilmeye çalışılmıştır. SI'ye göre artık 1 Kilobyte 1000
|
||
byte, 1 Kikibyte ise 1024 byte'tır. Ancak Microsoft gibi bazı kurumlar Kilobyte tanımını 1024 olarak kullanmaya devam
|
||
etmektedir.
|
||
|
||
1 byte içerisinde yazılabilecek en küçük ve en büyük sayılar şöyledir:
|
||
|
||
0000 0000 ---> 0
|
||
1111 1111 ---> 255
|
||
|
||
1 byte içerisinde 1 ve 0'ların bütün permütasyonları 256 tanedir. 2 byte içerisinde en büyük sayıyı yazacak olsak şöyle
|
||
olurdu:
|
||
|
||
1111 1111 1111 1111 ---> 65535
|
||
|
||
Biz burada ikilik sistemde tamsayıları ifade ettik. Ama bütün sayıları pozitif kabul ettik. Pekiyi negatif tamsayılar
|
||
nasıl ifade edilmektedir?
|
||
|
||
Bugün negatif sayıların ifade edilmesi için "ikiye tümleyen (two's complement)" sistemi denilen bir sistem kullanılmaktadır.
|
||
Bu sistemde pozitif ve negatif sayılar birbirlerinin ikiye tümleyenidirler. ikiye tümleyen bire tümleyene bir eklenerek
|
||
elde edilmektedir. Bir sayının bire tümleyeni sayıdaki 0'ların 1, 1'lerin 0 yapılmasıyla elde edilir. Bu durumda ikiye
|
||
tümleyen şöyle hesaplanır. Örneğin aşağıdaki sayının ikiye tümleyenini bulmaya çalışalım:
|
||
|
||
0101 0110
|
||
|
||
Sayının bire tümleyenine bir ekleyeceğiz. Sayının bire tümöleyeni şöyledir:
|
||
|
||
1010 1001
|
||
|
||
Şimdi ona 1 ekleyelim:
|
||
|
||
1010 1001
|
||
0000 0001
|
||
---------
|
||
1010 1010
|
||
|
||
Aslında ikiye tümleyeni bulmanın kolay bir yolu da vardır: Sayıda sağdan sola ilk 1 görene ilk 1 dahil olmak üzere aynısı
|
||
yazılarak ilerlenir. Sonra 0'lar 1, 1'ler 0 yapılarak devam edilir. Örneğin:
|
||
|
||
0101 0110
|
||
|
||
sayının ikiye tümleyenini tek hamlede bulalım:
|
||
|
||
10101010
|
||
|
||
Negatif sayıları ifade edebilmek için kullanılan ikiye tümleme sisteminde en soldaki bir işaret bitidir. Bu bit 0 ise sayı
|
||
pozitif, 1 ise negatiftir. Negatif ve pozitif sayılar birbirlerinin ikiye tümleyenidir. Örneğin bu sistemde +10 yazmak
|
||
isteyelim. Bunu işaret bitini 0 yaparak yazabiliriz:
|
||
|
||
0 000 1010 ---> +10
|
||
|
||
Şimdi -10 yazmak isteyelim. Bunun için +10'un ikiye tümleyenini alalım:
|
||
|
||
1 111 0110 ---> -10
|
||
|
||
Bu sistemde +n ile -n toplandığında 0 elde edilir:
|
||
|
||
0 000 1010 ---> +10
|
||
1 111 0110 ---> -10
|
||
---------------------
|
||
0 000 0000 ---> 0
|
||
|
||
Bu sistemde tek bir sıfır vardır. O da tüm bitleri 0 olan sıfırdır. Bu sistemde 1 byte içerisinde yazılabilecek en büyük
|
||
pozitif sayı şöyledir:
|
||
|
||
0 111 1111 ---> +127
|
||
|
||
Şimdi bunun ikiye tümleyenini alalım:
|
||
|
||
1 000 0001 ---> -127
|
||
|
||
Pekiyi bu sistemde en küçük negatif sayı nedir? Bu sistemde bir tane sıfır olduğuna göre 255 tane permütasyon eşit bölünemez.
|
||
Demek ki ya pozitif sayılar ya negatif sayılar bir tane daha fazla olmak zorundadır. Bu sistemde ikiye tümleyeni olmayan iki
|
||
sayı vardır:
|
||
|
||
0000 0000
|
||
1000 0000
|
||
|
||
Birincisi 0'dır. İkinci sayı -127'den bir eksik olan sayıdır. O halde bu sayının -128 kabul edilmesi daha uygundur.
|
||
|
||
Demek ki bu sistemde n byte içerisinde yazılabilecek en büyük pozitif sayı ilk biti 0 olan diğer tüm birleri 1 olan sayıdır.
|
||
En küçük negatif sayı ise ilk biti 1 olan diğer tüm bitleri 0 olan sayıdır. Örneğin bu sistemde iki byte ile yazabileceğimiz
|
||
en büyük pozitif sayı şöyledir:
|
||
|
||
0111 1111 1111 1111 ---> +32767
|
||
|
||
En küçük negatif sayı ise şöyledir:
|
||
|
||
1000 0000 0000 000 ---> -32768
|
||
|
||
Bu sisteme ilişkin tipik sorular ve yanıtları şöyledir:
|
||
|
||
SORU: Bu sistemde +n sayısını nasıl yazarsınız?
|
||
YANIT: En soldaki bit 0 yapılıp n sayısı ikilik sistemde yazılır.
|
||
|
||
SORU: Bu sistemde -n nasıl yazarsınız?
|
||
CEVAP: Yazabiliyorsanız doğrudan yazın. Ancak doğrudan yazamıyorsanız önce +n değerini yazın ve ikiye tümleyenini alın.
|
||
Örneğin bu sistemde -1 yazalım. Önce +1 yazalım:
|
||
|
||
0000 0001 ---> +1
|
||
|
||
Şimdi bunun ikiye tümleyenini alalım:
|
||
|
||
1111 1111 ----> -1
|
||
|
||
SORU: Bu sistemde bir sayının kaç olduğu bize sorulsa bunu nasıl yanıtlarız?
|
||
CEVAP: Eğer en soldaki bit 0 ise sayının değeri doğrudan hesplanır. Eğer en soldaki bit 1 ise bu sayının negatif olduğunu
|
||
gösterir. Bu durumda sayının ikiye tümleyeni alınır. Pozitifinden hareketle negatifi bulunur. Örneğin 1110 1110 sayısı
|
||
kaçtır? Burada işaret biti 1 olduğuna göre sayı negatiftir. Negatif ve pozitif sayılar birbirlerinin ikiye tümleyenidirler.
|
||
O zaman bu sayının ikiye tümleyenini alıp pozitifinden faydalanarak sayıyı bulalım:
|
||
|
||
0001 0010 ---> +18
|
||
|
||
o zaman bize sorulan sayı -18'dir.
|
||
|
||
Bu sistemde örneğin 1 byte içerisinde yazılabilecek en büyük pozitif sayıya 1 toplayalım:
|
||
|
||
0111 1111 ---> +127
|
||
1000 0000 ---> -128
|
||
|
||
Demek ki bu sistemde bir sayıyı üst limitten taşırırsak yüksek bir negatif sayıyla karışılaırız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tamsayılar ikilik sistemde "işaretsiz (unsigned)" ya da "işaretli (signed)" sistemde yorumlanabilirler. İşaretsiz
|
||
sistemde sayının en soldaki biti olarak yorumlanmaz. Sayı herzaman sıfır ya da pozitiftir. İşaretli sistemde ise
|
||
sayının en solundaki bit işaret bitidir. Sayı ikiye tümleyen aritmetiğine göre yorumlanır.
|
||
|
||
Pekiyi sayının işaretli mi işaretsiz mi olduğuna nasıl karar verilmektedir? Programcı sayıyı tutacağı değişkeni C'de
|
||
işaretli ya da işaretsiz tamsayı türü olarak belirleyebilir. İşlemciler aslında genellikle işaretli ve işaretsiz
|
||
ayırımını yapmazlar. Çünkü bu tür de aslında aynı biçimde işleme sokulmaktadır. Sonucun yorumu değişmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi noktalı sayılar ikilik sistemde nasıl ifade edilmektedir? İşte insanlar noktalı sayıları ifade etmek için iki
|
||
format geliştirmişlerdir. Bunlardan birine "sabit noktalı formatlar (fixed point formats)" diğerine "kayan noktalı
|
||
formatlar (floating point formats)" denilmektedir. Sabit noktalı formatlar eski devirlerde basit bir mantıkla tasarlanmıştır.
|
||
Bu formatlar bugün hala kullanılıyor olsa da büyük ölçüde artık bunların çağı kapanmıştır. Bugün kayan noktalı format
|
||
denilen formatlar kullanılmaktadır.
|
||
|
||
Sabit noktalı formatlarda noktalı sayı için n byte yer ayrılır. Noktanın yeri önceden bellidir. Örneğin sayı 4 byte
|
||
ile ifade edilsin. Noktanın yeri de tam ortada olsun. Bu durumda syının tam kısmı 2 byte ile noktalı kısmı 2 byte ile
|
||
ifade edilir. Ancak sayının noktalı kısmı 2'nin negatif kuvvetleriyle kodlanmaktadır. VBöylece iki sabit noktalı sayıyı
|
||
paralel toplayıcılarla kolay bir biçimde toplayabiliriz: Örneğin bu sistemde 5.25 ile 6.25 sayılarını ifade edip toplayalım:
|
||
|
||
0000 0000 0000 0101 . 0100 0000 0000 0000 ---> 5.25
|
||
0000 0000 0000 0110 . 0100 0000 0000 0000 ---> 6.25
|
||
-------------------------------------------------------
|
||
0000 0000 0000 1011 . 1000 0000 0000 0000 ---> 11.5
|
||
|
||
Pekiyi bu yöntemin ne dezavantajı vardır? Yöntemin en önemli dezavantajı dinamik olmamasıdır. Yani sayının tam kısmı
|
||
düşükse tam kısım için gereksiz bir yer ayrılmış olur. Benzer biçimde sayının noktadan sonraki kısmı düşükse noktadan
|
||
sonrakşi kısım için gereksiz bir biçimde yer ayrılmış olur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
6. Ders - 09/06/2022
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Sabit noktalı formatların dinamik olmaması nedeniyle kayan noktalşı formatlar geliştirilmiştir. Bu formatlarda noktanın
|
||
yeri sabit değildir. Noktanın yeri format içerisinde ayrıca tutulmaktadır. Noktalı sayının noktası yokmuş gibi ifade
|
||
edilmesi durumunda sayının bu haline "mantis (mantissa)" denilmektedir. İşte kayan formatlarda sayı için ayrılan alanın
|
||
bir bölümünde mantis bir bölümünde de "noktanın yeri" tutulmaktadır. Noktanın yerini belirleyen kısma "üstel kısım
|
||
(exponential part)" denilmektedir. Tabii bir de sayının başında işaret biti bulunur. Bu durumda kayan noktalı bir
|
||
sayının format aşağıdakine benzerdir:
|
||
|
||
[işaret biti] [mantis] [noktanın yeri (exponential)]
|
||
|
||
Bugün ağırlıklı kullanılan kayan noktalı format IEEE 754 denilen formattır. Bu formatın üç farklı genişlikte biçimi
|
||
vardır:
|
||
|
||
IEEE 754 - Short Real Format (4 byte)
|
||
IEEE 754 - Long Real Format (8 byte)
|
||
IEEE 754 - Extended Real Format (10 byte)
|
||
|
||
Bugün Intel, ARM, MIPS, Alpha, Power PC gibi yaygın işlemciler donanımsal olarak bu formatı desteklemektedir. Aynı
|
||
zamanda bu format yaygın olarak Reel Sayı Ünitesi olmayan mikrodenetleyicilerdeki derleyiciler tarafından da kullanılmaktadır.
|
||
|
||
Kayan noktalı formatların (örneğin IEEE 754 formatının) en ilginç ve problemli tarafı "yuvarlama hatası (rounding error)"
|
||
denilen durumdur. Yuvarlama hatası noktalı sayının tam olarak ifade edilemeyip onun yerine ona yakın bir sayının ifade
|
||
edilmesiyle oluşan hatadır. Yuvarlama hatası sayıyı ilk kez depolarken de oluşabilir, aritmetik işlemlerin sonucunda
|
||
da oluşabilir. Tabii noktalı sayıların bir bölümü bu formatta hiçbir yuvarlama hatasına maruz kalmadan ifade edilebilmektedir.
|
||
Ancak bazı sayılarda bu hata oluşabilmektedir. Bu hatayı ortadan kaldırmanın yolu yoktur. Tabii sayı için daha fazla
|
||
alan ayrılırsa yuvarlama hatasının etkisi de azalacaktır.
|
||
|
||
Yuvarlama hatalarından dolayı programlama dillerinde iki noktalı sayının tam eşitliğinin karşılaştırılması anlamlı
|
||
değildir. Örneğin aşağıdaki işlemde yuvarlama hatasından dolayı sayılar sanki eşit değişmiş gibi ele alınacaktır.
|
||
|
||
0.3 - 0.1 == 0.2 (false)
|
||
|
||
Pekiyi yuvarlama hatasının önemli olduüu ve bunun istenmediği tarzda uygulamalarda (örneğin finansal uygulamalarda,
|
||
bilimsel birtakım uygulamalarda) ne yapak gerekir? İşte bunun tek yolu noktalı sayıları kayan noktalı formatta tutmamak
|
||
olabilir. Bazı programlama dillerinde noktalı sayıyı kayan noktalı formatta tutmayan böylece yuvarlama hatalarına
|
||
maruz bırkmayan özel türler (örneğin C#'taki decimal) vardır. Ancak bu türler işlemciler tarafından desteklenmediği
|
||
için yapay türlerdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yazılar da aslında bilgisayar belleğinde ikilik sistemde sayılar biçiminde tutulmaktadır. Bir yazıyı oluşturan elemanlara
|
||
"karakter" denilmektedir. İşte bir yazıda her bir karakter ikilik sistemde bir sayı ile ifade edilir. Böylece yazı
|
||
aslında ikilik sistemde bir sayı dizisi gibi tutulmaktadır. İşte bir karakter için hangi sayının karşı geldiğini belirten
|
||
tablolara "karakter tabloları" denilmektedir. Karakter tablosundaki karakter şekillerine "glyph" denilmektedir. Her
|
||
karaktere tabloda bir sıra numarası verilmiştir. Buna da "code point" denilmektedir. Dünyanın ilk standart karakter
|
||
tablosu "ASCII (American Standard Code Information Interchange)" denilen tablodur. ASCII tablosu aslında 7 bit bir
|
||
tablodur. Dolayısıyla tabloda 128 tane glyph için code point bulundurulmuştur. ASCII dışında IBM EBCDIC tablosunu
|
||
geliştirmiştir. Wang firması WISCII tablosunu kullanmıştır. ASCII tablosu Amerikalılar tarafından yalnızca İngilizce
|
||
karakterleri ifade etmek için oluşturulmuştur. Bilgisayarlar yaygınlaşmaya başladığında farklı karakterlere sahip
|
||
olan Türkiye gibi, Yunanistan gibi, Almanya gibi ülkeler bu ASCII tablosunu 8 bite çıkartıp elde edilen 128'lik yeni
|
||
alanı kendi karakterlerini ifade etmek için kullanmışlardır. ASCII tablosunun ilk yarısı (yani [0, 128] numaraları
|
||
karakterleri) standarttır. Ancak ikinci yarısı "code page" adı altında farklı ülkeler tarafından farklı yerleşimler
|
||
yapılarak kullanılmaktadır. DOS zamanlarında Türkçe karakterler için OEM 857 denilen code page kullanılıyordu. Daha
|
||
sonra Microsoft Windows sistemlerinde Türkçe karakterler için 1254 code page'i düzenledi. ISO bu code page'leri standart
|
||
hale getirmiştir. Bugün Türkçe karakterler ISO tarafından ASCII 8859-9 Code page'i ile düzenlenmiştir.
|
||
|
||
ASCII tablosu ve onların code page'leri uzun süre kullanılmış ve hala kullanılmakta olsa da maalesef karışıklıklara
|
||
yol açmaktadır. İşte son 20 yıldır artık karakterleri 2 byte içerisinde ifade ederek dünyanın bütün dillerinin ve ortak
|
||
sembollerinin tek bir tabloya yerleştirilmesi ile ismine Unicode denilen bir tablo oluşturulmuştur (www-Unicode.org).
|
||
Unicode tablo ISO tarafından 10646 ismiyle de bazı farklılıklarla standardize edilmiştir. Unicode tablonun
|
||
ilk 128 karakteri standart ASCII karakterleri, ikinci 128 karakteri ISO 8859-9 code page'indeki karakterlerdir.
|
||
|
||
Bir karakter tablosundaki code point'lerin ikilik sistemde ifade edilme biçimine "encoding" denilmektedir. ASCII code
|
||
page'lerinde encoding doğrudan code point'in 1 byte'lık sayı karşılığıdır. Ancak Unicode tablonun değişik encoding'leri
|
||
kullanılmaktadır. Unicode tablonun klasik encoding'i UTF-16'dır. Burada code point doğrudan 16 bir bir sayı biçiminde
|
||
ifade edilir. UTF-32 encoding'inde ise code point 32 bitlik bir sayı biçiminde ifade edilmektedir. (Unicode tablodaki
|
||
karakterler için başlangıçta 2 byte yetiyordu. Ancak daha sonraları tabloya pek çok karakter eklendi ve 2 byte yetmez
|
||
hale geldi.) Ancak Unicode tablonun en yaygın kullanılan encoding'i UTF-8 encoding'idir. UTF-8 kodlamasında standart
|
||
ASCII karakterler 1 byte ile, diğer karakterler 2 byte, 3 byte, 4 byte ve 5 byte kodlanabilmekedir. Türkçe karakterler
|
||
UTF-8 encoding'inde 2 byte yer kaplamaktadr. UTF-8 encoding'i Unicode bir yazının adeta sıkıştırılmış bir hali gibi
|
||
düşünülebilir.
|
||
|
||
Bugün pek çok programlama editörleri default durumda dosyayı Unicode UTF-8 encoding'ine göre saklamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bilgisayar dünyasında çok kullanılan diğer bir sayı sistemi de 16'lık sistemdir. 16'lık sisteme İngilizce "hexadecimal
|
||
system" denilmektedir. 16'lık sistemde syaıları ifade etmek için 16 sembol bulunmaktadır. İlk 10 sembol 10'luk sistemdeki
|
||
sembollerden alınmıştır. Sonraki 6 sembol alfabetik karakterlerden alınmıştır:
|
||
|
||
0
|
||
1
|
||
2
|
||
3
|
||
4
|
||
5
|
||
6
|
||
7
|
||
8
|
||
9
|
||
A
|
||
B
|
||
C
|
||
D
|
||
E
|
||
F
|
||
|
||
16'lık sistemdeki her bir basamağa "hex digit" denilmektedir. Örneğin:
|
||
|
||
1FC8
|
||
|
||
Burada 4 hex digit'lik bir sayı vardır. 16'lık sistemdeki bir sayıyı 10'luk sisteme dönüştürmek için her hex digit
|
||
16'lık kuvvetleriyle çarpılıp toplanır. Ancak 16'lık sistemdeki sayı kullanım gereği bakımından aslında 10'lu sisteme
|
||
pek dönüştürülmez. 16'lık sistemdeki her bir hex digit 4 bit ile ifade edilebilmektedir:
|
||
|
||
0 0000
|
||
1 0001
|
||
2 0010
|
||
3 0011
|
||
4 0100
|
||
5 0101
|
||
6 0110
|
||
7 0111
|
||
8 1000
|
||
9 1001
|
||
A 1010
|
||
B 1011
|
||
C 1100
|
||
D 1101
|
||
E 1110
|
||
F 1111
|
||
|
||
16'lık sistemden ikilik sisteme dönüştürme yapmak çok kolaydır. Tek yapılacak şey bir hex digit'e karşılık yandaki
|
||
tablodaki 4 biti getirmektir. Örneğin:
|
||
|
||
1FC9 = 0001 1111 1100 1001
|
||
FA3D = 1111 1010 0011 1101
|
||
|
||
ikilik sistemdeki bir sayı da 16'lık sisteme çok kolay dönüştürülür. Tek yapılacak şey sayıyı dörderli gruplayıp ona
|
||
karşı gelen hex digit'i yazmaktır. Örneğin:
|
||
|
||
1010 0001 1110 1000 0011 0101 = A1E835
|
||
A 1 E 8 3 5
|
||
|
||
Bilgisayar dünyasında 16'lık sistem aslında ikilik sistemin yoğun bir gösterimi olarak kullanılmaktadır. Yani ikilik
|
||
sistem çok yer kapladığı için kişiler ikilik sistem yerine 16'lık sistemi kullanırlar. Bu nedenle belleğin, dosyanın
|
||
içeriğini görüntüleyen programlar bunları ikilik sistem yerine 16'lık sistemde görüntülemektedir.
|
||
|
||
1 byte 2 hex digit ile ifade edilmektedir. Örneğin:
|
||
|
||
1A 23 5C 78
|
||
|
||
Burada 4 byte'lık bir bilgi vardır. Örneğin 2 byte içerisinde yazılabilecek en küçük negatif işaretli sayının hex
|
||
karşılığı 8000 biçimindedir. Örneğin bir byte'lık işaretli sistemde yazılabilecek en büyük pozitif sayı 7F biçimindedir.
|
||
İşareti tamsayı sisteminde 4 byte içerisinde -1 sayısı FFFFFFFF biçimindedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Eskiden daha fazla kullanılıyor olsa da toplamda oldukça seyrek kullanılan dğer bir sayı sistemi de 8'lik sayı sistemidir.
|
||
Bu sisteme İngilizce "octal system" denilmektedir. 8'lik sayı sistemindeki her bir basamağa "octal digit" denir. Octal
|
||
digit sembolleri olarak 10'luk sistemin ilk 8 sembolü kullanılmaktadır:
|
||
|
||
0
|
||
1
|
||
2
|
||
3
|
||
4
|
||
5
|
||
6
|
||
7
|
||
|
||
Her octal digit 3 bit ile ifade edilebilir:
|
||
|
||
0 000
|
||
1 001
|
||
2 010
|
||
3 011
|
||
4 100
|
||
5 101
|
||
6 110
|
||
7 111
|
||
|
||
Bu durumda bir octal sayı ikilik sisteme kolay bir biçimde dönüştürülebilir:
|
||
|
||
476 100 111 110
|
||
741 111 100 001
|
||
|
||
Benzer biçimde ikilik sistemdeki bir sayı da sağdan sola üçer bir gruplandırılarak 8'lik sisteme dönüştürülebilmektedir. Örneğin:
|
||
|
||
1011 1011 = 273
|
||
0111 1110 = 176
|
||
|
||
8'lik sistem de ikilik sistemin yoğun bir gösterimi olarak kullanılmaktadır. Ancak 8'i tam ortalayamadığı için kullanımı
|
||
seyrektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
7. Ders - 14/06/2022-Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Klavyeden bastımız tuşlara ilişkin karakterlerin İngilizce isimleri şöyledir:
|
||
|
||
+ plus
|
||
- minus, dash, hyphen
|
||
* asterisk
|
||
/ slash
|
||
\ back slash
|
||
% percent sign
|
||
() paranthesis (left, right, opening, closing)
|
||
{} (curly) brace (left, right)
|
||
[] (square) bracket (left, right)
|
||
= equal sign
|
||
# sharp, number sign
|
||
' single quote
|
||
"" doueble quote
|
||
_ underscore
|
||
^ caret
|
||
& ampersand
|
||
! exclamation mark
|
||
, comma
|
||
: colon
|
||
; semicolon
|
||
| pipe
|
||
< less than
|
||
> greater than
|
||
. period
|
||
? question mark
|
||
` back tick
|
||
~ tilde
|
||
@ at
|
||
... ellipsis
|
||
$ dollar sign
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Ekrana Merhaba Dunya yazısını çıkartan örnek C programı aşağıdaki gibidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
printf("Hello World\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir C programı en yalın olarak önce bir text editör ile yazılır ve diske uzantısı ".c" biçiminde kaydedilir. Sonra
|
||
komut satırından C derleyicisi ile derlenir. Derleyiciler genel olarak komut satırından çalıştırılacak biçimde yazılırlar.
|
||
En çok kullanılan C derleyicileri şunlardır:
|
||
|
||
- Microsoft C Derleyicisi
|
||
- gcc Derleyicisi
|
||
- clang Derleyicisi
|
||
- Intel C derleyicisi
|
||
|
||
Bu derleyicilerin dışında daha pek çok C derleyicisi vardır ve çeşitli kesimler tarafından kullanılmaktadır. Windows'ta
|
||
ağırlıklı olarak Microsoft'un C derleyicileri kullanılmaktadır. UNIX/Linux ve macOS sistemlerinde ise çoğu kez "gcc"
|
||
ya da "clang" derleyicileri tercih edilmektedir. "gcc" derleyicisinin ve clang derleyicisinin Windows versiyonu da
|
||
vardır. "gcc" derleyicisinin Windows için gerçekleştirimine (Windows port;'una) "mingw" denilmektedir.
|
||
|
||
C programını C derleyicisi ile derledikten sonra eğer hiçbir hata yoksa derleyici "relocatable object module" denilen
|
||
bir dosya oluşturmaktadır. Bu dosyaya biz Türkçe "amaç dosya" da diyeceğiz. Amaç dosya (relocatable object module)
|
||
daha sonra "bağlayıcı (linker)" denilen bir programa sokulur. Bu bağlayıcı programı da "çalıştırılabilir (executable)"
|
||
dosya üretir. Biz de nihayetinde bu dosyayı çalıştırırız.
|
||
|
||
.c ---> C Derleyicisi ---> Amaç Dosya (Object file) ---> Bağlayıcı (Linker) ---> Çalıştırılabilir (executable) Dosya
|
||
|
||
Bağlayıcı (linker da diyeceğiz) aslında bir grup amaç dosyayı alıp tek bir çalıştırılabilir dosya oluşturabilmektedir.
|
||
Bir amaç dosyanın içerisinde derlenmiş kodların yanı sıra bağlayıcnın birleştirme yapabilmesi için çeşitli bilgiler
|
||
de vardır.
|
||
|
||
Windows'ta bağlayıcı olarak genellikle Microsoft'un "link.exe" isimli programı kullanılmaktadır. UNIX/Linux sistemlerinde
|
||
de ağırklıklı olarak GNU'nun "ld" isimli bağlayıcı ya da LLVM projesindeki "ldd" bağlayıcısı kullanılır.
|
||
|
||
Derleyicinin ürettiği amaç dosyanın uzantıları Windows sistemlerinde ".obj", UNIX/Linux ve macOS sistemlerinde ise
|
||
".o" biçimindedir.
|
||
|
||
Bağlayıcının ürettiği "çalıştırılabilir" dosya ise Windows sistemlerinde ".exe" uzantılıdır. UNIX/Linux ve macOS
|
||
sistemlerinde dosyanın çalıştırılabilir olup olmadığı uzantı ile değil dosya erişim hakları ile ("x" hakkı ile)
|
||
belirlenmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Windows sistemlerinde Merhaba Dünya programının komut satırından derlenip çalıştırışması şöyle yapılır:
|
||
|
||
1) Program bir editörde yazılır ve .c uzantılı biçimde saklanır. Biz bunun "sample.c" olduğunu varsayalım.
|
||
|
||
2) Daha sonra komut satırı programı çalıştırılır ve dosayanın saklandığı dizine gidilir. Komut satırı programı olarak "cmd.exe" programını doğrudan kullanmayınız.
|
||
Çünkü bu program gerekli "path ayarlarına" sahip değildir. Bunun yerine komut satırına geçmek için "Developer Command Prompt for VS 2022" programını kullanınız.
|
||
Bu kısa yolu masaüstüne taşırsanız rahat edersiniz.
|
||
|
||
3) Microsoft'un C derleyicisi "cl.exe" isimli programdır. Bu program en basit olarak şöyle çalıştırılır:
|
||
|
||
cl <kaynak dosya ismi>
|
||
|
||
Örneğin:
|
||
|
||
cl sample.c
|
||
|
||
cl.exe programı 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 macOS sistemlerinde ./isim biçiminde çalıştırma
|
||
yapılır. gcc derleyicisinde çalıştırılabilir dosyaya isim vermek için "-o isim" çeneği kullanı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ü
|
||
açıp Add/NEw Item seçilerek de yapılabilir. Artık karşımıza başka bir diyalog penceresi çıkacaktır. Burada "C++ File" seçilip dosya ismi "uzantısı .c olacak biçimde"
|
||
seçilmelidir. Microsoft C++ demekle aynı zamanda C'yi 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
|
||
açısal parantezler içerisinde öğeler zorunlu öğeleri, köşeli parantezler içerisindeki öğeler "isteğe bağlı (optional)" öğeleri belirtmektedir. Bunların dışındaki
|
||
tüm atomlar 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 macOS sistemlerindeki derleyicilerde int türü 4 bye (32 bit)
|
||
uzunluktadır. Dolayısıyla int türden bir nesne bu sistemlerde [-2147483678, 2147483647] aralığında tamsayı değerler tutabilir. Bazı mikrodenetleyici
|
||
derleyicilerinde ise int 2 byte (16 bit) uzunluğunda olabilmektedir. Derleyicileri yazanlar genellikle int türünü o sistemdeki CPU yazmaçlarının uzunluğu kadar
|
||
ya da o uzunlukla ifade edilebilecek kadar almaktadır. Bu tür belirtilirken "int" demekle "signed int" demek arasında ya da "int signed" demek arasında
|
||
bir fark yoktur.
|
||
|
||
[unsigned] int: Her işaretli tamsayı türünün bir de işaretsiz biçimi vardır. "signed int" türünün işaretsiz biçimi "unsigned int" türüdür. Tamsayı türlerinin
|
||
işaretli biçimleri ile işaretsiz biçimleri aynı miktarda yer kaplarlar. Aralarındaki tek fark işaret bitinin yorumudur. Dolayısıyla bu tür de 32 bir ve
|
||
64 bit UNIX/Linux ve macOS sistemlerinde 4 byte yer kaplamaktadır. unsigned int türünden bir nesne içerisine bu sistemlerde yerleştirilebilecek sayı
|
||
sınırı [0, +4294967295] biçimindedir. Bu türü biz "unsigned" biçiminde ya da "unsigned int" biçiminde ya da "int unsigned" biçiminde ifade edebiliriz.
|
||
|
||
[signed] long [int]: long türü int türünden uzun olabilir ya da int türüyle aynı uzunlukta olabilir. Ancak int türünden daha kısa olamaz. Standratlara
|
||
göre long türü en az 4 byte (32 bit) uzunlukta olmak zorundadır. long türü de "işaretli" bir tamsayı türüdür. Buradaki "long" ismi "int türünden uzun olabilen"
|
||
anlamına gelmektedir. 32 bit ve 64 bit Windows sistemlerindeki derleyicilerde long türü int türüyle aynı uzunluktadır (yani 4 byte). Ancak 32 bit UNIX/Linux ve
|
||
macOS sistemlerindeki derleyicilerde long türü 4 byte iken, 64 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 macOS sistemlerinde bu tür long türünde olduğu gibi
|
||
4 byte (yani 32 bit) yer kaplamaktadır. Dolayısıyla bu sistemlerdeki sını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 macOS sistemlerinde short türü 2 byte (yani 16 bit) uzunluktadır.
|
||
Dolayısıyla bu sistemlerde bu türden bir nesnesinin içerisine biz [-32768, +32767] sınırları içerisinde bir tamsayı yerleştirebiliriz.
|
||
|
||
unsigned short [int]: Bu tür signed short türünün işaretsiz biçimidir. Dolayısıyla short türü kadar yer kaplar. short türünün 2 byte olduğu sistemlerde
|
||
bu türden bir nesnenin içerisine biz [0, +65535] arasında tamsayı değerler yerleştirebiliriz.
|
||
|
||
signed char: C standartlarında "byte" 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ı açıkça belirtmiş olmasa da
|
||
"Implementation Limits" kısmında gerçek sayı türleri için belirtilen limitler "IEE 754" standardını ima etmektedir. Bu durumda float türü hemen her sistemde
|
||
4 byte uzunluktadır. float türünün yuvarlama hatalarına direnci zayıftır. Bu nedenle float türü aslında C programcıları tarafından az tercih edilen bir gerçek
|
||
sayı türüdür.
|
||
|
||
double: Standartlara göre double türü float türü ile aynı olabilir ya da ondan daha duyarlıklı olabilir. Yaygın sistemlerin büyük çoğunluğunda
|
||
double türü 8 byte uzunluktadır ve ""IEEE 754 LLong Real Format" biçiminde temsil edilmektedir. Ancak bazı mikrodenetleyici derleyicilerinde
|
||
double türü float ile tamamen aynı uzunlukta olabilmektedir. C 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ı ikilik sistemde tutulmaktadır.
|
||
|
||
C'de bir tamsayıyı ikilik sistemde yazmanın bir yolu yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
main(void)
|
||
{
|
||
int a;
|
||
|
||
a = 100;
|
||
printf("%d\n", a); /* 100 */
|
||
|
||
a = 0x64;
|
||
printf("%d\n", a); /* 100 */
|
||
|
||
a = 012;
|
||
printf("%d\n", a); /* 10 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bir noktalı sayı üstel biçimde de yazılabilir. Bunun için sayıdan sonra 'e' ya da 'E' karakteri ve üs sayısı belirtilir. Buradaki üs 10'un
|
||
kaçıncı kuvveti olduğunu belirtmektedir. Örneğin:
|
||
|
||
a = 1.23e20;
|
||
b = 1.23E-12
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
main(void)
|
||
{
|
||
double a;
|
||
|
||
a = 123.456e5;
|
||
printf("%f\n", a); /* 12345600.000000 */
|
||
|
||
a = 1.23e-5;
|
||
printf("%f\n", a); /* 0.000012 */
|
||
|
||
a = 1e20;
|
||
printf("%f\n", a); /* 100000000000000000000.000000 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
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ıkğı değerler üzerinde işlemler yaparak sonucu geri dönüş değeri olarak veren bir standart matematiksel fonksiyon vardır.
|
||
Bu fonksiyonları kullanmadan önce <math.h> dosyası include edilmelidir. Örneğin sqrt fonksiyonun parametrik yapısı şöyledir:
|
||
|
||
double sqrt(double x);
|
||
|
||
Fonksiyon parametresiyle aldığı double sayının kareköküne geri dönmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
int main(void)
|
||
{
|
||
double val, result;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%lf", &val);
|
||
|
||
result = sqrt(val);
|
||
printf("%f\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
pow fonksiyonu bir sayının belli bir kuvvetine geri dönmektedir. Fonksiyonun parametrik yapısı şöyledir:
|
||
|
||
double pow(double a, double b);
|
||
|
||
Fonksiyon a üzeri b işlemine geri dönmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
int main(void)
|
||
{
|
||
double a, b, result;
|
||
|
||
printf("Taban:");
|
||
scanf("%lf", &a);
|
||
|
||
printf("Us:");
|
||
scanf("%lf", &b);
|
||
|
||
result = pow(a, b);
|
||
printf("%f\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
sin, cos, tan, asin, acos, atan fonksiyonları trigonometrik işlemler yapmaktadır. Bu fonksiyonların parametreleri ve geri dönüş değerleri double
|
||
türdendir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
int main(void)
|
||
{
|
||
double result, radian;
|
||
|
||
result = sin(3.141592653589793238462643 / 2);
|
||
printf("%f\n", result);
|
||
|
||
radian = asin(result);
|
||
printf("%f\n", radian);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
round fonksiyonu double bir değeri parametre olarak alıp ona en yakın tamsayıyı yine double bir değer olarak vermektedir.
|
||
Fonksiyonun parametrik yapısı şöyledir:
|
||
|
||
double round(double x);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
int main(void)
|
||
{
|
||
double result;
|
||
|
||
result = round(3.6);
|
||
printf("%f\n", result); /* 4 */
|
||
|
||
result = round(3.4);
|
||
printf("%f\n", result); /* 3 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C99 ile birlikte roundf ve roundl fonksiyonları da standartlara eklenmiştir. Bunların parametrik yapıları şöyledir:
|
||
|
||
float roundf(float x);
|
||
long double roundl(float x);
|
||
|
||
Yine C99 ile birlikte tamsayı değerlere geri dönen aşağıdakai fonksiyonlar da eklenmiştir.
|
||
|
||
long int lround(double x);
|
||
long int lroundf(float x);
|
||
long int lroundl(long double x);
|
||
long long int llround(double x);
|
||
long long int llroundf(float x);
|
||
long long int llroundl(long double x);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
int main(void)
|
||
{
|
||
long result;
|
||
|
||
result= lround(3.6);
|
||
printf("%ld\n", result); /* 4 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
floor isimli fonksiyon double bir sayıya en yakın ondan küçük ya da ona eşit tamsayıyı bize double türden vermektedir. ceil ise tam ters işlem yapar. Yani
|
||
bir double sayıdan büyük ya da ona eşit en yakın tamsayıyı double türden vermektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
int main(void)
|
||
{
|
||
double result;
|
||
|
||
result= floor(3.9);
|
||
printf("%f\n", result); /* 3 */
|
||
|
||
result = floor(-3.9);
|
||
printf("%f\n", result); /* -4 */
|
||
|
||
result = ceil(3.1);
|
||
printf("%f\n", result); /* 4 */
|
||
|
||
result = ceil(-3.1);
|
||
printf("%f\n", result); /* -3 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C99 ile birlikte float ve long double için aşağıdaki fonksiyonlar da standartlara eklenmiştir:
|
||
|
||
float floorf(float x);
|
||
long double floorl(long double x);
|
||
float ceilf(float x);
|
||
long double ceill(long double x);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
18. Ders 28/07/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bildirilen bir değişkenin kullanılabildiği program aralığına "faaliyet alanı (scope)" denilmektedir. C'de üç faaliyet alanı vardır:
|
||
|
||
1) Blok Faaliyet Alanı (Block Scope): Yalnızca bir blokta o bloğun kapsadığı bloklarda tanınma aralığıdır.
|
||
2) Dosya Faaliyet Alanı (File Scope): Tüm fonksiyonlarda yani her yerde tanınma aralığıdır.
|
||
3) Fonksiyon Faaliyet Alanı (Function Scope): Bir fonksiyonun her yerinde tanınma aralığıdır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de yerel değişkenler blok faaliyet alanı kuralına uyarlar. Yani bildirildikleri yerden bildirildikleri bloğun sonuna kadarki bölgede kullanılabilirler.
|
||
Anımsanacağı gibi C90'da yerel değişkenler blokların başlarında yani blokların ilk işlemleri olacak biçimde bildirilmek zorundaydı. Bu kural C99'da
|
||
kaldırıldı. Yerel değişkenlerin bloğun herhangi bir yerinde bildirilmeleri sağlandı. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo\n");
|
||
|
||
int a; /* C90'da geçersiz! C99'dan itibaren geçerli */
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
int a; /* C90'da da geçerli */
|
||
|
||
printf("bar\n");
|
||
}
|
||
|
||
Bir fonksiyonun içerisinde içerisinde istenildiği kadar iç içe ve ayrık blok oluşturulabilir. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
{
|
||
printf("Ok\n");
|
||
/* .... */
|
||
}
|
||
{
|
||
printf("Ok\n");
|
||
}
|
||
}
|
||
|
||
Yerel değişkenler bildirildikleri yerden itibaren bildirildikleri bloğun sonuna kadarki bölgede kullanılabilirler. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
int a;
|
||
|
||
{
|
||
int b;
|
||
|
||
a = 10; /* geçerli, a faaliyet gösteriyor */
|
||
b = 20; /* geçerli, b faaliyet gösteriyor */
|
||
}
|
||
printf("%d\n", a); /* geçerli, a faaliyet gösteriyor */
|
||
printf("%d\n", b); /* geçersiz! b burada faaliyet göstermiyor */
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
a = 100; /* geçersiz, a burada faaliyet göstermiyor */
|
||
}
|
||
|
||
Tabii C99 ve sonrasında bildirilen bir yeral değişken bildirim yerinden önce de kullanılamaz. Örneğin:
|
||
|
||
void bar(void)
|
||
{
|
||
a = 10; /* geçersiz! a faaliyet göstermiyor */
|
||
|
||
int a;
|
||
|
||
a = 20; /* geçerli */
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de aynı faaliyet alanına ilişkin aynı isimli birden fazla değişken tanımlanamaz. Ancak farklı faaliyet alanlarına ilişkin aynı isimli değişkenler
|
||
tanımlanabilir. Aynı bloğun farklı yerlerinde tanımlanan değişkenler bu bakımdan aynı faaliyet alanı içerisinde kabul edilirler. Bu nedenle C'de aynı
|
||
blok içerisinde aynı isimli bir den fazla değişken tanımlanamaz. Ancak farklı bloklarda aynı isimli değişkenler tanımlanabilir. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
int a;
|
||
/* ....*/
|
||
double a; /* geçersiz! aynı blok içerisinde aynı isimli tek bir değişken tanımlanabilir */
|
||
}
|
||
|
||
Örneğin:
|
||
|
||
void bar(void)
|
||
{
|
||
int a;
|
||
{
|
||
double a; /* geçerli iç içe bloklarda aynı isimli değişkenler tanımlanabilir */
|
||
/* ... */
|
||
}
|
||
/* ... */
|
||
}
|
||
|
||
void tar(void)
|
||
{
|
||
int a; /* geçerli */
|
||
/* ... */
|
||
}
|
||
|
||
Farklı bloklardaki aynı isimli değişkenler aslında tamamen farklı nesneler belirtirler, bunların yalnızca isimleri aynıdır. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
int a;
|
||
|
||
{
|
||
int a; /* Bu tamamen farklı bir a */
|
||
/* ... */
|
||
}
|
||
}
|
||
|
||
C'de aynı blokta birden fazla aynı isimli değişken faaliyet gösteriyorsa o blokta o değişken kullanıldığında her zaman "dar faaliyet alanına sahip olan"
|
||
değişkenin kullanılmış olduğu kabul edilir. Örneğin:
|
||
|
||
{
|
||
int a;
|
||
|
||
{
|
||
int a;
|
||
|
||
a = 10; /* dar faaliyet alanına sahip olan iç bloktaki a'dır */
|
||
}
|
||
|
||
a = 20; /* dış bloktaki a, zaten iç bloktaki a burada faaliyet göstermiyor */
|
||
}
|
||
|
||
İç içe bloklarda aynı isimli değişkenlerin bildirildiği durumda iç blokta dış bloktaki değişkene erişmenin herhangi bir yolu yoktur. Bu duruma
|
||
"iç bloktaki değişkenin dış bloktaki gizlemesi (hiding)" denilmektedir.
|
||
|
||
C99 ve sonrasında bir yerel değişken bloğun herhangi bir yerinde bildirilebildiğine göre bildirim yerine kadar üst bloktaki dğeişken faaliyet gösteriyor
|
||
durumdadır ve henüz üst bloktaki aynı isimli değişken gizlenmemiştir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
{
|
||
a = 20; /* ana bloktaki a */
|
||
|
||
int a = 30;
|
||
|
||
printf("%d\n", a); /* iç bloktaki a */
|
||
}
|
||
|
||
printf("%d", a); /* ana bloktaki a */
|
||
|
||
return 0;
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bildirimi fonksiyonların dışında yapılan global değişkenler "dosya faaliyet alanı (file scope)" kuralına uyarlar. Yani kaynak dosyanın her yerinde,
|
||
tüm fonksiyonların içerisinde biz global değişkenleri kullanabiliriz. Ancak C'de derleme işleminin de bir yönü vardır. Bu yön yukarıdan aşağıya
|
||
doğrudur. Bir değişken bildirilmeden önce kullanılamaz. Bu nedenle bir global değişkeni aşağıda bir yerde bildirirsek bildirim yerinde aşağıya kadar
|
||
her yerde kullanırız. Ancak genel olarak global değişkenler kaynak dosyanın tepesinde bildirilirler. Öneğin:
|
||
|
||
int a;
|
||
|
||
void foo(void)
|
||
{
|
||
a = 20; /* global olan a */
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
a = 10; /* global a */
|
||
foo();
|
||
printf("%d\n", a); /* global a, 20 çıkacak */
|
||
}
|
||
|
||
Bir global değişken için en iyi tanımlama yeri kaynak kodun tepesidir. Örneğin:
|
||
|
||
|
||
void foo(void)
|
||
{
|
||
a = 10; /* geçersiz! henüz derleyici a'yı görmedi */
|
||
}
|
||
|
||
int a;
|
||
|
||
void bar(void)
|
||
{
|
||
a = 20; /* geçerli */
|
||
}
|
||
|
||
void tar(void)
|
||
{
|
||
a = 30; /* geçerli */
|
||
}
|
||
|
||
Bir global değişkenle aynı isimli bir yerel değişken tanımlanabilir. Çünkü bunların faaliyet alanları farklıdır. Bir blokta aynı isimli birden fazla
|
||
değişken faaliyet gösteriyorsa o blokta dar faaliyet alanına sahip olan değişkene erişilmektedir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int x;
|
||
|
||
void foo(void)
|
||
{
|
||
double x;
|
||
|
||
x = 20; /* yerel x kullanılıyor */
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
x = 10; /* global x */
|
||
|
||
foo();
|
||
printf("%d\n", x); /* global x, 10 çıkacak */
|
||
|
||
return 0;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de istisnai olarak ilkdeğer verilmemiş birden fazla aynı isimli global değişken tanımlanabilir. Buna "tentative definition" denilmektedir.
|
||
Bu durumda aslında toplamda tek bir nesne yaratılır. Yani birden fazla tanımlama bu istisnasi durumda birden fazla nesne anlamına gelmez.
|
||
Aynı nesnenin gereksiz bir biçimde yeniden "tentative" olarak belirtlmesi anlamına gelir. Tabii bu durum yerel değişkenler için söz konusu değildir.
|
||
Tentative tanımlama tamamen istisnai bir durumdur ve yalnızca global değişkenler için söz konusudur. Bu nedenle aşağıdaki tanımlama geçerlidir.
|
||
Ancak aşağıdaki kodda tek bir x nesnesi vardır. "Tentative" sözcüğü "deneme niteliğinde" gibi bir anlama gelmektedir:
|
||
|
||
#include <stdio.h>
|
||
|
||
int x;
|
||
int x; /* geçerli, özel bir durum, tentative definition */
|
||
|
||
int main(void)
|
||
{
|
||
x = 10;
|
||
printf("%d\n", x);
|
||
|
||
return 0;
|
||
}
|
||
|
||
int x; /* geçerli, tentative definiton */
|
||
|
||
Tentative tanımlama olması için global değişkene ilkdeğer verilmemiş olması gerekmektedir. Aynı isimli bir global değişkene bir kez ilkdeğer verilebilir.
|
||
Ancak birden fazla kez ilkdeğer verilemez. Örneğin:
|
||
|
||
int a = 10; /* geçerli, tentative değil */
|
||
int a; /* geçerli, tentative, aslında burada bir a yaratılmıyor */
|
||
|
||
Ancak örneğin:
|
||
|
||
int a = 10;
|
||
int a = 20; /* geçersiz! tentative değil */
|
||
|
||
Bu kural ileride yeniden başka bir konunun içerisinde ele alınacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Daha önce de belirtidliği gibi içerisine henüz değer atanmamış yerel değişkenin içeisinde bellekte daha önceden kalmış olan "çöp bir değer (garbage value)"
|
||
bulunur. Ancak içerisine henüz değer atanmamış global bir değişkende her zaman 0 değeri olması garanti edilmiştir.
|
||
Ayrıca C'de içerisinde çöp değerlerin olduğu yerel değişkenlerin kullanılması "tanımsız davranışa (undefined behavior)" yol açmaktadır
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int x;
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("%d\n", a); /* geçerli, içerisine henüz değer atanmamış yerel değişkenler içerisinde çöp değer vardır (tanımsız davranış) */
|
||
printf("%d\n", x); /* içerisine henüz değer atanmamış global nesneler içerisinde her zaman 0 olur */
|
||
s
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Genel olarak global değişkenlerin yalnızca gerektiği durumlarda kullanılması gerekir. Yerel değişkenlerle yapabileceğimiz şeyler için global değişken
|
||
tanımlamak kötü bir tekniktir. Örneğin programımızda yalnızca main fonksiyonu olsun. Bu durumda global bir değişken tanımlamaya hiç gerek yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyonların parametre değişkenleri faaliyet alanı bakımından ana bloğun başında tanımlanmış olan yerel değişkenler gibidir. Örneğin:
|
||
|
||
void foo(int a, int b)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Buradaki a ve b parametre değişkenleri faaliyet alanı bakımından aşağıdaki fonksiyonla eşdeğerdir:
|
||
|
||
void foo()
|
||
{
|
||
int a;
|
||
int b;
|
||
|
||
/* ... */
|
||
}
|
||
|
||
Görüldüğü gibi fonksiyonların parametre değişkenleri "blok faaliyet alanı (block scope)" uymaktadır. Yani yalnızca o fonksiyonda kulalnılabilirler.
|
||
Dolayısıyla farklı iki fonksiyonun parametre değişkenleri aynı isimde olabilir. Örneğin:
|
||
|
||
void foo(int a)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
void bar(int a) /* geçerli, a yalnızca bu fonksiyonda kullanılabilir.
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Mademki fonksiyonun parametre değişkenleri faaliyet alanı bakımından ana bloğun başında bildirilen değişkenler gibidir, o halde parametre değişkeni ile
|
||
aynı isimli fonksiyonun ana bloğunda bir değişken bildirilemez. Örneğin:
|
||
|
||
void foo(int a)
|
||
{
|
||
int a; /* geçersiz! parametre değişkeni olan a da aynı faaliyet alanına sahip */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
Fakat örneğin:
|
||
|
||
void foo(int a)
|
||
{
|
||
{
|
||
int a; /* geçerli! iç içe yerel bloklarda aynı isimli değişkenler tanımlanabilir */
|
||
|
||
/* ... */
|
||
}
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de fonksiyon faaliyet alanına sahip tek değişken "goto etiketleridir". Goto deyimi deyimlerin ele alındığı geşecek bölümlerde görülecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Daha önceden de belirtildiği gibi C Programlama Dili "Prosedürel Programlama Modeline (Procedural Programming Paradigm)" uygun tasarlanmıştır.
|
||
Prosedürel programlamada "fonksiyonlar" birer yapı taşıdır. Programlar fonksiyonların birbirlerini çağırması biçiminde organize edilirler. Halbuki
|
||
Nesne Yönelimli Programlama Modelinin yapı taşı "sınıf" denilen kavramdır. C++ Programala Dili zaten C Programa Dilinin "Nesne Yönelimli Programalama Modelinin"
|
||
uygulanabilmesi için genişletilmiş bir biçimidir.
|
||
|
||
Pekiyi prosedürel teknikte neden program fonksiyonların birbirlerini çağırması biçiminde organize edilmektedir? Yani neden bürün program main fonksiyonunda
|
||
yazılıp bitirilmemektedir? Programın fonksiyonlar biçiminde organize edilmesinin birkaç açık sebei vardır:
|
||
|
||
1) Mühendislikte karmaşık bir problem genellikle parçalarına ayrılarak çözülmektedir. İşte fonksiyonlar karmaşık problemin parçalarını oluşturmak
|
||
için kulalnılmaktadır. Karmaşık işlemin parçaları fonksiyonlara yaptırılır. Sonra bu fonksiyonların çağrılmasıyla karmaşık işlem gerçekleştirilir.
|
||
Örneğin bir otomobil aslında çok fazla parçadan oluşmaktadır. Bu parçalar birbirleriyle monte edilmiştir. Sonuçta otomobil çalışır hale gelmiştir.
|
||
Aynı yöntem yazılımda da izlenmektedir.
|
||
|
||
2) Fonksiyonlar "yeniden kullanılabilirliği (reusability)" mümkün hale getirmektedir. Yani işin bir kısmını yapan kodları fonksiyon olarak yazarsak
|
||
başka projelerde de aynı fonksiyonları kullanabiliriz. Fonksiyonların oluşturduğu topluluğa "kütüphane (library)" denilmektedir. Örneğin standart C
|
||
fonksiyonları kütüphane biçiminde oluşturulmuştur. Biz onları farklı projelerde kullanabilmekteyiz.
|
||
|
||
3) Fonksiyonlar tekrarı engellemek amacıyla kullanılmaktadır. Bir iş kodun çeşitli yerlerinde yineleniyorsa onu fonksiyon olarak yazarsak toplamda
|
||
bu kodlardan bir tane projemizde bulundurmuş oluruz. Fonksiyonlar olmasaydı aynı kodu tekrar tekrar yazmak zorunda kalırdık. Bu durumda kod tekrarı
|
||
toplamda kodun fazla yer kaplamasına yol açardı. O kısımda yapılacak değişikler programın pek çok yerinde yapılmak zorunda kalırdı. Bu durum kodun aynı zamanda
|
||
daha karmaşık gözükmesine yol açardı.
|
||
|
||
4) Fonksiyonlar okunabilirliği de artırmaktadır. Fonksiyonların isimleri olduğu için kodu inceleyen kişiler onu daha kolay anlamlandırırlar.
|
||
Bu isimler aslında o kodun ne yaptığı hakkında da bilgi verir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
19. Ders - 02.08.2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir programlama dilindeki "çalıştırma birimlerine" "deyim (statement)" denilmektedir. Yani program aslında deyim denilen kod parçalarının peşi sıra
|
||
çalıştırılmasıyla çalışmaktadır. Deyimler C'de 5 gruba ayrılmaktadır:
|
||
|
||
1) Basit Deyimler (Simple Statements): Bunlar bir ifadenin sonuna ';' atomu konularak elde edilen deyimlerdir. Yani ifade; biçiminde bir görünüme sahiplerdir.
|
||
Örneğin:
|
||
|
||
a = b + c;
|
||
foo();
|
||
|
||
Bunlar birer basit deyimdit. İfade (expression) kavramının ';' atomunu içermediğine ifadenin sonuna ';' getirildiğinde onun bir deyim olduğuna
|
||
dikkat ediniz.
|
||
|
||
2) Bileşik Deyimler (Compound Statements): Bir blok içerisine sıfır tane ya da daha fazla deyim yerleştirilirse bloğun kendisi de bir deyim olur.
|
||
Ona "bileşik deyim" denilmektedir. Örneğin
|
||
|
||
{
|
||
ifade1;
|
||
ifade2;
|
||
ifade3;
|
||
}
|
||
|
||
Burada bu bloğun tamamı dışarıdan bakıldığında tek bir deyimdir.
|
||
|
||
3) Kontrol Deyimleri (Control Statements): Programlama dillerinde programın akışı üzerinde etkili olan, if gibi, while gibi, for gibi deyimlere
|
||
"kontrol deyimleri" denilmektedir. Kontrol deyimleri dışarıdan bakıldığında tek bir deyim olarak ele alınırlar.
|
||
|
||
4) Bildirim Deyimleri (Declarartion Statements): Bildirim yapmakta kullandığımız sentaks biçimi de aslında bir deyim belirtir. Bunlara bildirim deyimleri
|
||
denilmektedir. Örneğin:
|
||
|
||
int a, b, c;
|
||
|
||
5) Boş Deyimler (Null Statements): Solunda ifade olmadan kullanılan noktalı virgüller de bir deyim belirtir. Bunlara boş deyim denilmektedir. Örneğin:
|
||
|
||
x = 10;;
|
||
|
||
Burada iki deyim vardır. Birincisi x = 10; deyimidir. Bu bir basit deyimdir. İkincisi bundan sonraki noktalı virgüldür. Boş deyimler için bir şey yapılmıyor
|
||
olsa da bunlar yine bir deyim statüsündedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıda da belirtildiği gibi program deyimlerin çalıştırılmasıyla çalıştırılmaktadır. Her deyim çalıştığında bir şeyler yapılır. Şimdi bu deyimler
|
||
çalıştırıldığında ne olacağı üzerinde duralım:
|
||
|
||
- Bir basit deyimin çalıştırılması demek o basit deyimdeki ifadenin çalıştırılması demektir.
|
||
|
||
- Bir bileşik deyimin çalıştırılması bileşik deyimi oluşturan deyimlerin sırasıyla çalıştırılması anlamına gelmektedir. Örneğin:
|
||
|
||
{
|
||
ifade1;
|
||
ifade2;
|
||
{
|
||
ifade3;
|
||
ifade4;
|
||
}
|
||
}
|
||
ifade5;
|
||
|
||
Burada dışarıdan bakıldığında iki deyim vardır: Bileşik deyim ve basit deyim. Bir bileşik deyimin çalıştırılması onu oluşturan deyimlerin sırasıyla
|
||
çalıştırılması anlamına geldiğine göre burada sırasıyla aslında ifade1, ifade2, ifade3, ifade4, ifade5 çalıştırılacaktır.
|
||
|
||
- Kontrol deyimleri çalıştırıldığında nelerin olacağı zaten sonraki başlıklarda ele alınacaktır.
|
||
|
||
- Bir bildirim deyimi çalıştırıldığında bildirilen değişkenler için bellekte yerler ayrılmaktadır. Örneğin:
|
||
|
||
int a, b, c;
|
||
|
||
Burada a, b ve c nesneleri için yerler ayrılacaktır.
|
||
|
||
- Boş deyimin çalıştırılması sırasında bir şey yapılmamaktadır. Yani boş deyimler bir yan etkiye yol açmamaktadır.
|
||
|
||
Bir fonksiyon çağrıldığında fonksiyonun belirttiği ana blok yani bileşik deyim çalıştırılır. Bu durumda bir C programının çalışması demek aslında
|
||
main fonksiyonun çağrılması demektir. Örneğin:
|
||
|
||
int add(int a, int b)
|
||
{
|
||
return a + b;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
a = add(10, 20);
|
||
printf("%d\n", a);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada main fonksiyonu çağrıldığında onun ana bloğunun belirttiği bileşik deyim çalıştırılır. Bu bileşik deyim içerisinde bir bildirim deyimi, 2 tane basit deyim ve bir
|
||
tane kontrol deyimi vardır. a = add(10, 20) basit deyimi çalıştırılırken de add fonksiyonun ana beloğunun belrttiği bileşik deyim çalıştırılmış olur.
|
||
Yani görüldüğü gibi aslında program deyimlerin çalıştırılmasıyla çalıştırılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Şimdi tek tek kontrol deyimlerini ele alacapız. En yaygın kullanılan kontrol deyimi if deyimidir. if deyiminin genel biçimi şöyledir:
|
||
|
||
if (<ifade>)
|
||
<deyim>
|
||
[ else
|
||
<deyim> ]
|
||
|
||
if anahtar sözcüğünden sonra parantezler içerisinde bir ifadenin bulunması gerekir. if deyiminin "doğru" ve "yanlış" kısımları vardır. Doğru ve yanlış kısımlarında
|
||
tek bir deyim bulunmak zorundadır. Programcı burada birden fazla deyim bulundurmak istiyorsa onu bileşik deyim olarak ifade etmelidir. if deyimiin yanlış kısmı
|
||
olmak zorunda değildir. if deyiminin tamamı dışarıdan bakıldığında tek bir deyim olarak ele alınmaktadır.
|
||
|
||
if deyimi şöyle çalıştırılmaktadır: Önce if parantezi içerisindeki ifadenin sayısal değeri hesaplanır. Bu değer sıfır dışı bir değerse deyimin yalnızca
|
||
"doğru" kısmındaki deyim çalıştırılır. Bu ifadenin değeri 0 ise deyimin yalnızca "yanlış" kısmındaki deyim çelıştırılır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
if (a > 0)
|
||
printf("pozitif\n");
|
||
else
|
||
printf("negatif ya da sifir\n");
|
||
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki programda ikinci derece denklemin kökleri yazdırılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
void disp_roots(double a, double b, double c)
|
||
{
|
||
double delta;
|
||
double x1, x2;
|
||
|
||
delta = b * b - 4 * a * c;
|
||
if (delta >= 0) {
|
||
x1 = (-b + sqrt(delta)) / (2 * a);
|
||
x2 = (-b - sqrt(delta)) / (2 * a);
|
||
|
||
printf("x1 = %f, x2 = %f\n", x1, x2);
|
||
}
|
||
else
|
||
printf("Gercek kok yok!..\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
double a, b, c;
|
||
|
||
printf("a:");
|
||
scanf("%lf", &a);
|
||
|
||
printf("b:");
|
||
scanf("%lf", &b);
|
||
|
||
printf("c:");
|
||
scanf("%lf", &c);
|
||
|
||
disp_roots(a, b, c);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İç içe (nested) if deyimi söz konusu olabilir. Örneğin aşağıda üç sayının en büyüğünü bulan bir if deyimi kullanılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a, b, c;
|
||
|
||
printf("a:");
|
||
scanf("%d", &a);
|
||
|
||
printf("b:");
|
||
scanf("%d", &b);
|
||
|
||
printf("c:");
|
||
scanf("%d", &c);
|
||
|
||
if (a > b)
|
||
if (a > c)
|
||
printf("%d\n", a);
|
||
else
|
||
printf("%d\n", c);
|
||
else
|
||
if (b > c)
|
||
printf("%d\n", b);
|
||
else
|
||
printf("%d\n", c);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
if deyiminin "yanlış" kısmı yani else kısmı olmak zoruda değildir. Eğer derleyici if deyiminin "doğru" kısmından sonra else anahtar sözcüğünü
|
||
göremezse bunun "else kısmı olmayan bir if" olduğuna karar verir ve if deyiminin bittiğini düşünür. Örneğin:
|
||
|
||
if (ifade1) ifade2; ifade3;
|
||
|
||
Burada if eyiminin doğru kısmı ifade2 ile birmiştir ve else anahtar sözcüğü gelmemiştir. Bu udurmda artık ifade3 if içerisinde değildir. Bu kod parçasına
|
||
dışarıdan bakıldığında iki deyim vardır: if deyimi ve bir basit deyim.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
if (a > 0)
|
||
printf("pozitif\n");
|
||
printf("son\n"); /* if deyiminin dışında */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bazen yeni programcılar if deyiminin doğru kısmını yanlışlıkla boş deyimle kapatırlar. Budurumda kod geçerli olduğu halde istenileni yapmaz hale
|
||
gelir. Örneğin:
|
||
|
||
if (ifade1); /* dikkat! yanlışlıkla yerleştirilmiş boş deyim */
|
||
ifade2
|
||
|
||
Burada artık if deyiminin "doğru" kısmında boş deyim vardır. Boş deyimden sonra else anahtar sözcüğü gemediği için if deyimi bitmiştir.
|
||
Dolayısıyla ifade2; if deyimi dışındadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
if (a > 0); /* dikkat! yanlışlıkla yerleştirilmiş boş deyim */
|
||
printf("pozitif\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki kod parçasında programcı if deyiminin "doğru" kısmına birden fazla deyim yerleştirmiştir. Bunları bloklayarak tek deyim biçiminde
|
||
ifade etmesi gerekirdi:
|
||
|
||
if (ifade1)
|
||
ifade2;
|
||
ifade3;
|
||
else
|
||
ifade4;
|
||
|
||
Derleyici bakış açısıyla kodu incelediğimizde derleyici if parantezinden sonra blok açılmadığını gördüğünde yalnızca ifade2; deyiminin if deyiminin doğru
|
||
kısmını oluşturduğunu düşünmektedir. ifade2; deyiminden sonra else gelmediği için derleyiciye göre if deyimi sonlanmıştır. Derleyici daha sonra
|
||
else anahtar sözcüğünü gördüğünde durumu "sanki if olmadan yalnız başına else anahtar sözcüğü kullanılmış gibi" ele almaktadır. Bu durumda verilen mesaj
|
||
"error: else without if" gibi bir şey olabilir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir koşul doğru iken diğerlerinin doğru olma olasılığı yoksa bu koşullara "ayrık koullar" denir. Yani ayrık koşullarda koşulların yalnızca bir
|
||
tanesi doğru olabilmektedir. Örneğin:
|
||
|
||
a > 0
|
||
a < 0
|
||
a == 0
|
||
|
||
Bu koşullar ayrıktır. Örneğin:
|
||
|
||
a == 1
|
||
a == 2
|
||
a == 3
|
||
|
||
Bu koşullar da ayrıktır. Ancak örneğin:
|
||
|
||
a > 0
|
||
a > 10
|
||
|
||
Bu koşullar ayrık değildir.
|
||
|
||
Ayrık koşulların ayrı if deyimleri ile ele alınması kötü bir tekniktir. Örneğin:
|
||
|
||
if (a == 1)
|
||
printf("bir\n");
|
||
if (a == 2)
|
||
printf("iki\n");
|
||
if (a == 3)
|
||
printf("uc\n");
|
||
|
||
Burada a == 1 ise gereksiz bir biçimde diğer iki koşul da -doğrulanmayacağı halde*- gereksiz bir biçimde yapılmaktadır. a == 2 ise de a == 3 koşulu
|
||
gereksiz biçimde yapılacaktır. İşte ayrık koşullar "else if" ile ele alınmalıdır. Örneğin:
|
||
|
||
if (a == 1)
|
||
printf("bir\n");
|
||
else
|
||
if (a == 2)
|
||
printf("iki\n");
|
||
else
|
||
if (a == 3)
|
||
printf("üc\n");
|
||
|
||
Burada dışarıdan bakıldığında tek bir if deyimi vardır. Her if diğerinin else kısmı içeisindedir. Pek çok programcı böyle else-if merdivenlerini
|
||
aşağıdaki gibi alt alta yazmaktadır:
|
||
|
||
if (a == 1)
|
||
printf("bir\n");
|
||
else if (a == 2)
|
||
printf("iki\n");
|
||
else if (a == 3)
|
||
printf("uc\n");
|
||
else if (a == 4)
|
||
printf("dort\n");
|
||
else
|
||
printf("hicbiri\n");
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
if (a == 1)
|
||
printf("bir\n");
|
||
else if (a == 2)
|
||
printf("iki\n");
|
||
else if (a == 3)
|
||
printf("uc\n");
|
||
else if (a == 4)
|
||
printf("dort");
|
||
else
|
||
printf("bes\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte sayının işareti yazdırılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
if (a > 0)
|
||
printf("pozitif\n");
|
||
else if (a < 0)
|
||
printf("negatif\n");
|
||
else
|
||
printf("sifir\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C, C++, Java ve C# gibi dillerde "dangling else" denilen bir durum vardır. Eğer iki if için tek bir else varsa bu else içteki if deyimine
|
||
ilişkin kabul edilmektedir. Örneğin:
|
||
|
||
if (ifade1) if (ifade2) ifade3; else ifade4;
|
||
|
||
Buradaki else içteki if deyiminin else kısmıdır. Bunu daha güzel şöyle yazabiliriz:
|
||
|
||
if (ifade1)
|
||
if (ifade2)
|
||
ifade3;
|
||
else
|
||
ifade4;
|
||
|
||
Bazen deneyimli programcılar bile bu "dangling else" durumunda hata yapabilmektedir. Örneğin aşağıdaki gibi bir kodla karşılaşmış olalım:
|
||
|
||
if (ifade1)
|
||
if (ifade2)
|
||
ifade3;
|
||
else
|
||
ifade4;
|
||
|
||
Burada muhtemelen programcı ifade4; deyiminin dıştaki if deyimin else kısmında olmasını istemiştir. Çünkü hizalaması bunu düşündürmektedir.
|
||
Ancak derleyici hizalamaya bakmamaktadır. Dolayısıylla derleyici buradaki else kısmının içteki if deyiminin else kısmı olduğuna karar verir.
|
||
O halde programcı bir "bug" yapmıştır. Bu tür "dangling else" durumlarında eğer gerçekten else kısmın dıştaki if deyimine ilişkin olması isteniyorsa
|
||
bilinçli bloklama yapılmalıdır. Örneğin:
|
||
|
||
if (ifade1) {
|
||
if (ifade2)
|
||
ifade3;
|
||
}
|
||
else
|
||
ifade4;
|
||
|
||
Burada artık else kısmı dıştaki if deyimine ilişkindir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
20.Ders 04/08/2022
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
if deyiminin yalnızca yanlış kısmı bulunamaz. Bu işlem koşulun tersi oluşturularak dolaylı biçimde sağlanabilir. Örneğin:
|
||
|
||
if (a > 0)
|
||
else { /* geçersiz */
|
||
/* ... */
|
||
}
|
||
|
||
Burada aslında a > 0 değilse bir işlem yapılmak istenmiştir:
|
||
|
||
if (a <= 0) {
|
||
/* ... */
|
||
}
|
||
|
||
Tabii mademki if deyiminin yalnızca else kısmı bulunamaz. O halde doğru kısmına bir boş deyim yerleştirilerek de aynı durum sağlanabilir:
|
||
|
||
if (a > 0)
|
||
;
|
||
else { /* geçerli */
|
||
/* ... */
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir program parçesının yinelemeli olarak çalıştırılmasını sağlayan kontrol deyimlerine "döngü (loop)" denilmektedir. C'de döngüler iki kısma yarılmaktadır:
|
||
|
||
1) while Döngüleri (while Loops)
|
||
2) for Döngüleri (for loops)
|
||
|
||
while döngüleri de kendi aralarınd "kontrolün başta yapıldığı while döngüleri" ve "kontrolün sonra yapıldığı while döngüleri" olmak üzere ikiye ayrılmaktadır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Kontrolün başta yapıldığı while döngülerinin genel biçimi şöyledir:
|
||
|
||
while (<ifade>)
|
||
<deyim>
|
||
|
||
while anahtar sözcüğünden sonra parantez içerisinde bir ifadenin bulunması gerekir. while döngüsü bir deyim içerir. Tabii bu deyim, basit, bileşik
|
||
ya da herhangi bir deyim olabilir. Yani döngünün içerisine birden fazla deyim yerleştirilecekse bloklama yapılmalıdır.
|
||
|
||
while döngüsü şöyle çalışmaktadır: Derleyici while parantezinin içerisindeki ifadenin sayısal değerini hesaplar. Bu değer sıfır dışı bir değerese (yani doğru ise)
|
||
döngü deyimi çalıştırılıp başa dönülür. Döngü while parantezi içerisindeki ifadenin değeri 0 olduğunda sonlanır.
|
||
|
||
Aşağıdaki örnekte 0'dan 10'a kadar (10 dahil değil) sayılar ekrana (stdout dosyasına)yazdırılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
i = 0;
|
||
while (i < 10) {
|
||
printf("%d\n", i);
|
||
++i;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte 10'dan başlanarak 0'a kadar (0 dahil değil) sayılar ekrana yazdırılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
i = 10;
|
||
while (i) {
|
||
printf("%d\n", i);
|
||
--i;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte klavyeden (stdin dosyasından) 'q' karakteri girilene kadar döngü devam etmektedir. Burada atama operatörüne öncelik vermek için
|
||
parantez kullanıldığına dikkat ediniz. Ayrıca getchar fonksiyonun ve stdin dosyasından okuma yapan diğer fonksiyonların tamponlu (buffered) çalıştırklarını
|
||
anımsayınız. Eğer tampon doluysa getchar yeni bir giriş istememektedir. Ancak tampon boşsa yeni bir giriş istemektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
while ((ch = getchar()) != 'q')
|
||
putchar(ch);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Virgül operatörünün önce sol tarafının sonra sağ tarafının yapıldığını ve virgül operatörünün sağ tarafındaki ifadenin değerini ürettiğini anımsayınız.
|
||
O halde yukarıdaki döngü eşdeğer olarak aşağıdaki gibi de olabilirdi.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
while (ch = getchar(), ch != 'q')
|
||
putchar(ch);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte 1'den n'e kadar sayıların toplamı hesplanmaktadır. (Tabii aslında bu toplam tek bir ifade ile de hesaplanabilirdi).
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i, n, total;
|
||
|
||
printf("Bir sayi giriniz:");
|
||
scanf("%d", &n);
|
||
|
||
i = 1;
|
||
total = 0;
|
||
while (i <= n) {
|
||
total += i;
|
||
++i;
|
||
}
|
||
printf("%d\n", total);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında scanf fonksiyonun da bir geri dönüş değeri vardır. scanf fonksiyonu başarılı bir biçimde yerleştirilen parça sayısı ile geri dönmektedir.
|
||
scanf stdin tamponunun başındaki boşluk karakterlerini (leading space) atar. Sonra format karakterlerine uygun olmayan ilk karakter gördüğünde
|
||
onu tampona geri bırakıp işlemini sonlandırır. Örneğin:
|
||
|
||
result = scanf("%d", &val);
|
||
|
||
Burada biz bir sayı yerine "ali" bir yazı girmiş olalım. Bu durumda scanf a karakterini tampondan aldığında bunun %d format karakterine uygun olmadığını tespit eder.
|
||
Bu a karakterini tampona geri bırakıp 0 değeri ile geri döner. Örneğin:
|
||
|
||
result = scanf("%d%d", &a, &b);
|
||
|
||
Burada klavyeden şunları girmiş olalım:
|
||
|
||
100 ali
|
||
|
||
scanf burada yalnızca a için yerleştirme yapabilecektir. Tamponda ali kalacaktır ve b için yerleştirme yapmayacaktır. Bu durumda scanf 1 değeri ile geri dönecektir.
|
||
Bu nedenle aşağıdaki örnekte eğer biz klavyeden bir syaı girmezsek sonsuz döngü oluşacaktır:
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
while (scanf("%d", &val), val != 0)
|
||
printf("%d\n", val * val);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki örnekte biz eğer klavyeden geçersiz bir karakter girildiğinde de döngüyü sonlandırmak istiyorsak scanf fonksiyonun geri dönüş değerine de
|
||
bakmalıyız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
while (scanf("%d", &val) && val != 0)
|
||
printf("%d\n", val * val);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
while parantezi içerisindeki ifadede önek ya da sonek ++ ya da -- operatörü kullanılabilir. Aşağıdaki örnekte önek ++ operatörü kullanılmıştır.
|
||
burada artırım önce yapılıp artırılmış değer karşılaştırmaya sokulacaktır. Dolayısıyla ilk yazılacak değer 1, son yazılacak değer 9 olacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
i = 0;
|
||
while (++i < 10)
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Eğer wehile parantezi içerisindeki ++ ya da -- operatörüğ sonek durumundaysa artırım ya da eksiltim öncelik sırasına göre yapılmakla birlikte sonraki
|
||
işleme artırılmamış ya da eksiltilmemiş değer sokulacaktır.
|
||
|
||
Aşağıdaki örnekte ilk yazdırılacak değer 1'dir. Son yazdırılacak değer ise 10 olacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
i = 0;
|
||
while (i++ < 10)
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
n bir nesne belirtmek üzere biz n defa yinelenen bir döngüyü while ile şöyle oluşturabiliriz:
|
||
|
||
while (n-- > 0) {
|
||
/* ... */
|
||
}
|
||
|
||
Bu bir kalıp olarak kullanılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int n;
|
||
|
||
n = 3;
|
||
while (n-- > 0)
|
||
printf("ok\n"); /* üç kere yinelenecek */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında while parantezinin içerisinde yalnızca sonek ++ ya da -- operatörü varsa artırmak ya da eksiltme yapılır. Ancak kontrole nesnenin artırılmamış ya da
|
||
eksiltilmemiş değeri sokulur. Dolayısıyla n pozitif olmak üzere aşağıdaki işlevsel olarak döngüler eşdeğerdir:
|
||
|
||
while (n-- > 0) {
|
||
/* ... */
|
||
}
|
||
|
||
while (n--) {
|
||
/* ... */
|
||
}
|
||
|
||
while (n-- != 0) {
|
||
/* ... */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Kontrolün sonda yapıldığı while döngüleri (do-while döngüleri) seyrek kullanılmaktadır. Genel biçimleri şöyledir:
|
||
|
||
do
|
||
<deyim>
|
||
while (<ifade>);
|
||
|
||
while parantezi sonundaki ';' boş deyim belirtmez. Kullanılması zorunlu olan sentaksın bir parçasını oluşturmaktadır. Döngünün do anahtar sözcüğü ile
|
||
başlatıldığına dikkat ediniz. Yine döngü içerisinde tek bir deyim vardır. Bu deyim basit, bileşik ya da herhangi bir deyim olabilir. Örneğin:
|
||
|
||
do
|
||
ifade1;
|
||
while (ifade2);
|
||
|
||
Örneğin:
|
||
|
||
do {
|
||
ifade1;
|
||
ifade2;
|
||
ifade3;
|
||
} while (ifade4);
|
||
|
||
do-while döngüsünde kontrol noktasının sonda olduğuna dikkat ediniz. Dolayısıyla döngü en az bir kez yinelenmektedir. Burada do anahtar sözcüğü olmasaydı
|
||
döngü kontrolün başta yapıldığı while döngüsü olarak ele alınırdı. Örneğin:
|
||
|
||
{
|
||
ifade1;
|
||
ifade2;
|
||
ifade3;
|
||
} while (ifade4);
|
||
|
||
Derleyiciye göre buarada iki deyim vardır: Bileşik deyim ve ondan bağımsız olarak kontrolün başta yapıldığı while döngüsü. Dolaysıyla buradaki ';'
|
||
boş deyim anlamına gelmektedir.
|
||
|
||
Aşağıdaki örnekte ekrana ilk çıkacak değer 0 son çıkacak değer 9'dur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
i = 0;
|
||
do {
|
||
printf("%d\n", i);
|
||
++i;
|
||
} while (i < 10);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Kontrolünm sonda yapıldığı while döngülerine yukarıda da belirttiğimiz gibi aslında oldukça seyrek gereksinim duyulmaktadır. Aşağıdaki örnekte
|
||
kullanıcıdan 'e' ya da 'h' karakteri ile bir seçim yapması istenmiştir. Eğer kullanıcı e' ya da 'h' karakterinden birini girmemişse aynı soru yinelenmiş
|
||
ve kullanıcı bu karakterlerdne birini girmeye zorlanmıştır. Buradaki döngünün kontrolün sonda yapıldığı while döngüsü olması çok daha anlamlıdır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void clear_stdin(void)
|
||
{
|
||
while (getchar() != '\n')
|
||
;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
do {
|
||
printf("(e)vet/(h)ayir?");
|
||
ch = getchar();
|
||
if (ch != '\n')
|
||
clear_stdin();
|
||
} while (ch != 'e' && ch != 'h');
|
||
|
||
if (ch == 'e')
|
||
printf("evet\n");
|
||
else
|
||
printf("hayir\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Programcılar yanlışlıkla kontrolün başta yapıldığı while döngülerini boş deyim ile kapatabilmektedir. Örneğin:
|
||
|
||
while (n-- > 0);
|
||
printf("%d\n", n);
|
||
|
||
Burada while parantezinin sonuna yerleştirilen ';' boş deyim belirtir. Dolayısıyla artık aşağıdaki printf while döngüsünün içerisinde değildir.
|
||
Eüer programcı gerçekten döngüyü boş deyim kapatmak istiyorsa (örneğin bir gecikme sağlamak istemiş olabilir) bu durumda ';' sanki bir deyim gibi
|
||
hizalanmalıdır. Çünkü kodu gören kişi bunun yanlışlıkla yapılmadığını anlayacaktır:
|
||
|
||
while (n-- > 0)
|
||
;
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int n;
|
||
|
||
n = 5;
|
||
while (n-- > 0); /* dikkat! yanlışlıkla yerleştirilmiş boş deyim */
|
||
printf("%d\n", n);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bazen sonsuz döngülerin oluşturulması gerekebilir. Bunun için while parantezi içerisine sıfırın dışında herhangi bir sayı yerleştirilebilir.
|
||
Tabii genellikle programcılar 1 sayısını tercih ederler. Örneğin:
|
||
|
||
while (1) { /* sonsuz döngü (infinite loop) */
|
||
/* ... */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
21.Ders 16/08/2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
En çok kullanılan döngüler for döngüleridir. for döngülerinin genel biçimi şöyledir:
|
||
|
||
for ([ifade1]; [ifade2]; [ifade3])
|
||
<deyim>
|
||
|
||
for anahtar sözcüğündne sonra parantezler içerisinde iki tane ';' bulunmak zorundadır. Bu iki ';' for döngüsünü üç kısma ayırır. for döngüsünün
|
||
bu kısımlarında "ifade (expression)" tanımna uyan herhangi ifadeler bulunabilir. for döngüsünün içeirsindeki deyim yine herhangi bir deyim olabilir.
|
||
|
||
for döngüleri en fazla aşağıdaki gibi karşımıza çıkar:
|
||
|
||
for (ilkdeğer; koşul; artırım) {
|
||
/* ... */
|
||
}
|
||
|
||
Örneğin:
|
||
|
||
for (i = 0; i < 10; ++i) {
|
||
/* ... */
|
||
}
|
||
|
||
for döngüsü şöyle çalışmaktadır: Önce döngüye girişte for döngüsünün birinci kısmındaki ifade bir kez çalıştırılır. Artık bu ifade bir daha çalıştırılmaz.
|
||
İkinci kısımdak ifade ilk girişte ve her yinelemede çalıştırılmaktadır. Döngü bu ikinci kısımdaki ifade sıfır dışı bir değerde olduğu sürece yinelenmektedir.
|
||
Döngünün üçüncü kısmı döngü deyimi çalıştırıldıktan sonra başa dönerken çalıştırılmaktadırç for döngüsünün çalışması tamamen aşağıdaki ile eşdeğerdir:
|
||
|
||
ifade1;
|
||
while (ifade2) {
|
||
<deyim>
|
||
ifade3;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; i < 10; ++i)
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Örneğin biz artırımı ikişer ikişer de yapabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; i < 10; i += 2)
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki döngüde belli bir değerden eksiltim uygulanarak sıfıra kadar yinelenme sağlanmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 10; i != 0; --i)
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsünün üçüncü kısmında ++ ya da -- operatörü kullanıldığında bunun önek mi yoksa sonek mi olduğunun hiçbir önemi yoktur. Örneğin:
|
||
|
||
for (i = 0; i < 10; ++i) {
|
||
/* ... */
|
||
}
|
||
|
||
ile
|
||
|
||
for (i = 0; i < 10; i++) {
|
||
/* ... */
|
||
}
|
||
|
||
eşdeğerdir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsünün üç kısmında da ifade tanımına uyan herhangi ifadeler yerleştirilebilir. Önemli olan bunun programcının amacına uygunluğudur.
|
||
Örneğin aşağıdaki gibi bir for döngüsü tamamen geçerlidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
i = 0;
|
||
for (printf("ifade1\n"); i < 3; printf("ifade3\n")) {
|
||
printf("deyim\n");
|
||
++i;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte 0'dan 6.28'e kadar sayıların sinüz değerleri 0.1 artırımla yazdırılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
int main(void)
|
||
{
|
||
double x, y;
|
||
|
||
for (x = 0; x < 6.28; x += 0.1) {
|
||
y = sin(x);
|
||
printf("%f\t%f\n", x, y);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte 1'den klavyeden girilen sayıya kadar sayıların toplamı hesaplanmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int n, total, i;
|
||
|
||
printf("Bir sayi giriniz:");
|
||
scanf("%d", &n);
|
||
|
||
total = 0;
|
||
for (i = 1; i <= n; ++i)
|
||
total += i;
|
||
|
||
printf("%d\n", total);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte isprime fonksiyonu bir sayının asal olup olmadığını tespit etmektedir. Eğer sayı asalsa fonksiyon 1 değeri ile, asal değilse 0
|
||
değeri ile geri dönmektedir. Bir döngü içerisinde return deyimini kullanırsak fonksiyon sonlanır dolayısıyla döngü de sonlanmış olur.
|
||
|
||
Aşağıdaki örnekte isprime fonksiyonundan faydalanılarak 2'den 1000'e kadar asal sayılar yan yana yazdrılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int isprime(int val)
|
||
{
|
||
int i;
|
||
|
||
for (i = 2; i < val; ++i)
|
||
if (val % i == 0)
|
||
return 0;
|
||
|
||
return 1;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 2; i < 1000; ++i)
|
||
if (isprime(i))
|
||
printf("%d ", i);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Öklit teoremine göre aslında sayı asal değilse mutlaka sayının kareköküne kadar bir çarpanı vardır. Yani sayının kareköküne kadar kontrol yapmak yeterlidir.
|
||
Ayrıca çift sayılın kontrol edilmesine de gerek yoktur. Ancak 2 için özel bir durum vardır. 2 çift olmasına karşın asal bir sayıdır.
|
||
|
||
O halde yukarıdaki isprime fonksiyonunu daha etkin çalışacak biçimde aşağıdaki gibi düzeltebiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
int isprime(int val)
|
||
{
|
||
int i;
|
||
double val_sqrt;
|
||
|
||
if (val % 2 == 0)
|
||
return val == 2;
|
||
|
||
val_sqrt = sqrt(val);
|
||
for (i = 3; i <= val_sqrt; i += 2)
|
||
if (val % i == 0)
|
||
return 0;
|
||
|
||
return 1;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 2; i < 1000; ++i)
|
||
if (isprime(i))
|
||
printf("%d ", i);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsünün birinci kısmındaki ifade hiç yazılmayabilir. Örneğin döngünün birinci kısmındaki ifade yukarıya alınırsa dğeişhen hiçbir şey olmaz:
|
||
|
||
for (ifade1; ifade2; ifade3)
|
||
<deyim>
|
||
|
||
ile
|
||
|
||
ifade1;
|
||
for(; ifade2; ifade3)
|
||
<deyim>
|
||
|
||
eşdeğerdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
i = 0;
|
||
for (; i < 10; ++i)
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsünün üçüncü kısmı da yazılmayabilir. Örneğin:
|
||
|
||
for (ifade1; ifade2; ifade3)
|
||
<deyim>
|
||
|
||
ile
|
||
|
||
ifade1;
|
||
for (; ifade2; ) {
|
||
<deyim>
|
||
ifade3;
|
||
}
|
||
|
||
eşdeğerdir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Birinci ve üçüncü kısmı olmayan for döngüleri tamamen while döngüleriyle eşdeğerdir. Örneğin:
|
||
|
||
while (ifade) {
|
||
/* ... */
|
||
}
|
||
|
||
ile
|
||
|
||
for (; ifade; ) {
|
||
/* .... */
|
||
}
|
||
|
||
eşdeğerdir.
|
||
|
||
Görüldüğü gibi for döngüsü while döngüsü gibi, while döngüsü de for döngüsü gibi kullanılabilmektedir:
|
||
|
||
ifade1;
|
||
while (ifade2) {
|
||
<deyim>
|
||
ifade3;
|
||
}
|
||
|
||
ile
|
||
|
||
|
||
for (ifade1; ifade2; ifade3)
|
||
<deyim>
|
||
|
||
eşdeğerdir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngülerinin ikinci ksımındaki ifade de hiç yazılmayabilir. Bu durumda koşulun sürekli bir biçimde sağlandığı kabul edilmektedir. Örneğin:
|
||
|
||
for (ifade1;; ifade2) {
|
||
/* .... */
|
||
}
|
||
|
||
Burada döngü sürekli yinelenir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0;; ++i) /* sonsuz döngü */
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında for döngüsünün hiçbir kısmı olmayabilir. Ancak her zaman iki tane ';' parantez içerisinde bulunmak zorundadır. Böyle for döngüleri "sonsuz döngü"
|
||
oluşturmak için kullanılabilmektedir. Örneğin:
|
||
|
||
for (;;) {
|
||
/* ... */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
double val;
|
||
|
||
for (;;) { /* sonsuz döngü */
|
||
printf("Bir deger giriniz:");
|
||
scanf("%lf", &val);
|
||
printf("%f\n", val * val);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsünün kısımlarında virgül operatörü kullanılarak ifadeler genişletilebilir. Örneğin biz for döngüsünün birinci kısmında birden fazla dğeişkene
|
||
virgül operatöründen faydalanarak değer atayabiliriz. Benzer biçimde üçüncü kısımda da virgül operatörü ile birden fazla işlem yapabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i, k;
|
||
|
||
for (i = 0, k = 100; i + k > 50; ++i, k -= 2)
|
||
printf("%d %d\n", i, k);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsü yanlışlıkla boş deyim ile kapatılabilmektedir. Bu durumda boş deyim döngü deyimi gibi ele alınır. Dolayısıyla kodun anlamı tamamen
|
||
değişir. Örneğin:
|
||
|
||
for (i = 0; i < 10; ++i);
|
||
printf("%d\n", i);
|
||
|
||
Burada döngü yanlışlıkla boş deyim ile kapatılmıştır. Bu durumda printf artık döngünün dışında kalmıştır. Tabii bazen döngü gerçekten boş deyim ile
|
||
kapatılmak istenebilir. Bu durumda ';' bir tab içeden yazılarak okunabilirlik artırılabilir. Örneğin:
|
||
|
||
for (i = 0; i < 1000000; ++i)
|
||
;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; i < 10; ++i); /* dikkat! döngü yanlışlıkla boş deyim ile kapatılmış! */
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte ilk getchar() klavyeden okunanları tampona yerleştirir ve ilk karakterin sıra numarasıyla geri döner. Sonraki getchar çağrıları
|
||
tampondaki sıradaki karakterleri alır. Tamponun sonunda ENTER tulu nedeniyle '\n' karakteri bulunacaktır. O halde aşağıdaki kodda klavyedne girilen karakterlerin
|
||
sayısı hesaplanmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; getchar() != '\n'; ++i)
|
||
;
|
||
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İç içe (nested) döngüler söz konusu olabilir. Döngü deyimleri de dışarıdan bakıldığında tek bir deyim durumundadır. Eğer bir döngünün içerisinde
|
||
başka bir döngü varsa blok açmaya hiç gerek yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i, k;
|
||
|
||
for (i = 0; i < 10; ++i)
|
||
for (k = 0; k < 10; ++k)
|
||
printf("(%d, %d)\n", i, k);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte klavyeden okunan n satır sayısı olmak üzere şu kalıp bastırılmaktadır:
|
||
|
||
*
|
||
**
|
||
***
|
||
****
|
||
...
|
||
****.... ****
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int n;
|
||
int i, k;
|
||
|
||
printf("Bir sayi giriniz:");
|
||
scanf("%d", &n);
|
||
|
||
for (i = 1; i <= n; ++i) {
|
||
for (k = 0; k < i; ++k)
|
||
putchar('*');
|
||
putchar('\n');
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'ye C99 ile birlikte C++'ta da zaten olan "for döngüsünün birinci kısmında bildirim yapabilme" olanağı eklendi. Bu kurala göre biz döngü
|
||
değişkenini doğrudan for döngüsünün birinci kısmında bildirebiliriz. Örneğin:
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
/* ... */
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", i);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsünün birinci kısmında bildirilen değişkene ilkdeğer vermek gerekir. Standartlarda burada bildirilen değişkenlere ilkdeğer vermemek
|
||
geçerli kabul edilse de toplamda anlamsızdır. Örneğin:
|
||
|
||
for (int i; i < 10; ++i) { /* geçerli ama anlamsız */
|
||
/* ... */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsünün birinci kısmında bildirilen değişkenler yalnızca o for döngüsünde kullanılabilir. Çünkü orada gizli bir bloğun olduğu kabul edilmektedir.
|
||
Yani örneğin:
|
||
|
||
for (bildirim; ifade2; ifade3)
|
||
<deyim>
|
||
|
||
döngüsünü eşdeğeri şöyledir:
|
||
|
||
{
|
||
bildirim;
|
||
for (; ifade2; ifade3)
|
||
<deyim>
|
||
}
|
||
|
||
Böylece örneğin:
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
/* ... */
|
||
}
|
||
printf("%d\n", i); /* geçersiz! i burada faaliyet göstermiyor */
|
||
|
||
Aşağıdaki örnekte her iki for döngüsündeki i aslında o for döngülerinde kullanılan farklı yerel i'lerdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", i);
|
||
printf("\n");
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", i);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki gibi bir durum da mümkündür:
|
||
|
||
int i;
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
/* burada i'yi kullanırsak for döngüsünün birinci kısmında bildirilen i olur */
|
||
}
|
||
|
||
Aşağıdaki durum da geçerli olsa da bu tür koslardan kaçınınız:
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
for (int i = 0; i < 10; ++i) { /* geçerli */
|
||
/* Burada i kullanılırsa iç for döngüsündeki i anlaşılır */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < 10; ++i)
|
||
for (int i = 0; i < 10; ++i) /* geçerli ama kötü teknik */
|
||
putchar('.');
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
for döngüsünün birinci kısmında birden fazla değişkenin bildirimi yapılabilir. Bu durumda bu değişkenlerin aynı türdne olması gerekir. Farklı türlerden
|
||
değişkenlerin birinci kısımda bildirilme olanağı yoktur. Örneğin:
|
||
|
||
for (int i = 0, k = 100; i + k > 50; ++i, k -= 2) { /* geçerli
|
||
/* ... */
|
||
}
|
||
|
||
Ancak örneğin:
|
||
|
||
for (int i = 0, double k = 0; ;) { /* geçersiz! böyle bir sentaks yok! */
|
||
/* .... */
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0, k = 100; i + k > 50; ++i, k -= 2)
|
||
printf("%d %d\n", i, k);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
break deyimi döngü deyimlerinin içerisinde ya da switch deyiminin içerisinde kullanılabilir. Genel biçimi şöyledir:
|
||
|
||
break;
|
||
|
||
Programın akışı break deyimini gördüğünde içinde bulunulan döngü deyimi sonlandırılır. Programın akışı döngü deyiminden sonraki deyim ile devam eder.
|
||
Yani break döngüyü bitirmektedir. Tabii döngü break genellikle bir koşul altında kullanılır. Örneğin:
|
||
|
||
for (;;) {
|
||
/* .... */
|
||
if (koşul)
|
||
break;
|
||
/* ... */
|
||
}
|
||
|
||
Sonsuz döngülerden çıkmak için break tek seçenektir. Ancak breajk deyimi sonsuz olmayan döngülerde de kullanılabilir.
|
||
|
||
Bazen döngülerden çıkış koşulları çok çeşitli olabilmektedir. Bu tür durumlarda programcılar döngüyü sonsuz döngü yapıp içeriden break ile çıkmayı
|
||
tercih edebilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
double val;
|
||
|
||
for (;;) {
|
||
printf("Bir sayi giriniz:");
|
||
scanf("%lf", &val);
|
||
if (val == 0)
|
||
break;
|
||
printf("%f\n", val * val);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
22.Ders 18/08/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İç içe döngülerde break deyimi yalnızca kendi döngüsünü sonlandırmaktadır. Yani break hangi döngünün içerisinde kullanılmışsa yalnızca onu
|
||
kırmaktadır.
|
||
|
||
Aşağıdaki örnekte ENTER tuşuna basıldığında iç döngü sonraki yinelemeyle devam eder. q tuşuyna basıldığında önce iç döngüdeki break ile iç döngüden
|
||
çıkılır, sonra dış döngüdeki break ile dış döngüden de çıkılır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void clear_stdin(void)
|
||
{
|
||
while (getchar() != '\n')
|
||
;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
for (int k = 0; k < 10; ++k) {
|
||
printf("(%d, %d)\n", i, k);
|
||
printf("Press ENTER to continue or q to exit:");
|
||
ch = getchar();
|
||
if (ch != '\n')
|
||
clear_stdin();
|
||
if (ch == 'q')
|
||
break;
|
||
}
|
||
if (ch == 'q')
|
||
break;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte bir prompt çıkartılmıştır. Bu prompt eşliğinde tek karakterli komutlar istenmektedir. q tuşuna basıldığında komut yorumlayıcıdan
|
||
çıkılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void clear_stdin(void)
|
||
{
|
||
while (getchar() != '\n')
|
||
;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
for (;;) {
|
||
printf("CSD>");
|
||
|
||
ch = getchar();
|
||
if (ch != '\n')
|
||
clear_stdin();
|
||
|
||
if (ch == 'q')
|
||
break;
|
||
if (ch == 'r')
|
||
printf("remove command executes...\n");
|
||
else if (ch == 'c')
|
||
printf("copy command executes...\n");
|
||
else if (ch == 'd')
|
||
printf("dir command executes...\n");
|
||
else if (ch == 'm')
|
||
printf("move command executes...\n");
|
||
else
|
||
printf("invalid command: %c\n", ch);
|
||
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki örnekteki programı baştaki SPACE ve TAB karakterlerini atacak biçimde geliştirebiliriz. Aynı zamanda baştak SPACE ve TAB atıldıktan sonra ENTER
|
||
tuşuna basılırsa yeni bir prompt'a geçecek biçimde programı düzenleyebiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int get_command(void)
|
||
{
|
||
int ch;
|
||
|
||
while ((ch = getchar()) == ' ' || ch == '\t')
|
||
;
|
||
|
||
if (ch != '\n')
|
||
while (getchar() != '\n')
|
||
;
|
||
|
||
return ch;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
for (;;) {
|
||
printf("CSD>");
|
||
|
||
ch = get_command();
|
||
|
||
if (ch != '\n') {
|
||
if (ch == 'q')
|
||
break;
|
||
if (ch == 'r')
|
||
printf("remove command executes...\n");
|
||
else if (ch == 'c')
|
||
printf("copy command executes...\n");
|
||
else if (ch == 'd')
|
||
printf("dir command executes...\n");
|
||
else if (ch == 'm')
|
||
printf("move command executes...\n");
|
||
else
|
||
printf("invalid command: %c\n", ch);
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
if deyiminin doğru kısmında break ya da return gibi deyimler varsa if deyimine else kısmının konulmasının bir anlamı olmaz. Örneğin:
|
||
|
||
if (ifade1)
|
||
break;
|
||
else
|
||
ifade2;
|
||
|
||
ile aşağıdaki eşdeğerdir:
|
||
|
||
if (ifade1)
|
||
break;
|
||
ifade2;
|
||
|
||
Aşağıdaki if deyimine bakınız:
|
||
|
||
if (ifade1 && ifade2)
|
||
ifade3;
|
||
|
||
Bu işlemin eşdeğeri şöyledir:
|
||
|
||
if (ifade1)
|
||
if (ifade2)
|
||
ifade3;
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
continue deyimi yalnızca döngü içerisinde kullanılaabilen bir deyimdir. Genel biçimi şöyledir:
|
||
|
||
continue;
|
||
|
||
Programın akışı continue deyimini gördüğünde döngünün içerisindeki deyim sonlandırılıp yeni bir yinelemeye geçilmektedir. Yani break deyimi döngü
|
||
deyiminin kendisini sonlandırırken, continue deyimi döngü içerisindeki deyimin sonlandırılmasına yol açar. continue seyrek kullanılan bir deyimdir.
|
||
continue deyimi for döngüsü içerisinde kullanılırsa yeni bir yineleme oluşacağı için for döngüsünün üçüncü kısmı başa dönüşte yapılacaktır.
|
||
|
||
Aşağıdaki örnekte i çift iken akış continue deyimini görür. Böylece döngüde yeni bir yinelemeye geçilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < 10; ++i) {
|
||
if (i % 2 == 0)
|
||
continue;
|
||
printf("%d\n", i);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
continue deyimi özelllikle döngüler içerisindeki geniş if bloklarını elimine etmek için kullanılmaktadır. Örneğin:
|
||
|
||
for (;;) {
|
||
ch = get_command();
|
||
if (ch != '\n') {
|
||
/* ... */
|
||
}
|
||
}
|
||
|
||
Bu işlemin eşdeğeri şöyle oluşturulabilir:
|
||
|
||
for (;;) {
|
||
ch = get_command();
|
||
if (ch == '\n')
|
||
continue;
|
||
/* ... */
|
||
}
|
||
|
||
Tabii bazen bir döngü içerisinde pek çok yerde akışın başa sarılması istenebilir. Bu tür durumlarda continue tasarımı oldukça sade göstermektedir.
|
||
continue deyimi de iç içe döngülerde yalnızca iç döngüyü başa sarar.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int get_command(void)
|
||
{
|
||
int ch;
|
||
|
||
while ((ch = getchar()) == ' ' || ch == '\t')
|
||
;
|
||
|
||
if (ch != '\n')
|
||
while (getchar() != '\n')
|
||
;
|
||
|
||
return ch;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
for (;;) {
|
||
printf("CSD>");
|
||
|
||
ch = get_command();
|
||
if (ch == '\n')
|
||
continue;
|
||
|
||
if (ch == 'q')
|
||
break;
|
||
if (ch == 'r')
|
||
printf("remove command executes...\n");
|
||
else if (ch == 'c')
|
||
printf("copy command executes...\n");
|
||
else if (ch == 'd')
|
||
printf("dir command executes...\n");
|
||
else if (ch == 'm')
|
||
printf("move command executes...\n");
|
||
else
|
||
printf("invalid command: %c\n", ch);
|
||
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yalnızca sabitlerden ve operatörlerden oluşan ifadelere "sabit ifadeleri (constant expression)" denilmektedir. Örneğin:
|
||
|
||
3
|
||
2 + 5
|
||
2 + 5 * 3
|
||
1 + 2 + 3 + 4
|
||
|
||
birer sabit ifadesidir. Sabit ifadelerinin derleme aşamasında derleyici tarafından sayısal değerleri hesaplanabilmektedir. Pek çok derleyici
|
||
sabit ifadelerini derleme işlemi sırasında hesaplar böylece bu işlemlerin gereksiz bir biçimde programın çalışma zamanı sırasında yapılmasını engeller.
|
||
Bu optimizasyon temasına "constant folding" denilmektedir. C'de bazı durumlarda sabit ifadelerinin kullanılması zorunludur. Örneğin:
|
||
|
||
- Global değişkenlere verilen ilkdeğerlerin sabit ifadesi olması zorunludur.
|
||
- Global dizilerde (C99 öncesi tüm dizilerde) uzunluk sabit ifadeleriyle belirtilmek zorundadır.
|
||
- case ifadeleri sabit ifadesi olmak zorundadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
switch deyimi bir ifadenin çeş,tl, sayısal değerleri için farklı birtakım işlemlerin yapılması için düşünülmüş bir deyimdir. switch deyimi olmasaydı
|
||
aslında gereksinim duyulan şeyler if deyimleriyle de yapılabilirdi. Ancak switch deyimi okunabilirliği artırmaktadır ve bazı durumlarda derleyicinin daha etkin
|
||
kod üretmesini sağlamaktadır. switch deyiminin tipik genel biçimi şöyledir:
|
||
|
||
switch (<ifade>) {
|
||
case <s.i>:
|
||
/* ... */
|
||
[break;]
|
||
case <s.i>:
|
||
/* ... */
|
||
[break;]
|
||
case <s.i>:
|
||
/* ... */
|
||
[break;]
|
||
/* ... */
|
||
[default:
|
||
/* .... */
|
||
]
|
||
}
|
||
|
||
switch anahttar sözcüğünden sonra parantez içerisinde bir ifade bulunmak zorundadır. switch deyimi tipik olarak case bölümlerinden oluşur.
|
||
case anahtar sözcüğünden sonra sabit ifadesi bulunması gerekir. case bölümleri tipik olarak break deyimleriyle sonlandırılmaktadır. Ancak bu zorunlu
|
||
değildir. switch deyiminin isteğe bağlı bir default bölümü olabilir.
|
||
|
||
switch deyimi şöyle çalışmaktadır: Önce switch parantezi içerisindeki ifadenin sayısal değeri hesaplanır. Sonra bu değere tam eşit olan case bölümü
|
||
araştırılır. Eğer bu değere eşit olan bir case bölümü varsa akış o bölüme aktarılır. O bölümdeki deyimler çalıştırılır. break deyimi döngülerde olduğu gibi
|
||
switch deyiminin de sonlandırılmasına yol açmaktadır. Eğer switch parantezi içerisindeki ifadenin değeri ile eşit olan bir case bölümü yoksa ancak
|
||
default bölüm varsa akış default bölüme aktarılmaktadır. default bölüm olmak zorunda değildir. Eğer switch parantezi içerisindeki ifadenin sayısal değerine
|
||
eşit olan bir case bölümü yoksa ve default bölüm de yoksa akış switch deyimiminin dışındaki ilk deyimle devam eder.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &val);
|
||
|
||
switch (val) {
|
||
case 1:
|
||
printf("bir\n");
|
||
break;
|
||
case 2:
|
||
printf("iki\n");
|
||
break;
|
||
case 3:
|
||
printf("üc\n");
|
||
break;
|
||
case 4:
|
||
printf("dort\n");
|
||
break;
|
||
case 5:
|
||
printf("bes\n");
|
||
break;
|
||
default:
|
||
printf("hicbiri\n");
|
||
break;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aynı switch deyiminde aynı değerde birden fazla case bölümü olamaz. case anahtar sözcüğünün yanındaki ifadenin sabit ifadesi olması zorunludur.
|
||
Bu sayede derleyici case ifadelerinin yinelenmediğini derleme aşamasında tespit edebilmektedir.
|
||
|
||
case bölümlerinin tamsayı türlerine ilişkin sabit ifadesi olması zorunludur. Yani case anahtar sözcüğünün yanında float, double, long double gibi
|
||
noktalı sayılar bulunamaz. Benzer biçimde switch parantezi içerisindeki ifade de tamsayı türlerine ilişkin olmak zorundadır.
|
||
|
||
switch deyiminde case bölümlerinin sıralı (sorted) olması ya da default bölümün sonda olması bir zorunluluk değildir. Ancak case bölümlerinin sıralı olması
|
||
ve default bölümün sonda olması okunabilirliği artırabilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &val);
|
||
|
||
switch (val) {
|
||
case 5:
|
||
printf("bes\n");
|
||
break;
|
||
case 1:
|
||
printf("bir\n");
|
||
break;
|
||
default:
|
||
printf("hicbiri\n");
|
||
break;
|
||
case 2:
|
||
printf("iki\n");
|
||
break;
|
||
|
||
case 4:
|
||
printf("dort\n");
|
||
break;
|
||
case 3:
|
||
printf("üc\n");
|
||
break;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
23. Ders 23/08/2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de switch deyiminde "aşağıda doğru düşme (fall through)" denilen bir özellik vardır. Akış bir case bölümüne devredildikten sonra o case bölümünün
|
||
sonunda break yok ise aşağıda doğru akmaya devam eder. İlk break görüldüğünde switch'ten çıkılır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &val);
|
||
|
||
switch (val) {
|
||
case 1:
|
||
printf("bir\n"); // fallthrough
|
||
case 2:
|
||
printf("iki\n");
|
||
break;
|
||
case 3:
|
||
printf("üc\n"); // fallthrough
|
||
case 4:
|
||
printf("dort\n");
|
||
break;
|
||
case 5:
|
||
printf("bes\n");
|
||
break;
|
||
default:
|
||
printf("hicbiri\n");
|
||
break;
|
||
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Frklı case değerleri için aynı şeylerin yapılması isteniyorsa bunun en pratik yöntemi aşağıdaki gibidir:
|
||
|
||
case 1:
|
||
case 2:
|
||
...
|
||
break;
|
||
|
||
Bunun daha pratik bir yolu yoktur. Burada switch ifadesi 1 ise fallthrouh nedeniyle zaten 2 ile aynı kod çalıştırılacaktır.
|
||
|
||
Aşağıdaki örnekte komut satırı uygulaması swithc deyimi ile yapılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int get_command(void)
|
||
{
|
||
int ch;
|
||
|
||
while ((ch = getchar()) == ' ' || ch == '\t')
|
||
;
|
||
|
||
if (ch != '\n')
|
||
while (getchar() != '\n')
|
||
;
|
||
|
||
return ch;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
for (;;) {
|
||
printf("CSD>");
|
||
|
||
ch = get_command();
|
||
if (ch == '\n')
|
||
continue;
|
||
if (ch == 'q')
|
||
break;
|
||
|
||
switch (ch) {
|
||
case 'e':
|
||
case 'r':
|
||
printf("remove command executes...\n");
|
||
break;
|
||
case 'c':
|
||
printf("copy command executes...\n");
|
||
break;
|
||
case 'd':
|
||
printf("dir command executes...\n");
|
||
break;
|
||
case 'm':
|
||
printf("move command executes...\n");
|
||
break;
|
||
default:
|
||
printf("invalid command: %c\n", ch);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
switch deyiminde hiçbir henüz case ya da default bölümlerinden önce kodlar yerleştirilebilir. Bildirimler yapılabilir.
|
||
Burada yapılan bildirimler geçerlidir. Ancak buraya yerleştirilen kodlar çalıştırılmazlar. Dolayısıyla buradaki
|
||
tanımlamada nesneye ilkdeğer veriliyor olsa bile bu ilkdeğerler işleme sokulmamaktadır. Örneğin:
|
||
|
||
switch (expr) {
|
||
int i = 4;
|
||
f(i);
|
||
//...
|
||
default:
|
||
printf("%d\n", i);
|
||
}
|
||
|
||
Burada i değişkeninin bildirimi geçerlidir. Ancak akış hiçbir zaman o noktaya gelemeyeceği için verilen ilkdeğer
|
||
nesneye yerleştirilemeyecektir. f fonksiyonu da hiçbir zaman çağrılmayacaktır. Bu tür durumlarda bu işlemlerin switch
|
||
yukarısına alınması gerekir. Örneğin:
|
||
|
||
int i = 4;
|
||
f(i);
|
||
switch (expr) {}
|
||
// ...
|
||
default:
|
||
printf("%d\n", i);
|
||
}
|
||
|
||
Ayrıca case ya da default bölümlerinde bildirim yapılamamaktadır. Örneğin:
|
||
|
||
switch (expr) {
|
||
case 1:
|
||
int a; // geçersiz!
|
||
//...
|
||
}
|
||
|
||
Yalnızca bir case ya da default bölümde değişkenin kullanılmasını istiyorsanız. Orada ayrı bir blok oluşturmalısınız.
|
||
Örneğin:
|
||
|
||
switch (expr) {
|
||
case 1:
|
||
{
|
||
int a; // geçerli
|
||
//...
|
||
}
|
||
//...
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
switch deyimlerinde case bölümlerinin çok uzatılması okunabilirliği bozmaktadır. Bu nedenle case bölümlerinde uzun işlemler yapılacaksa
|
||
o işlemleri yapan fonksiyonlar tanımlanmalı ve case bölümlerinde bu fonksiyonlar çağrılmalıdır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int get_command(void)
|
||
{
|
||
int ch;
|
||
|
||
while ((ch = getchar()) == ' ' || ch == '\t')
|
||
;
|
||
|
||
if (ch != '\n')
|
||
while (getchar() != '\n')
|
||
;
|
||
|
||
return ch;
|
||
}
|
||
|
||
void erase_command(void)
|
||
{
|
||
printf("erase command executes...\n");
|
||
}
|
||
|
||
void copy_command(void)
|
||
{
|
||
printf("copy command executes...\n");
|
||
}
|
||
|
||
void dir_command(void)
|
||
{
|
||
printf("dircommand executes...\n");
|
||
}
|
||
|
||
void move_command(void)
|
||
{
|
||
printf("move command executes...\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
for (;;) {
|
||
printf("CSD>");
|
||
|
||
ch = get_command();
|
||
if (ch == '\n')
|
||
continue;
|
||
if (ch == 'q')
|
||
break;
|
||
|
||
switch (ch) {
|
||
case 'e':
|
||
case 'r':
|
||
erase_command();
|
||
break;
|
||
case 'c':
|
||
copy_command();
|
||
break;
|
||
case 'd':
|
||
dir_command();
|
||
break;
|
||
case 'm':
|
||
move_command();
|
||
break;
|
||
default:
|
||
printf("invalid command: %c\n", ch);
|
||
break;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de case bölümlerinin hemen switch bloğüunun içerisinde olması zorunlu değildir. Bir case bölümü başka bir case bölümünün içerisinde bir yerlerde
|
||
olabilir. C# ve Java gibi dillerde böyle bir özellik yoktur. Örneğin:
|
||
|
||
switch (ifade) {
|
||
case 1:
|
||
...
|
||
...
|
||
if (falanca) {
|
||
...
|
||
case 2:
|
||
.....
|
||
.....
|
||
break;
|
||
}
|
||
break;
|
||
...
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a, b;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
switch (a) {
|
||
case 1:
|
||
printf("bir\n");
|
||
printf("Bir deger daha giriniz:");
|
||
scanf("%d", &b);
|
||
if (b > 0) {
|
||
case 2:
|
||
printf("islemler devam ediyor..\n");
|
||
break;
|
||
}
|
||
break;
|
||
case 3:
|
||
printf("uc\n");
|
||
break;
|
||
default:
|
||
printf("default\n");
|
||
break;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında switch içerisind tek bir deyim varsa switch deyimi de bloklanmak zorunda değildir. Örneğin:
|
||
|
||
switch (ifade)
|
||
case 1:
|
||
printf("bir\n"); printf("iki\n");
|
||
|
||
Burada yalnızca ilk printf printf çağrısı switch deyiminin içerisindedir. İkinci printf switch içerisinde değildir. Standartlarda case bir deyim olarak
|
||
şöyle ifade edilmiştir:
|
||
|
||
case <sabit ifadesi>: deyim
|
||
|
||
switch (ifade)
|
||
case 1: {
|
||
printf("bir\n"); printf("iki\n");
|
||
}
|
||
|
||
Burada switch içerisinde yine tek bir deyim vardır. O deyim de bileşik deyimdir. Dolayısıyla iki printf çağrısı da switch içerisinddir. Örneğin:
|
||
|
||
switch (a)
|
||
case 1:
|
||
printf("bir\n");
|
||
break; /* geçersiz! */
|
||
|
||
Burada switch içerisinde yalnızca printf çağrısı vardır. break deyimi switch içerisinde değildir. switch ve döngü içerisinde olmayan break deyimleri
|
||
geçersizdir.
|
||
|
||
C standartlarında aslında switch deyiminin genel biçimi şöyle verilmiştir:
|
||
|
||
switch (<ifade>)
|
||
<deyim>
|
||
|
||
Yani bu genel biçime göre aslında switch deyimi case deyimini içermeyebilir. Ancak case içermeyen switch deyimleri geçerli olsa da anlamlı değildir. Örneğin:
|
||
|
||
switch (ifade) { /* geçerli ama anlamlı değil */
|
||
ifade1;
|
||
ifade2;
|
||
}
|
||
|
||
İşte switch deyiminde eğer bloklama yapılmazsa onun içeriinde tek deyimin olduğu kabul edilmektedir. Örneğin:
|
||
|
||
switch (ifade)
|
||
ifade1; ifade2;
|
||
|
||
Burada switch içerisinde case bölümü ya da default bölümü olmadığına göre switch deyimi anlmasızdır. Ancak gerçerlidir. Burada ifade2 switch deyimi
|
||
dışındadır.
|
||
|
||
Bir döngü içerisinde bir switch deyimi olsun. Bu switch deyimi içerisinde break kullandığımızda biz switch deyimini sonlandırmış oluruz. Döngü deyimini
|
||
sonlandırmış olmayız. Ancak continue deyimi switch için anlamlı olmadığına göre döngü içerisindeki switch deyiminde continue kullanıldığında
|
||
switch deyimi de sonlanarak sonraki yinelemeye geçilir. Örneğin:
|
||
|
||
for (;;) {
|
||
switch (ifade) {
|
||
case 1:
|
||
...
|
||
if (falanca)
|
||
continue; /* bu continue döngü başına dönüşü sağlar
|
||
...
|
||
}
|
||
....
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
goto deyimi programın akışını koşulsuz biçimde belli bir noktaya aktarmak için kullanılan bir kontrol deyimidir. Genel biçimi şöyledir:
|
||
|
||
goto <label>;
|
||
.....
|
||
<label:>
|
||
....
|
||
|
||
goto anahtar sözcüğünün yanınada isimlendirme kuralına uygun bir isim bulunur. Bu isme "etiket (label)" denilmektedir.
|
||
Daha sonra bu etiket ':' atomuyla fonksiyonda bir yerde bulundurulmak zorudandır. Etiket akışın aktarılacağı yeri belirtir.
|
||
Örneğin:
|
||
|
||
if (ifade)
|
||
goto EXIT;
|
||
...
|
||
EXIT:
|
||
....
|
||
|
||
Etiketler genelikle programcılar tarafından büyük harflerle isimlendirilmektedir.
|
||
|
||
goto deyimi döngü oluşturmak için kullanılmamalıdır. Çünkü goto deyimleri programın okunabilirliğini, anlaşılabilirliğini
|
||
bozabilmektedir. Aşağıdaki örnekte goto deyiminin çalışmasına ilişkin bir örnek veriyoruz. Ancak goro deyimi aşağıdaki
|
||
örnekte olduğu gibi döngü oluşturmak için kullanılmamalıdır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
i = 0;
|
||
REPEAT:
|
||
printf("%d\n", i);
|
||
++i;
|
||
if (i < 10)
|
||
goto REPEAT;
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
goto deyimi ile başka bir fonksiyona atlanamaz. Aynı fonksiyon içerisinde başka bir yere atlanabilir. Etiketler
|
||
yalnızca goto işleminde etki gösterir. Yoksa programın akışı sırasında etiketle karşılaşılmasının bir etkisi yoktur.
|
||
Kaldı ki bir etiketin bulunyor olması bir goto bulundurulmasını zornlu kılmamaktadır. Tabii goto'suz etiketlerin de
|
||
bir anlamı yoktur. Bir etikete birden fazla yerden goto yapılabilir. Örneğin:
|
||
|
||
if (falanca)
|
||
goto EXIT;
|
||
/* ... */
|
||
if (filanca)
|
||
goto EXIT;
|
||
/* ... */
|
||
EXIT:
|
||
/* .... */
|
||
|
||
goto etiketleri "fonksiyon faaliyet alanına (function scope)" ilişkindir. Yani bir fonksiyon içerisinde aynı isimli tek
|
||
bir goto etiketi olabilir. goto etiketlerinin blok faaliyet alanına ilişkin olmadığına dikkat ediniz. Örneğin:
|
||
|
||
int main(void)
|
||
{
|
||
/* ... */
|
||
{
|
||
/* ... */
|
||
EXIT:
|
||
printf("exit\n");
|
||
}
|
||
/* ... */
|
||
|
||
{
|
||
/* ... */
|
||
EXIT: /* geçersiz! */
|
||
printf("exit\n");
|
||
}
|
||
return 0;
|
||
}
|
||
|
||
Standratlara göre goto etiketinden sonra bir deyim bulunmak zorundadır. Çünkü aslında bir deyim için goto yapılmaktadır.
|
||
Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
/* ... */
|
||
|
||
if (falanca)
|
||
goto EXIT;
|
||
|
||
/* ... */
|
||
EXIT: /* geçersiz! etiketten sonra bir deyim olması gerekir */
|
||
}
|
||
|
||
Bu tür durumlarda boş deyimden faydalanılabilir. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
/* ... */
|
||
|
||
if (falanca)
|
||
goto EXIT;
|
||
|
||
/* ... */
|
||
|
||
EXIT: /* geçerli, goto etiketinden sonra bir deyim var */
|
||
;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
goto deyimi üç durumda anlamlı ve güzel bir biçimde kullanılabilir:
|
||
|
||
1) İç içe döngülerden ya da döngü içerisindeki switch deyiminden tek hamlede çıkmak için
|
||
2) Ters sırada kaynak boşaltımı yapmak için
|
||
3) Bazı özel algoritmalarda çözümü kolaylaştırmak için
|
||
|
||
Aşağıdaki örnekte iç bir döngüden tek hamlede goto ile çıkılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
for (int k = 0; k < 10; ++k) {
|
||
printf("(%d,%d)\n", i, k);
|
||
printf("press q to exit:");
|
||
ch = getchar();
|
||
if (ch == 'q')
|
||
goto EXIT;
|
||
if (ch != '\n')
|
||
while (getchar() != '\n')
|
||
;
|
||
}
|
||
}
|
||
|
||
EXIT:
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte bir döngü içerisindeki switch deyiminden tek hamlede goto ile çıkılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int get_command(void)
|
||
{
|
||
int ch;
|
||
|
||
while ((ch = getchar()) == ' ' || ch == '\t')
|
||
;
|
||
|
||
if (ch != '\n')
|
||
while (getchar() != '\n')
|
||
;
|
||
|
||
return ch;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
for (;;) {
|
||
printf("CSD>");
|
||
|
||
ch = get_command();
|
||
if (ch == '\n')
|
||
continue;
|
||
|
||
switch (ch) {
|
||
case 'e':
|
||
case 'r':
|
||
printf("remove command executes...\n");
|
||
break;
|
||
case 'c':
|
||
printf("copy command executes...\n");
|
||
break;
|
||
case 'd':
|
||
printf("dir command executes...\n");
|
||
break;
|
||
case 'm':
|
||
printf("move command executes...\n");
|
||
break;
|
||
case 'q':
|
||
goto EXIT;
|
||
default:
|
||
printf("invalid command: %c\n", ch);
|
||
break;
|
||
}
|
||
}
|
||
EXIT:
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Kaynak tahsisatlarında ters sırada boşaltım yapmak için goto deyminden faydalanılmaktadır. Biz kursumuzun bu noktasında henüz bu tür kavramları görmedik.
|
||
Ancak yine kavramsal bir örnek verebiliriz. alloc_resource isimli bir fonksiyon bir kaynağı yahsis ediyor olsun. Eğer tahsisat başarısızsa 0 değerine
|
||
geri dönüyor olsun. Biz de bu fonksiyonla bir dizi kaynağı tahsis etmek isteyelim. Tahsisatı geri bırakan free_Source isimli bir fonksiyonun olduğunu düşünelim.
|
||
Örneğin:
|
||
|
||
int foo(void)
|
||
{
|
||
int r1, r2, r3, r4, r5;
|
||
|
||
r1 = alloc_resource();
|
||
if (!r1) {
|
||
return 0;
|
||
}
|
||
r2 = alloc_resource();
|
||
if (!r2) {
|
||
free_resource(r1);
|
||
return 0;
|
||
}
|
||
r3 = alloc_resource();
|
||
if (!r3) {
|
||
free_resource(r1);
|
||
free_resource(r2);
|
||
return 0;
|
||
}
|
||
r4 = alloc_resource();
|
||
if (!r4) {
|
||
free_resource(r1);
|
||
free_resource(r2);
|
||
free_resource(r3);
|
||
return 0;
|
||
}
|
||
r5 = alloc_resource();
|
||
if (!r5) {
|
||
free_resource(r1);
|
||
free_resource(r2);
|
||
free_resource(r3);
|
||
free_resource(r4);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* işlemler yapılıyor */
|
||
|
||
free_resource(r1);
|
||
free_resource(r2);
|
||
free_resource(r3);
|
||
free_resource(r4);
|
||
free_resource(r5);
|
||
|
||
return 1; /* başarılı */
|
||
|
||
}
|
||
|
||
Burada kod tekrarı oldukça kötü bir yazım oluşturmaktadır. İşte bu tür durumlarda goto ile ters sırada boşaltım uygulayabiliriz:
|
||
|
||
int foo(void)
|
||
{
|
||
int r1, r2, r3, r4, r5;
|
||
|
||
r1 = alloc_resource();
|
||
if (!r1)
|
||
goro EXIT1;
|
||
|
||
r2 = alloc_resource();
|
||
if (!r2)
|
||
goto EXIT2;
|
||
|
||
r3 = alloc_resource();
|
||
if (!r3)
|
||
goto EXIT3;
|
||
|
||
r4 = alloc_resource();
|
||
if (!r4)
|
||
goto EXIT4;
|
||
|
||
r5 = alloc_resource();
|
||
if (!r5)
|
||
goto EXIT5;
|
||
|
||
/* işlemler yapılıyor */
|
||
|
||
return 1; /* başarılı */
|
||
|
||
EXIT5:
|
||
free_resource(r4);
|
||
EXIT4:
|
||
free_resource(r3);
|
||
EXIT3:
|
||
free_resource(r2);
|
||
EXIT2:
|
||
free_resource(r1);
|
||
EXIT1:
|
||
return 0; /* başarısız */
|
||
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İç bir bloğa goto ile atlanırken o blokta tanımlanan değişkenler çöp değer almış olabilirler. Bu tür durumlara dikkat
|
||
ediniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
printf("Bir sayi giriniz: ");
|
||
scanf("%d", &val);
|
||
if (val == 0)
|
||
goto INSIDE;
|
||
|
||
{
|
||
int a;
|
||
|
||
a = 10;
|
||
INSIDE:
|
||
printf("%d\n", a);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
24. Ders 23/08/2022 - Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bir atama işlemi sırasında atanan değerin türüne "kaynak tür", atanan değere "kaynak değer" ve atamanın yapıldığı
|
||
nesnenin türüne ise "hedef tür" denilmektedir. Atama işlemi sırasında kaynak türle hedef tür farklı türler olabilir.
|
||
Örneğin kaynak tür double iken hedef tür int olabilir. Farklı türlerin birbirlerine atanması sırasında kaynak türdeki
|
||
değer hedef türe dönüştürülür sonra atama gerçekleştirilir. Örneğin:
|
||
|
||
int = double
|
||
|
||
gibi bir atama söz konusu olsun. Bu durumda önce double türü int türüne dönüştürülür sonra atama gerçekleştirilir. Buna
|
||
"otomatik tür dönüştürmesi (implicit type conversion)" denilmektedir. Yani bir türden bir türe atamanın olması o türden
|
||
o türe otomatik dönüştürmenin olması anlamına gelir. C'de nümerik türler arasında otomatik dönüştürme vardır. Ancak bu
|
||
dönüştürmeler sırasında bilgi kaybı söz konusu olabilmektedir. Bu durumda programcının nasıl bir kayıp ile karşı karşıya
|
||
kalacağını kestirmesi gerekir. Swift gibi Rust gibi bazı dillerde hiçbir zaman farklı türler birbirilerine atanamamaktadır.
|
||
Farklı türlerin birbirlerine atanabilirliğine programlama dillerinde "katı tür sistemi (strong type)" ve "gevşek tür sistemi
|
||
(weakly type)" denilmektedir. C bu bakımdan gevşek tür tür sistemine sahip bir programlama dilidir.
|
||
|
||
Farklı türlerin birbirlerine atanmasında şu kurallar izlenmektedir (buradaki maddeleri else-if biçiminde değerlendirmelisiniz):
|
||
|
||
1) Kaynak türdeki değer hedef türün sınırları içerisinde kalıyorsa bilgi kaybı söz konusu olmaz. Örneğin long long int
|
||
bir değeri int türüne atamak isteyelim: Eğer long long int içerisindeki değer int türünün sınırları içerisinde kalıyorsa
|
||
bilgi kaybı söz konusu olmaz. Örneğin her int değer long türünün sınırları içerisinde kalmaktadır. Bu durumda C'de int türünden
|
||
long türüne yapılan atamalarda bir bilgi kaybı söz konusu olmaz.
|
||
|
||
2) Büyük tamsayı türünden küçük sayı türüne yapılan atamalarda hedef tür işaretsiz bir tamsayı türü ise kaynak türdeki değerin
|
||
yüksek anlamlı (most significant) byte'ları kaybedilir, düşük anlamlı (least significant) byte'ları atanır. Ancak hedef tür
|
||
işaretli bir tamsayı türü ise bilgi kaybının nasıl olacağı derleyicileri yazanların isteğine bırakılmıştır (implementetion
|
||
dependent). Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 0x12345678;
|
||
unsigned short b;
|
||
|
||
b = a;
|
||
|
||
printf("%x\n", b); /* 5678 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Bu tür durumlarda sayılar 10'luk sistemdeyse sanki atama işleminde rastgele bir değer elde ediliyormuş duygusuna kapılınabilir.
|
||
Aslında yüksek byte'ler atılıp düşük anlamlı byte'lar atanmaktadır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 56732683; /* 0x0361AC0B */
|
||
unsigned short b;
|
||
|
||
b = a;
|
||
|
||
printf("%u\n", b); /* 0xAC0B = 44043 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Büyük tamsayı türünden küçük tamsayı türüne atama yapılırken hedef tür olan küçük tamsayı türü işaretli bir tür ise bu durumda
|
||
bilgi kaybının nasıl olacağı derleyicileri yazanların istedğine bırkaılmışsa da derleyicilerin hemen hepsi yine saının yüksek anlamlı byte'larını
|
||
atmaktadır. Tabii düşük anlamlı byte atandığı zaman sayının işareti de değişebilmektedir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 56732683; /* 0361AC0B */
|
||
short b;
|
||
|
||
b = a;
|
||
|
||
printf("%d\n", b); /* 0xAC0B = -21493 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
3) Kaynak tür işaretli bir tamsayı türü, hedef tür de kaynak türün işaretsiz biçimi ise bu durumda sayının bit kalıbı değişmez.
|
||
Yani hedef türdeki değer aynı byte değerleriyle kaynak türe atanır. Başka bir deyişle kaynak türdeki değerin bit karşılığının
|
||
tamamı hedef türde depolanır. Tabii işaret bitinin anlamı değişecektir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned int a = 0xAB8254C2;
|
||
int b;
|
||
|
||
b = a;
|
||
|
||
printf("%u\n", b); /* 0xFFFFFFFF = 4294967295 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
İşaretsiz bir tamsayı türü aynı türün işaretli biçimine atanırsa zaten eğer kaynak değer hedef türün sınırları içerisinde
|
||
kalıyorsa 1. Madde uygulanır. Kalmıyorsa bilgi kaybının nasıl olacağı derleyicileri yazanların isteğine bırakılmıştır. Fakat
|
||
derleyicilerin hemen hepsi yine sayının bit kalıbını değiştirmeden hedef türe atamaktadır.
|
||
|
||
4) Küçük işaretli tamsayı türünden büyük işaretsiz tamsayı türüne atama yapılırken eğer küçük işaretli tamsayı türü pozitifse
|
||
zaten bilgi kaybı söz konusu olmaz. Ancak negatifse bu durumda dönüştürme iki aşamada yapılmaktadır: Önce küçük işaretli
|
||
türdeki değer büyük türün işaretli biçimine dönüştürülür, sonra büyük türün işaretli biçiminden büyük türün işaretsiz biçimine
|
||
dönüştürme yapılır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
signed char a = -1;
|
||
unsigned int b;
|
||
|
||
b = a; /* önce signed char, signed int türüne sonra da unsigned int türüne dönüştürülür */
|
||
|
||
printf("%u\n", b); /* 4294967295 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
5) Gerçek sayı türlerinden tamsayı türlerine yapılan atamalarda eğer gerçek sayı türü içerisindeki değer tamsayı türü
|
||
ile ifade edilebiliyorsa" zaten 1. Madde uygulanır, bilgi kaybı söz konusu olmaz. Ancak kaynak türdeki gerçek sayı değeri noktalı
|
||
bir sayı ise (yani noktadan sonraki kısım 0 değilse) bu durumda sayının noktadan sonraki kısmı tamamen atılır, tam kısmı atanır
|
||
(truncation toward zero). Eğer sayının noktadan sonraki kısmı atıldıktan sonra tam kısmı hala hedef türün sınırları içerisine
|
||
sığmıyorsa bu durum "tanımsız davranışa (undefined behavior)" yol açmaktadır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
double a = -12.99;
|
||
int b;
|
||
|
||
b = a; /* -12 */
|
||
printf("%d\n", b);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Örneğin -2.99 biçimindeki double değer işaretsiz bir tamsayı türüne atanırsa bu da tanımsız davranış oluşturmaktadır. Yani C standartlarına
|
||
göre önce -2 değerinin elde edilip bu -2 değerinin işaretsiz tamsayı türüne dönüştürülmesi garanti edilmemiştir.
|
||
|
||
6) Tamsayı türlerinen gerçek sayı türlerine atama yapılırken eğer atama yapılan değer hedef gerçek sayı türü ile tam olarak ifade
|
||
edilebiliyorsa bilgi kaybı oluşmaz (1. Madde uygulanır), eğer tamsayı türünden değer hedef gerçek sayı türüyle tam olarak ifade edielmiyorsa
|
||
ancak basamaksal bir kayıp söz konusu değilse (yani mantissel bir kayıp söz konusu ise) bu durumda orijinale en yakın ondan küçük olan
|
||
ya da orijinal değer en yakın ondan büyük olan değer elde edilir. Fakat kayıp basamaksalsa bu durumda "tanımsız davranış (undefined behavior)"
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned int a = 1234567890;
|
||
float b;
|
||
|
||
b = a;
|
||
printf("%f\n", b); /* 1234567936.000000 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Aslında bugün kullandığımız sistemlerde long long türü bile float türüne atandığında basamaksal bir kayıp oluşmamaktadır. Fakat S
|
||
standartlarında "derleyicilere özgü daha büyük tamsayı türlerinin olabileceği" belirtilmiştir. derleyicilere özgü çok büyük tamsayı
|
||
türleri ancak basamaksal kayıplar oluşturabilir.
|
||
|
||
7)Küçük gerçek sayı türünden büyük gerçek sayı türüne yapılan atamalarda bilgi kaybı söz konusu olmaz. (Yani 1. Madde uygulanır.)
|
||
Ancak büyük gerçek sayı türünden küçük gerçek sayı türüne yapılan atamalarda bilgi kaybı söz konusu olabilir. Eğer kayıp basamaksal değilse,
|
||
yani mantis kaybı söz konusu ise kaynak değere en yakın ondan küçük ya da kaynak değere en yakın ondan büyük sayı elde edilir.
|
||
Basamaksal kayıp söz konusu olursa bu durum "tanımsız davranışa" yol açacaktır. Örneğin:
|
||
|
||
double a = 1e200;
|
||
float b;
|
||
|
||
b = a; /* undefined behavior */
|
||
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
double a = 1.234567890123;
|
||
float b;
|
||
|
||
b = a;
|
||
printf("%.10f\n", b); /* 1.2345678806 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
8) Herhangi bir türden _Bool türüne atama yapıldığında eğer atanan değer 0 ise 0 değeri atanır, sıfır dışı bir değer
|
||
ise 1 değeri atanır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = -123;
|
||
_Bool b;
|
||
|
||
b = a;
|
||
printf("%d\n", b); /* 1 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Adres türleri söz konusu olduğunda NULL adres 0 olarak, NULL olmayan adres 1 olarak atanmaktadır. Adres türleri
|
||
ileride ayrı bir bölümde ele alınacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İki operandlı bir operatör işleme sokulduğunda eğer operandlar aynı türden ise işlemin sonucu da bu türdne elde edilir. Ancak operand'lar farklı
|
||
türlerden ise önce operand'lar aynı türe dönüştürülür. Sonra işlem yapılır. İşlemin sonucu da dönüştürmenin yapıldığı ortak tür türünden olur.
|
||
C'de yalnızca değişkenlerin ve sabitlerin değil aslında her ifadenin bir türü vardır.
|
||
|
||
İşlem öncesi tür dönüştürmesi derleyici tarafından otomatik bir biçimde yapılmaktadır. Dönüştürmenin özet ancak üstünkörü kuralı
|
||
"küçük türün büyük türe dönüştürülmesi sonucun büyük tür türünden elde edilmesidir." Örneğin int ile long işleme sokulursa int önce long'a
|
||
dnüştürülür sonuç long türünden çıkar. Burada dönüştürme geçici nesne yoluyla yapılmaktadır. ÖÇrneğin a int türünden, b long türünden olsun. c'nid de
|
||
long türünden olduğunu düşünelim:
|
||
|
||
c = a + b;
|
||
|
||
Burada dönüştürme geçici nesne yoluyla yapılmaktadır. Yani önce derleyici long türünden geçici bir nesneyi kendisi yaratır. Sonra a'yı bu nesneye
|
||
nesneye atar. Sonra iki long değeri toplar. Sonra geçici nesneyi yok eder ve sonucu c'ye atar.
|
||
|
||
temp = a;
|
||
c = temp + b;
|
||
temp yok ediliyor
|
||
|
||
Uygulamada derleyiciler bu tür dönüştürmesini CPU yazmaçları içerisinde çok hızlı bir biçimde yaparlar. İşlem öncesi otomatik tür dönüştürmesinin
|
||
ayrıntalı şöyledir:
|
||
|
||
1) Tamsayı türü ile gerçek sayı türü işleme sokulduğunda dönüştürme her zaman gerçek sayı türüne doğru yapılır. Örneğin long long ile float işleme
|
||
sokulacak olsa long long türü float türüne dönüştürülür sonuç float türünden çıkar.
|
||
|
||
2) Küçük tamsayı türü ile büyük tamsayı türü işleme sokulduğunda küçük tamayı türü büyük tamsayı türüne dönüştürülür. Örneğin int ile long işleme sokulduğunda
|
||
int türü long türüne dönüştürülür sonuç long türünden çıkar. Ya da örneğin int türü ile unsigned long türü işleme sokulursa şint türü unsigned long
|
||
türüne dönüştürülür, sonuç unsigned long türünden çıkar. Ancak küçük işaretsiz tamsayı türü ile büyük işaretli tamsayı türü işleme sokulurken eğer
|
||
küçük tamsayı türü ile büyük tamsayı türü aynı uzunluktaysa dönüştürme büyük türün işaretsiz biçimine doğru yapılır. Örneğin int ile long türlerinin aynı
|
||
uzunlukta olduğunu varsayalım. Biz unsigned int ile long türünü işleme sokarsak unsigned int türü ve long türü unsigned long türüne dönüştürülür sonuç
|
||
unsigned long türünden çıkar.
|
||
|
||
3) Aynı tamsayı türünün işaretli ve işaretsiz biçimleri işleme sokulursa işaretli tamsayı türü işaretsize dönüştürülür sonuç işaretsiz türden çıkar.
|
||
Örneğin int ile unsigned int işleme sokulursa int türü unsigned int türüne dönüştürülür sonuç unsigned int türünden çıkar. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = -1;
|
||
unsigned int b = 1;
|
||
unsigned result;
|
||
|
||
result = a * b;
|
||
printf("%u\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
4) İki gerçek sayı türü kendi aralarında işleme sokulursa küçük gerçek sayı türü büyük gerçek sayı türüne dönüştürülür, sonuç büyük gerçek sayı türünden çıkar.
|
||
Örneğin float ile double işleme sokulursa sonuç double türünden çıkar.
|
||
|
||
5) C'de tamsayı işlemleri en az int duyarlılığında yapılmaktadır. int türünden küçük olan türler kendi aralarında işleme sokulursa önce her iki tür de
|
||
int türüne dönüştürülür sonuç int türünden çıkar. Bu işleme "int türüne yükselme (integer (integral) promotion)" denilmektedir. Örneğin short ile short
|
||
işleme sokulursa sonuç short türünden çıkmaz. Önce her iki operand da bağımsız olarak int türüne dönüştürülür sonuç int türünden çıkar. Benzer biçimde
|
||
örneğin short türü ile char türü işleme sokulursa önce her iki operand da bağımsız olarak int türüne dönüştürülür, sonuç int türünden çıkar. int türüne
|
||
yükseltme kuralının şöyle bir ayrıntısı vardır: Eğer ilgili sistemde short türü ile int türü aynı uzunluktaysa bu durumda operandlardan biri unsigned
|
||
short ise diğeri int ya da int türünden küçük ise dönüştürme int türüne değil unsigned int türüne yapılmaktadır. Örneğin short türü ile int türünün aynı
|
||
olduğu DOS sisteminde çalışıyor olalım. Burada biz short ile unsigned short türünü işleme soksak sonuç unsigned int türünden çıkar. Benzer biçimde
|
||
unsigned short ile int türünü işleme soksak sonuç yine unsigned int türünden çıkar (burada int türüne yükseltme kuralının değil küçük tamsayı türünün
|
||
büyük tamsayı türüne yükseltme kuralının uygulandığına dikkat ediniz.)
|
||
|
||
6) Bölme işleminde her iki operand da tamsayı türlerine ilişkinse sonuç tamsayı türüne ilişkin çıkar. Bölüm noktalı olsa bile noktadan sonraki kısım atılmaktadır.
|
||
Örneğin:
|
||
|
||
a = 10 / 4;
|
||
|
||
Burada 10 ve 4 int türdendir. Bu durumda a'ya 2 değeri atanır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10;
|
||
int b = 4;
|
||
double c;
|
||
|
||
c = a / b;
|
||
printf("%f\n", c); /* 2.000000 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada sonucun double çıkmasını istiyorsak operandlardan en az birinin double yapmamız gerekir. Örneğin:
|
||
|
||
a = 10.0 / 4;
|
||
|
||
Burada artık sonuç double türünden çıkacaktır. Aşağıdaki örnekte sayı tersten basamaklarına ayrılmaktadır.
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 12345;
|
||
int digit;
|
||
|
||
while (a) {
|
||
digit = a % 10;
|
||
printf("%d\n", digit);
|
||
a /= 10;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
7) int türüne yükseltme kuralı tek operandlı operatörlerde de yürütülmektedir. Örneğin a short türden ise -a ifadesinde işaret eksi operatörü ugulanmadan
|
||
önce int türünden küçük olan short türü önce int türüne yükseltilir. Sonuç int türünden elde edilir. Yani -a ifadesi int türden olur. Benzer biçimde !a
|
||
gibi bir işlemde de eğer a int türünden küçük olsa bile sonuç int türden elde edilir.
|
||
|
||
8) Daha önceden de bahsedildiği gibi C'de aşağıdaki operatörler, operandları hangi türden olursa olsun her zaman int türden 0 ya da 1 değeri üretmektedir:
|
||
|
||
!
|
||
<, >, <=, >=
|
||
==, !=
|
||
&&
|
||
||
|
||
|
||
Örneğin:
|
||
|
||
a == b işleminde a double türden b long türden olsa bile her zaman bu işlemin sonucu int türden elde edilir. Ancak C standartlarına göre buradaki operatörler
|
||
önce işlem öncrsi tür dönüştürmelerine sokulur. Karşılaştırma dönüştürülmüş türe göre yapılır. Ancak sonuç her zaman int türden elde edilir. Yani örneğin
|
||
a == b işleminde a double türden b long türden ise, önce long tür double türüne dönüştürülür. Karşılaştırma iki double türü ile yapılır. Ancak elde edilen ürün her zaman
|
||
int türden olur. Benzer biçimde !a gibi bir işlemde sonuç her zaman int türden elde edilir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tür dönüştürme işlemi programcı tarafından "açıkça (explicit)" da yapılabilmektedir. Bunun için tür dönüştürme
|
||
operatörü (type cast operator) kullanılmaktadır. Tür dönüştürme operatörünün kullanımı şöyledir:
|
||
|
||
(<tür>) ifade
|
||
|
||
Dönüştürülecek türün parantezler içerisine yazıldığında dikkat ediniz. Örneğin:
|
||
|
||
b = (double)a;
|
||
|
||
Burada a ifadesi açıkça double türüne dönüştürülmüş sonra atama yapılmıştır. Tür dönüştürme operatörü tek operand'lı
|
||
önek (unary prefix) bir operetördür. Diğer tek operand'lı operatörlerde olduğu gibi tür dönüştürme operatörü de öncelik
|
||
tablosunun ikinci düzeyinde sağdan-sola grupta bulunmaktadır:
|
||
|
||
() Soldan-Sağa
|
||
+ - ++ -- ! (tür) Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
Örneğin:
|
||
|
||
result = (double)a / b;
|
||
|
||
Bu ifadede de üç operatör vardır. En yüksek öncelikli operatör tür dönüştürme operatörüdür. O halde işlemler şu
|
||
sırada yapılacaktır:
|
||
|
||
İ1: (double)a
|
||
İ2: İ1 / b
|
||
İ3: result = İ2
|
||
|
||
Burada görüldüğü gibi a ifadesi önce double türüne dönüştürülüp sonra b'ye bölünmüştür. Eğer a / b ifadesinin sonucunu
|
||
double türüne dönüştüreceksiniz ifadeyi parantez içerşsşne almalısınız. Örneğin:,
|
||
|
||
result = (double)(a / b);
|
||
|
||
Burada önce a / b işlemi yapılacak bunun sonucu double türüne dönüştürülecektir. Örneğin:
|
||
|
||
result = (double)(long)a;
|
||
|
||
Tür dönüştürme operatörünün sağdan sola öncelikli grupta bulunduğuna dikkat ediniz. Dolayısıyla burada işlemler
|
||
şu sırada yapılacaktır:
|
||
|
||
İ1: (long)a
|
||
İ2: (double)İ1
|
||
İ3: result = İ2
|
||
|
||
Yani a ifadesi önce long türüne sonra double türüne dönüştürülmektedir.
|
||
|
||
Pekiyi açıkça (explicit) dönüştürmeye ne gerek vardır? Aslında bazı durumlarda mecburen açıkça dönüştürme kullanılmak
|
||
zorundadır. Özellikle göstericiler söz konusu olduğunda bazı dönüştürmelerin açıkça yapılması zorunludur. Aşağıdaki
|
||
ifadeye dikkat ediniz:
|
||
|
||
int a = 10, b = 4;
|
||
double result;
|
||
|
||
result = a / b;
|
||
|
||
Burada a / b işleminin sonucu 2 çıkacak ve noktadan sonraki kısım kırpılacaktır. Pekiyi biz sonucun 2.5 çıkmasını
|
||
nasıl sağlarız? İşte bu tür durumlarda mecburen açıkça dönüştürme uygulamak gerekir. Örneğin:
|
||
|
||
result = (double)a / b;
|
||
|
||
Elimizde double türden bir d değişkeni olsun. Biz de bunun tam kısmını işleme sokmak isteyelim. Böylesi bir durumda da
|
||
mecburen açıkça tür dönüştürmesi uygulamak gerekir. Örneğin:
|
||
|
||
result = (int)d * 2;
|
||
|
||
Açıkça dönüştürmeler de yine "geçici nesne yoluyla" yapılmaktadır. Örneğin:
|
||
|
||
result = (double)a / b;
|
||
|
||
Burada a değişkeni double türüne kalıcı olarak dönüştürlmemektedir. Bu işlem için dönüştürme uygulanmaktadır. Yani
|
||
bu işlemin eşdeğeri şöyledir:
|
||
|
||
double result = a;
|
||
result = temp / b;
|
||
<temp yok ediliyor>
|
||
|
||
Tabii derleyiciler bu biçimdeki geçici nesneleri genellikle CPU yazmaçlarında oluşturmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10, b = 4;
|
||
double result;
|
||
|
||
result = a / b;
|
||
printf("%f\n", result); /* 2.000000 */
|
||
|
||
result = (double)a / b;
|
||
printf("%f\n", result); /* 2.500000 */
|
||
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
25. Ders 01/09/2022 - Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir işlemde operandlar aynı türdense ya da farklı türlendense ancak işlem öncesi otomatik tür dönüştürmesi uygulanmışsa elde edilen değer söz konusu
|
||
ortak türün sınırları içerisine girmiyorsa bu duruma "taşma (overflow)" denilmektedir. C'de işaretli tamsayı türleri üzerinde taşma oluşursa bu durum
|
||
"tanımsız davranışa"" yol açmaktadır. Ancak işaretsiz tamsayı türleri üzerinde taşma olursa her zaman taşan yüksek anlamlı byte'lar atılmaktadır. Örneğin:
|
||
|
||
int a, b, c;
|
||
...
|
||
c = a + b;
|
||
|
||
Burada a ve b int türden olsun. a + b işleminin sonucu da int türden olacaktır. Ancak a + b işleminin sonucu int türün sınırları içerisinde kalmıyorsa
|
||
bu durum tanımsız davranışa yol açar. Örneğin:
|
||
|
||
unsigned int a, b, c;
|
||
...
|
||
c = a + b;
|
||
|
||
Burada a + b işleminin sonucu unsigned int türden olacaktır. Ancak sonuç unsigned int türünün sınırları dışında ise bu durum tanımsız davranış değildir.
|
||
Yüksek anlamlı byte'lar her zaman atılır. Tabii taşma bazı tek operandlı operatörlerde de ortaya çıkabilir. Örneğin:
|
||
|
||
int a, b;
|
||
|
||
a = -2147483648;
|
||
b = -a; /* tanımsız davranış */
|
||
|
||
|
||
Burada -a işleminin sonucu da int türdendir. Ancak bu işlemin sonucunda elde edilen değer int türünün sınırları içerisinde değildir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İşaretsiz bir tamsayı türüne işaret eksi operatörü uygulanırsa tür int türünden küçükse önce int türüne yükseltme kuralı uygulanır. Sonra eğer sayı işaretsiz
|
||
ise önce onun negatifi elde edilir. Elde edilen negatif değer işaretsiz türe dönüştürülür. Örneğin:
|
||
|
||
usnigned a = 1, b;
|
||
|
||
b = -a;
|
||
|
||
Burada -a işleminin sonucu unsigned int türden olacaktır. -a işleminde önce -1 elde edilir. Ancak sonuç unsigned int türünden olacağı için unsigned int türüne
|
||
dönüştürülür. Böylece işlemden en büyük unsigned int değer elde edilir. Başka bir deyişle burada 1 değerine ikiye tümleme işlemi uygulanır ve elde edilen değer
|
||
unsigned int biçiminde ele alınır. Ya da örneğin -a gibi bir değer -1 * a olarak düşünülebilir. Bu durumda a unsigned int türden ise -1 de unsigned
|
||
int türüne dönüştürülür. Buradan en büyük pozitif değer elde edilir. Bu değer a ile çarpılıp yüksek anlamlı byte'lar atılırsa aslında önceki ifade
|
||
edilen durumla aynı durum oluşmuş olur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned a = 1, b;
|
||
|
||
b = -a;
|
||
printf("%u\n", b); /* 4294967295 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de geleneksel olarak bir fonksiyonun parametresi int türünden küçük bir türdense parametre o türden değil int türünden ifade edilir. Aynı durum geri
|
||
dönüş değeri için de uygulanmaktadır. Bu bir zorunluluk değildir. Ancak bir gelenektir. Örneğin foo fonksiyonun parametresinin short bir değer aldığını varsayalım.
|
||
Programcı parametreyi short yapmaz int yapar. Benzer biçimde putchar fonksiyonu bir karakteri ekrana (stdout dosyasına) yazdırmaktadır. Parametre char
|
||
yerine int yapılmıştır. Aynı durum geri dönüş değerleri için de benzer biçimde uygulanmaktadır. Bu nedenle C programlarında genel olarak fonksiyonların
|
||
parametrelerinde ve geri dönüş değerlerinde char gibi, short gibi türler geleneksel olarak kullanılmaz. Onların yerine int türü kullanılır.
|
||
|
||
Pekiyi neden fonksiyonların parametrelerinde ve geri dönüş değerlerinde int türünden küçük türler programcılar tarafından tercih edilmemektedir?
|
||
Yani bu geleneğin anlamı nedir? İşte bunun iki nedeni vardır:
|
||
|
||
1) C'de zaten tamsayı işlemleri her zaman "int türüne yükseltme kuralı" gereği en az int duyarlılığında yapılmaktadır. Bu durumda bir değişkenin int türünden
|
||
küçük olmasının çoğu kez bir anlamı yoktur. Aynı zamanda parametre aktarımı ve geri dönüş değerinin oluşturulması da zaten işlemciler tarafından en az
|
||
int duyarlılıkta yapılmaktadır. Yani parametrelerin ve geri dönüş değerlerinin int türden olması daha doğal bir gösterim sunmaktadır.
|
||
|
||
2) Eskiden fonksiyon prototiplerinin olmadığı zamanlarda zaten "default argument conversion" kuralı gereğince int türünden küçük olan türler int türüne
|
||
yükseltilerek fonksiyona aktarılıyordu. Dolayısıyla bu gelenek zaten eski zamanlardan beri bu gerekçeyle uygulanıyordu.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C standartlarına göre bir kodun etkisi aynı kalacak biçimde derleyiciler kodu daha hızlı çalışacak ya da daha az yer kaplayacak biçimde optimize edebilir.
|
||
Burada önemli olan programcının varsaydığı ya da oluşturmak istediği her şeyin optimize edilmil kodda oluşturulmuş olmasıdır. Örneğin:
|
||
|
||
x = a + b + c + 1;
|
||
y = a + b + c + 2;
|
||
z = a + b + c + 3;
|
||
|
||
Biz bu kodu böyle yazmış olsak da örneğin derleyici daha az işlem yapılacak şekilde kodu şöyle düzenleyebilir:
|
||
|
||
temp = a + b + c;
|
||
x = temp + 1;
|
||
y = temp + 2;
|
||
z = temp + 3;
|
||
|
||
Biz derleyicinin kodu böyle düzenlediğini bilmek zorunda değiliz. Ne de olsa bizim niyetlediğimiz her şeyi derleyicinin optimize ettiği kod da yapmaktadır. Örneğin:
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
printf("%d\n", i);
|
||
x = 100;
|
||
}
|
||
printf("%d\n", x);
|
||
|
||
Burada x'in döngü içerisinde durmasının programı yavaşlatmak dışında hiçbir anlamı yoktur. Derleyici kodu şöyle optimize edebilir:
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
printf("%d\n", i);
|
||
}
|
||
x = 100;
|
||
printf("%d\n", x);
|
||
|
||
Örneğin:
|
||
|
||
for (int i = 0; i < 1000000; ++i)
|
||
ifade;
|
||
|
||
Derleyici bu kodu isterse aşağıdaki gibi düzenleyebilir ve biz bunu programın çalışması sırasında anlayamayız:
|
||
|
||
for (int i = 0; < 1000000; i += 5) {
|
||
ifade;
|
||
ifade;
|
||
ifade;
|
||
ifade;
|
||
}
|
||
|
||
Örneğin:
|
||
|
||
int foo(void)
|
||
{
|
||
return 100;
|
||
}
|
||
...
|
||
x = foo();
|
||
|
||
Burada derleyici aslında bu fonksiyonu hiç çağırmadan aşağıdaki gibi de kod üretebilir:
|
||
|
||
x = 100;
|
||
|
||
Ancak fonksiyon şöyle olsaydı:
|
||
|
||
int foo(void)
|
||
{
|
||
printf("foo\n");
|
||
|
||
return 100;
|
||
}
|
||
...
|
||
x = foo();
|
||
|
||
Artık bu optimizasyonu yukarıdaki gibi yapamayacaktı. Tabii derleyici kütüpahedeki ya da başka modüldeki fonksiyonlar
|
||
için bu biçimde bir optimizasyon yapamamaktadır.
|
||
|
||
C derleyicilerinde optimizasyonlar genellikle derleyicilerin komut satırı seçenekleriyle azaltılıp yükseltilebilmektedir.
|
||
Döngü açımı (loop unrolling), otomatik inline açımı gibi optimizasyonlar "argresif" optimizasyonlardır. Pek çok
|
||
derleyicide bu optimizasyonların yapılabilmesi için özel komut satırı seçeneklerinin girilmesi gerekmektedir. Kod
|
||
optimizasyonlarının derleme zamanı üzerinde ciddi yavaşlatıcı etkileri olabilmektedir. Kod optimizasyonu programın
|
||
kaynak kod düzeyinde debug edilmesini engelleyebilmektedir. Bu nedenle kaynak kod düzeyinde debug yapılabilmesi için
|
||
optimizasyonların büyük ölçüde kapatılması gerekmektedir. Örneğin Microsoft C derleyicilerinde projenin "debug"
|
||
versiyonunda optimizasyonlar kapatılırken, ""relese" versiyonunda optimizasyonlar açılmaktadır. gcc ve clang derleyicilerinde
|
||
optimizasyonlar kategorik olarak -O0, -O1, -O2 ve -O3 gibi seçeneklerle ayarlanabilmektedir. Microsoft derleyicilerinde
|
||
de benzer biçimde /Od, /O1, /O2 ve /Ox seçenekleri bulunmaktadır.
|
||
|
||
Optimizasyon default olarak hıza dayalı biçimde yapılmaktadır. Optimize edilmiş kod optimize edilmemiş koddan daha
|
||
büyük hale gelebilmektedir. (Örneğin döngü açımında aslında kod büyümektedir.) Fakat bazen derleyicilerin hız yerine
|
||
kodu küçültecek optimizasyon yapmasını da isteyebiliriz. Özellikle küçük kapasiteli bilgisayar sistemlerinde ve
|
||
mikrodenetleyicilerde bu tür istekler söz konusu olabilmektedir. gcc ve clang derleyicilerinde -Os seöeneği,
|
||
Microsoft derleyicilerinde /Ox seçeneği size optimizasyonları için kullanılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de ismi isxxx başlayan ve ismine "karakter test fonksiyonları" denilen bir grup standart C fonksiyonu vardır. Bu fonksiyonların parametreleri int türden
|
||
ve geri dönüş değerleri de int türdendir. Her ne kadar bu fonksiyonların parametreleri int türdense de aslında bu fonksiyonlar char bir değeri parametre
|
||
olarak alırlar. Bu fonksiyonlar parametreleriyle aldıkları karakteri test ederler. Eğer test olumlu ise sıfır dışı herhangi bir değere olumsuz ise sıfır değerine
|
||
geri dönerler. Bunların listesi şöyledir:
|
||
|
||
isupper Büyük harf bir karakter mi?
|
||
islower Küçük harf bir karakter mi?
|
||
isalpha Alfabetik karakter mi?
|
||
isalnum Alfabetik ya da nümerik bir karakter mi?
|
||
isdigit Sayısal bir karakter mi?
|
||
isxdigit HEx digit bir karakter mi?
|
||
isspace Boşluk karakterlerinden biri mi?
|
||
ispunct Noktalama karakterlerinden biri mi?
|
||
isascii İlk 128 karakterden biri mi?
|
||
iscntrl Kontrol karakterlerinden biri mi? (ASCII tablosunun ilk 32 karakteri kontrol karakterleridir)
|
||
|
||
Bu fonksiyonlar kullanılırken <ctype.h> dosyası include edilmelidir. Karakter test fonksiyonları yalnızca ACII tablosundaki karakterler için çalışmaktadır.
|
||
Biz bu fonksiyonlarla öğrneğin Türkçe karakterleri test edemeyiz.
|
||
|
||
Karakter test fonksiyonlarının parametreleri unsigned char türüyle temsil edilebilmelidir. Yani örneğin biz bu fonksiyonlara [0, 255] aralığının dışında
|
||
herhangi bir değer girersek bu fonksiyonlar tanımsız davranışa yol açarlar. Bu fonksiyonlar UTF-8 gibi multibyte karakterler için kullanılamazlar.
|
||
Ancak bir byte'lık encodinglerde lokal spsifik davranış gösterebilirler.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <ctype.h>
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
printf("Bir karakter giriniz:");
|
||
ch = getchar();
|
||
|
||
if (isupper(ch))
|
||
printf("upper case\n");
|
||
else if (islower(ch))
|
||
printf("lower case\n");
|
||
else if (isdigit(ch))
|
||
printf("digit\n");
|
||
else if (isspace(ch))
|
||
printf("white space char\n");
|
||
else
|
||
printf("another character\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İki önemli karakter fonksiyonu da toupper ve tolower fonksiyonlarıdır. toupper fonksiyonu küçük bir karakteri büyük harfe, tolower fonksiyonu da
|
||
büyük harf bir karakteri küçük harfe dönüştürür. toupper eğer parametresi küçük bir karakter değilse aynı karakterle geri dönmektedir.
|
||
Benzer biçimde tolower fonksiyonu da eğer parametresi büyük bir harf bir karakter değilse aynı değerle geri döner. Fonksiyonların parametrik yapıları şöyledir:
|
||
|
||
int toupper(int ch);
|
||
int tolower(int ch);
|
||
|
||
Her ne kadar bu fonksiyonların parametreleri ve geri dönüş değerleri int türdense de aslında char türden bir bilgiyi kabul etmektedir.
|
||
Bu fonksiyonların geri döndürdüğü int değerlerin yüksek anlamlı byte'ları her zaman 0'dır. Düşük anlamlı byte'larında dönüştürülmüş karakterin numarası vardır.
|
||
Dolayısıyla biz bu fonksiyonların geri dönüş değerlerini char türünden bir nesneye atayabiliriz.
|
||
|
||
Bu fonksiyonların parametreleri de unsigned char sınırları içerisinde [0, 255] arasında değerler girilebilir.
|
||
|
||
Bu fonksiyonları kullanırken de <ctype.h> dosyasının include edilmesi gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <ctype.h>
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
printf("Bir karakter giriniz:");
|
||
ch = getchar();
|
||
|
||
ch = toupper(ch);
|
||
putchar(ch);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte "case insensitive" karakter karşılaştırması örneği verilmiştir. Biz bir karakter toupper ya da tolower fonksiyonuna sokup bunun sonucunu
|
||
karşılaştırırsak "case insensitive" karşılaştırma yapmış oluruz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <ctype.h>
|
||
|
||
int main(void)
|
||
{
|
||
int ch;
|
||
|
||
printf("(e)vet/(h)ayir?\n");
|
||
ch = getchar();
|
||
ch = tolower(ch);
|
||
|
||
if (ch == 'e')
|
||
printf("evet\n");
|
||
else if (ch == 'h')
|
||
printf("hayir\n");
|
||
else
|
||
printf("e ya da h girilmedi!\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
toupper fonksiyonunu basit bir biçimde aşağıdaki yazabiliriz. Tabii bu yazımda küçük harf ve büyük harflerin karakter tablosunda peşi sıra gittiği
|
||
varsayılmaktadır. ASCII tablosunda bunlar gerçekten peşi sıra girmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int mytoupper(int ch)
|
||
{
|
||
if (ch >= 'a' && ch <= 'z')
|
||
return ch - 'a' + 'A';
|
||
|
||
return ch;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char ch;
|
||
|
||
ch = getchar();
|
||
ch = mytoupper(ch);
|
||
|
||
putchar(ch);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
26. Ders 06/09/2022 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de derleme işlemi kaynak dosyada yukarıdan aşağıya doğru yapılmaktadır. Bir fonksiyonun çağrıldığını gören derleyici fonksiyonun çağrılma noktasına kadar
|
||
fonksiyonun geri dönüş değerinin türü hakkında bir bilgiyi edinmesi gerekir. Çünkü çağrılma noktasında doğru kodu üretebilmek için derleyicinin
|
||
çağrılan fonksiyonun en azından geri dönüş değerinin türünü biliyor olması gerekmektedir. C90'da fonksiyonun çağrılma noktasına kadar fonksiyonun
|
||
geri dönüş değerinin türü hakkında derleyici bir bilgi edinememişse fonksiyonun int türden geri dönüş değerine sahip olduğunu ancak herhangi bir
|
||
parametrik yapıya sahip olabileceğini varsayarak kod üretmektedir. Bu durumda eğer derleyici fonksiyonu çağrılma noktasının aşağısında
|
||
int geri dönüş değerinin dışında bir geri dönüş değeri türüyle tanımlandığını görürse bu durum geçersizdir ve error oluşturmaktadır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
foo();
|
||
|
||
return 0;
|
||
}
|
||
|
||
int foo(void)
|
||
{
|
||
return 100;
|
||
}
|
||
|
||
Bu kod C90'a göre geçerlidir. Çünkü yukarıdan aşağıya derleyici fonksiyonun çağrılma noktasına kadar fonksiyonun geri dönüş değerinin türünü anlayamadıysa
|
||
fonksiyonun int geri dönüş değerine sahip olduğu varsayımıyla kod üretir. Daha sonra fonksiyonun gerçekten de int geri dönüş değerine sahip olarak
|
||
tanımlandığını görürse kendi varsaydığı durumla gerçekleşen durum aynı olduğu için bu durumda herhangi bir sorun ortaya çıkmaz. Ancak eğer fonksiyonun
|
||
daha aşağıda int geri dönüş değeri dışında herhangi bir geri dönüş değerine sahip olarak tanmlandığını görürse bu durumda yanlış ürettiği için derleme işlemi
|
||
başarısız olacaktır. Derleyici bu tür durumlarda geri dönüp ürettiği kodu düzeltmemektedir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
foo();
|
||
|
||
return 0;
|
||
}
|
||
|
||
double foo(void) /* error! */
|
||
{
|
||
return 3.14
|
||
}
|
||
|
||
C90'da çağrılma noktasına kadar fonksiyonun geri dönüş değerinin türüne ilişkin derleyici bilgi edinememişse onun int geri dönüş değerine sahip
|
||
ancak herhangi bir parametrik yapıya ilişkin olabileceği varsayımıyla kod üretmektedir. Yani C90'da da fonksiyon daha aşağıda int geri dönüş değerine sahip
|
||
ancak farklı bir parametrik yapıyla tanımlanmışsa bu durum yine geçerlidir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
foo();
|
||
|
||
return 0;
|
||
}
|
||
|
||
int foo(int a, int b) /* C90'da geçerli */
|
||
{
|
||
return a + b;
|
||
}
|
||
|
||
Ancak C++'ta ve C99 ve ötesinde yukarıda açıklanan kural değiştirilmiştir. C99 ve ötesinde artık bir fonksiyon çağrılmışsa çağrılma noktasına kadar
|
||
mutlaka derleyicinin fonksiyonun en azından geri dönüş değerinin türü hakkında bir bilgiyi edinmiş olması gerekmektedir. Dolayısıyla aşağıdaki
|
||
örnek C90'da geçerli olduğu halde C99 ve ötesinde (ve C++'ta) geçersizdir:
|
||
|
||
int main(void)
|
||
{
|
||
foo(); /* C99 ve ötesinde geçersiz! Ancak C90'da geçerli */
|
||
|
||
return 0;
|
||
}
|
||
|
||
int foo(void)
|
||
{
|
||
return 100
|
||
}
|
||
|
||
İşte çağrılma noktasına kadar çağrılan fonksiyonun en azından geri dönüş değerinin türü hakkında derleyicinin bir bilgi edinebilmesinin iki
|
||
yoldu vardır:
|
||
|
||
1) Çağrılan fonksiyonu çağıran fonksiyonun daha yukarısında tanımlamak
|
||
2) Çağrılan fonksiyonun "prototip" denilen bir bildirimini çağrılma noktasından yukarıda bir yere yerleştirmek. Örneğin:
|
||
|
||
double foo(void)
|
||
{
|
||
return 3.14;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
double result;
|
||
|
||
result = foo();
|
||
printf("%f\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada çağrılan fonksiyon çağıran fonksiyonun yukarısında tanımlandığı için herhangi bir sorun yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon protoipleri bir "tanımalama (definition)" işlemi değildir. Yani prototip bildirimini gördüğünde derleyici bellekte bir yer ayırmaz.
|
||
Yalnızca o fonksiyonun geri dönüş değeri ve parametrik yapısı hakkında bilgi edinir. Prototip bildiriminin genel biçimi şöyledir:
|
||
|
||
<fonksiyonun geri dönüş değerinin türü> <fonksiyonun_ismi>([parametre bildirimi]);
|
||
|
||
Örneğin:
|
||
|
||
double foo(void); /* Fonksiyon prototip bildirimi */
|
||
|
||
int main(void)
|
||
{
|
||
double result;
|
||
|
||
result = foo();
|
||
printf("%f\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
double foo(void)
|
||
{
|
||
return 3.14;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Prototip bildirimi oluşturmanın en pratik yolu fonksiyon tanımlamasının ilk satırını alıp sonuna ';' atomunu yerleştirmektedir. Örneğin:
|
||
|
||
double div(double a, double b)
|
||
{
|
||
return a / b;
|
||
}
|
||
|
||
Burada bu fonksiyonun ilk satırı alınıp sonuna ';' konulursa zaten prototip haline gelir.
|
||
|
||
double div(double a, double b);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
double div(double a, double b);
|
||
|
||
int main(void)
|
||
{
|
||
double result;
|
||
|
||
result = div(3, 2);
|
||
printf("%f\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
double div(double a, double b)
|
||
{
|
||
return a / b;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Prototipteki parametre değişkeni isimleriyle tanımlamadaki parametre değişkenlerinin isimlerinin uyuşması bir zorunluluk değildir. Örneğin:
|
||
|
||
double div(double x, double y);
|
||
|
||
/* .... */
|
||
|
||
double div(double a, double b) /* geçerli */
|
||
{
|
||
return a / b;
|
||
}
|
||
|
||
Prototipte yalnızca parametre değişkenlerinin türleri belirtilebilir, isimleri belirtilmeyebilir. Örneğin:
|
||
|
||
double div(double, double); /* geçerli */
|
||
|
||
Prototipte belirtilen geri dönüş değeri türü ve parametre türlerinin eğer tanımlama yapılmışsa tanımlamadkiyle uyuşması zorunludur. Aksi takdirde
|
||
kod geçersizdir. Örneğin:
|
||
|
||
double div(double a, double b);
|
||
|
||
/* .... */
|
||
|
||
double div(float a, float b) /* geçersiz! */
|
||
{
|
||
return a / b;
|
||
}
|
||
|
||
Bir fonksiyon ikinci kez tanımlanamaz ancak bir fonksiyonun prototipi birden fazla kez bildirilirse bir sorun oluşturmaz. Tabii bu durumda tüm prototip
|
||
bildirimlerindeki geri dönüş değeri türü ve parametre türlerinin aynı lması gerekir. Örneğin:
|
||
|
||
|
||
double div(double x, double y);
|
||
double div(double x, double y); /* geçerli, aynı olmak koşuluyla bir prototip bildirimi birden fazla kez yazılabilir */
|
||
|
||
/* .... */
|
||
|
||
double div(double a, double b) /* geçerli */
|
||
{
|
||
return a / b;
|
||
}
|
||
|
||
C90'da prototip bildiriminde her ne kadar anlamsız olsa da geri dönüş değerinin türü yazılmayabiliyordu. Bu durumda geri dönüş değerinin türü için
|
||
int yazılmış olduğu varsayılıyordu. Ancak bu kural C99 ile birlikte kaldırılmıştır. Artık prototipte geri dönüş değerinin türü yazılmak zorundadır. Örneğin:
|
||
|
||
foo(void); /* Bu prototip C90'da geçerli ancak C99 ve sonrasında geçersiz! */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'nin tüm versiyonlarında prototipte parametre parantezinin içinin boş bırakılmasıyla void yazılamsı farklı anlamlara gelmektedir. Parametre
|
||
parantezinin içi boş bırakılırsa bu durum "derleyicinin çağrılma sırasında parametreleri sayıca kontrol etmeyeceği" anlamına gelmektedir. Oysa parametre
|
||
parantezinin içine void yazılması fonksiyonun parametreye sahip olmadağı anlamına gelir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(); /* Bu prototip parametrelerin herhangi bir biçimde olabileceği anlamına gelmektedir */
|
||
|
||
int main(void)
|
||
{
|
||
|
||
foo(10, 20); /* geçerli, parametreler sayıca kontrol edilmiyor */
|
||
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo(double a, double b)
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
Ancak örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(void); /* bu prototip fonksiyonun parametreye sahip olmadığı anlamına gelmektedir */
|
||
|
||
int main(void)
|
||
{
|
||
|
||
foo(10, 20); /* geçersiz! Fonksiyon parametreye sahip değil */
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
Tabii eğer parametre parantezinin içinin boş bırakıldığı bir prototipten sonra artık derleyici parametre parantezinin içinin boş bırakılmaıdğı bir prototoip
|
||
ile ya da fonksiyonun tanımlamasıyla karşılaşırsa bu durumda parametre kontrolü artık yapılır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(); /* bu prototip parametre kontrolünün yapılmayacağı anlamına geliyor */
|
||
|
||
void foo(int a, int b) /* artın bu tanımlamayla fonksiyonun parametreleri çağrım sırasında derleyici tarafından kontrol edilecektir */
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
|
||
foo(10, 20, 30); /* geçersiz! */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Aşağıdaki iki prototip birlikte bulunabilir:
|
||
|
||
void foo(); /* bu prototip parametre kontrolünün yapılmayacağı anlamına geliyor */
|
||
void foo(int a, int b); /* artık derleyici parametre kontrolü yapacaktır */
|
||
|
||
|
||
Tabii parametre parantezinin içi boş bırakıldığında fonksiyon yine uygun olmayan sayıda argümanla çağrılırsa bu durum "tanımsız davranışa" yol açar.
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(); /* bu prototip parametre kontrolünün yapılmayacağı anlamına geliyor */
|
||
|
||
int main(void)
|
||
{
|
||
|
||
foo(10, 20, 30); /* geçerli! derleme başarıyla sonuçlanır, ancak tanımsız davranış oluşur */
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo(int a, int b)
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
Pekiyi prototipte parametre parantezinin içinin boş bırakılabilmesi gibi bir kuralın anlamı nedir? İşte eskiden (80'lerin ilk yarısına kadar) prototip
|
||
diye bir kavram C'de yoktu. Yalnızvca "fonksiyon bildirimi (function declarations)" denilen bir kavram vardı. Fonksiyon bildiriminde de parametre
|
||
parantezinin içi boş bırakılyordu. Derleyici de o zamanlar fonksiyon bildirimini gördüğünde parametre kontrolü yapmıyordu. Daha sonra C'ye fonksiyon
|
||
prototipleri eklenince eskiye doğru uyumu korumak için prototiplerde parametre parantezinin içinin boş bırakılabilmesi geçerli kabul edildi.
|
||
Yani prototiplerde parametre parantezinin içinin boş bırakılabilmesi tamamen eskiye doğru uyumu korumak için düşünülmüştür. Tabii programcı prototip
|
||
yazarken parametre türlerini belirtmelidir.
|
||
|
||
C++ C'nin geçmişe doğru uyumu koruma gibi bir çabasına ortak olmamıştır. Dolayısıyla örneğin C++'ta prototiplerde parametre parantezinin içinin
|
||
boş bırakılmasıyla void yazılması arasında hiçbir farklılık yoktur. Her iki durum da "fonksiyonun parametreye sahip olmadığı" anlamına gelmektedir. Örneğin:
|
||
|
||
void foo();
|
||
void foo(void); // C++'ta ikisi arasında hiçbir farklılık yok
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında C'de farklılık olduğunu belirtmiştik. Ancak fonksiyon tanımlaması söz konusu
|
||
olduğunda parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında hiçbir farkılık yoktur. Her iki durum da "fonksiyonun" parametreye
|
||
sahip olmadığı anlamına gelir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo() /* tanımlamada parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında farklılık yok */
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
|
||
foo(10); /* geçersiz! foo'nun parametresi yok! */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Biz kursumuzda parametreye sahip olmayan fonksiyonların tanımlamasında açıkça parametre parantezinin içerisine void
|
||
anahtar sözcüğünü yerleştireceğiz. C'deki gelenek daha çok bu biçimdedir. Ancak C++'ta programcılar genellikle
|
||
prototipte ya da tanımlamada parametre parantezinin içerisinde void anahtar sözcüğünü yerleştirmezler. Biz de C++
|
||
kurslarında parametre parantezlerinin içerisine void anahtar sözcüğünü yerleştirmiyoruz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonun prototipinin yazılmış olması onu çağırmayı zorunlu hale getirmez. Yazni biz onlarca fonksiyonun prototipini yazıp onları hiç çağırmayabiliriz.
|
||
Bu durum tamamen geçerlidir. Fonksityon prototipleri bir tanımlama olmadığı bellekte yer kaplamazlar. Yalnızca derleyici tarafından kod derlenirken bunlardan
|
||
faydalanılmaktadır. Tabii çok sayıda fonksşyon için prototip yazıldığında derleyici bunların hepsini gözden geçireceği için mikro mertebede de olsa derleme süresi uzayabilir.
|
||
Ancak program çalışırken bu protiplerin bellk üzerinde ya da performans üzerinde hiçbir etkisi yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon prototipleri global düzeyde ya da yerel düzeyde bildirilebilir. Eğer prototip global alana yerleştirilirse bu durumda yerleştirildiği
|
||
yerden dosyanın sonunaa kadar her yerde etikili olur. Eğer prototip bir yerel bloğa yerleştirilmişse yerleştirildiği yerde o bloğun sonuna
|
||
kadarki bölgede etkili olur. Hemen her zaman programcılar prototipleri global düzeysde programın tepesinde ya da bir başlık dosyasının içerisinde
|
||
bildirirler. Örneğin
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(void);
|
||
|
||
int main(void)
|
||
{
|
||
void bar(void);
|
||
|
||
foo(); /* geçerli, prototip bildirimi görülmüş durumda */
|
||
bar(); /* geçerli, prototip bildirimi görülmüş durumda */
|
||
|
||
return 0;
|
||
}
|
||
|
||
void tar()
|
||
{
|
||
foo(); /* geçerli prototip bildirimi görülmüş durumda */
|
||
bar(); /* geçersiz! prototip bildirimi derleyici tarafından görülmüyor */
|
||
}
|
||
|
||
void foo(void) /* tanımlamada parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında farklılık yok */
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
printf("bar\n");
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Derleyici standart C fonksiyonlarının farkında değildir. Yani derleyici bizim foo fonksiyonumuz için ne yapıyorsa printf, sqrt gibi standart C
|
||
fonksiyonları için de aynı şeyi yapar. Başka bir deyişle örneğin derleyici printf fonksiyonunu gördüğünde ona hiçbir özel muamele yapmamaktadır.
|
||
O halde standart C fonksiyonları için de prototip gerekmektedir. Eğer standart C fonksiyonları için prototip yazılmzsa C90'da onların geri dönüş değerleri
|
||
int kabul edilmektedir. C99 ve ötesinde zaten bu durum geçerli değildir. Örneğin aşağıdaki kodda sqrt fonksiyonun prototipi olmadığı için
|
||
C90'da onun geri dönüş değeri int kabul edilecek ve "tanımsız davranış" oluşacaktır. C99 ve ötesinde aşağıdaki kod geçerli değildir:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
double result;
|
||
|
||
result = sqrt(10); /* C90'da tanımsız davranış! C99 ve ötesinde geçersiz' */
|
||
printf("%f\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Standart C fonksiyonları için prototipleri biz kendimiz yazabiliriz. Ama bu tavsiye edilen bir durum değildir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
double sqrt(double);
|
||
|
||
int main(void)
|
||
{
|
||
double result;
|
||
|
||
result = sqrt(10); /* geçerli, prototip yazılmış ve doğru */
|
||
printf("%f\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
İşte C'de standart C fonksiyonarının prototipleri gruplara ayrılarak çeşitli başlık dosyalarının içerisine yazılmıştır. Örneğin tüm matematiksel
|
||
fonksiyonların prototipleri <math.h> dosyasının içerisine, tüm dosya, ekran ve klavye fonksiyonlarının prototipleri <stdio.h> dosyasının içerisine,
|
||
tüm karakter test fonksiyonlarının prototipleri <ctype.h> dosyasının içerisine yerleştirilmiştir. Böylece programcı standart C fonksiyonlarının
|
||
protiplerini elle yazmak yerine onların zaten yazılı olduğu başlık dosyasını include eder. Burada önemli bir nokta bu başlık dosyalarında bu fonksiyonların
|
||
tanımlamalarının yani kodlarının olmadığı yalnızca prototiplerinin olduğudur.
|
||
|
||
Tabii bir bir başlık dosyasını include ettiğimizde aslında bir grup standart C fonksiyonunun prototipini kaynak koda eklemiş oluruz. Ancak yukarıda da
|
||
belirttiğimiz gibi bir fonksiyonun prototipinin yazılmış olması onu çağırmayı zorunlu hale getirmemektedir.
|
||
|
||
O halde programcı tipik olarak hangi standart C fonksiyonunu çağıracaksa onun prototipinin hangi başlık dosyasında olduğunu öğrenmeli ve o dosyayı
|
||
include etmelidir.
|
||
|
||
Tabii aslında başlık dosyalarının içerisinde prototiplerden başka birtakım bildirimler de vardır. Konular ilerledikçe biz bu başlık dosyalarının ieçrisinde
|
||
başka nelerin olduğunu da göreceğiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
27. Ders 08/09/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Başlık dosyalarında standart C fonksiyonlarının prototiplerinin bulunduğu belirtmiştirk. Pekiyi bunların kendileri nerededir? İşte standart C fonksiyonları
|
||
derlenmiş bir biçimde "kütüphane (library)" denilen özel dosyaların içerisinde bulunmaktadır. Kütüphaneler linker tarafından link işlemi sırasında
|
||
bakılmaktadır. Linker standart C fonksiyonlarının bulunduğu kütüphane dosyalarına otomatik olarak bakar böylece onları oradan alır. Kütüphaneler
|
||
içerisinde fonksiyonlar derlenmiş bir biçimde bulunmaktadır. Dolayısıyla onların yeniden derlenmesine gerek olmaz.
|
||
|
||
Tabii kütüphaneler programcılar tarafından da oluşturulabilmektedir. Programcılar da kütüphane dosyaları oluşturup link aşamasında linker'a oluşturdukları
|
||
kütüphaneye de bakmasını söyleyebilmektedirler. Yukarıda da belirttiğimiz gibi linker zaten (genellikle) otomatik olarak standart C fonksiyonlarının
|
||
bulunduğu kütüphanelere de bakmaktadır.
|
||
|
||
Kütüphane dosyaları "statik kütüpahenler" ve "dinamik kütüphaneler" olmak üzere ikiye ayrılmaktadır. Statik kütüphane dosyalarının Windows'ta uazantıları
|
||
".lib" biçiminde UNIX/Linux ve macOS sistemlerinde ise ".a" (archive sözcüğünden kısaltma) biçimindedir. Dinamik kütüphanelerin ise Windows'ta uzantıları ".dll"
|
||
(dynamic link library'den kısaltma) biçimindedir.
|
||
|
||
Tabii kütüphaneler programcılar tarafından da oluşturulabilmektedir. Programcılar da kütüphane dosyaları oluşturup link aşamasında linker'a oluşturdukları
|
||
kütüphaneye de bakmasını söyleyebilmektedirler. Yukarıda da belirttiğimiz gibi linker zaten (genellikle) otomatik olarak standart C fonk/Linux ve macOS sistemlerinde
|
||
ise ".so" (shared object sözcüklerinden kısaltma) biçimindedir. Bugün Windows, UNIX/Linux ve macOS sistemlerinde ağırlıklı biçimde dinamik kütüphaneler
|
||
kullanılmaktadır.
|
||
|
||
Statik kütüphane dosyaları "object modüllerden", object modüller ise fonksiyonlardan oluşmaktadır. Yani bir statik kütüphane aslında doğrudan fonksiyonları tutmaz.
|
||
Object modülleri tutar. Derlenmiş fonksiyonlar object modüllerin içerisindedir. O halde biz bir grup fonksiyonu statik kütüphane dosyasının içerisine yerleştirmek istersek
|
||
ona onu derleriz. Object modül haline getiririz. Object modülü statik kütüphane dosyasına ekleriz.
|
||
|
||
Linker programı kaynak kodda olmayan fonksiyonları belirtilen kütüphane dosyalarında aramaktadır. Eğer fonksiyonu linker bir statik kütüphane dosyasında
|
||
bulursa onun içinde bulunduğu object modülün tamamını oradan alarak çalıştırılabilen dosyaya enjekte eder. Böylece artık çalıştırılabilen dosya
|
||
gerçekten kütüphanedeki fonksiyonların makine kodlarını içeriyor durumda olur. Dolayısıyla artık bu program çalıştırılırken statik kütüphane dosyalarının
|
||
bulundurulmasına gerek kalmamaktadır. Burada iki önemli nokta vardır:
|
||
|
||
1) Linker fonksiyonu statik kütüphane dosyasında bulursa onun içinde bulunduğu object modülün hepsini çalıştırılabilen koda enjekte eder.
|
||
Örneğin biz yalnızca foo fonksiyonunu çağırmış olsak bile foo fonksiyonun içinde bulunduğu statik kütüphanede 100 tane fonksiyon varsa bu
|
||
100 fonksiyonun hepsi çalıştırılabilen dosyaya yazılacaktır.
|
||
|
||
2) Çalıştırılabilen dosya kütüphaneden çekilen object modülleri de içerir. Böylece bu programın çaçıştırılması sırasında artık statik kütüphanelere
|
||
gereksinim duyulmayacaktır.
|
||
|
||
Statik kütüphanelerin en önemli dezavantajı her çalıştırılabilen dosyanın kullanılan kütüphane fonksiyonlarının kodlarını barındırmasıdır. Bu da çalıştırılabilen
|
||
dosyaların diskte fazlaca yer kaplaması anlamına gelir. Örneğibn pek çok C programı printf fonksiyonunu kullanmaktadırç. O zaman o programlarının hepsinin
|
||
içerisinde printf fonksiyonun makine kodları bulunur.
|
||
|
||
Dinamik kütüpaheneler içerisinden bir fonksiyon çağrıldığında linker fonksiyonun kodunu dinamik kütüphaneden alarak çalıştırılabilen dosyaya yazmaz.
|
||
Bunun yerine linker çalıştırılabilen dosyaya işletim sistemi için "ilgili programın hangi danamik kütüphaneleri onların içerisindeki hangi fonksiyonları
|
||
kullandığı bilgisini" yazar. Böylece çalıştırılabilen dosyalar bu fonksiyonların kodlarını içermezler. İşletim sisteminin yükleyicisi (loader) çalıştırılabilen
|
||
dosyayı belleğe yüklerken o dosyanın kullanmış olduğu dinamik kütüphaneleri de belleğe yüklemektedir. Böylece program çalışırken akış belleğe yüklenmiş olan
|
||
dinamik kütüphane içerisine geçerek oradaki kodları çalıştırır. Bu sistemde dinamik kütüphanenin bir bölümü değil hepsi belleğe yüklenmektedir. (Yani
|
||
örneğin biz dinamik kütüphaneden tek bir fonksiyon çağırmış olsak bile onun tamamı belleğe yüklenir.) Dinamik kütüphaneler programın birer parçası kabul edilmektedir.
|
||
Dolayısıyla program başka bir makineye konuşlandırılırken yalnızca çalıştırılabilen dosya değil o dosyanın kullandığı dinamik kütüphane
|
||
dosyaları da o sisteme taşınmak zorundadır. Dinamik kütüphane kullanan programlar biraz daha geç yüklenme eğilimindedir. Ancak işletim sistemleri
|
||
farklı programlar aynı dinamik kütüphaneyi kullanıyorsa mümkün olduğu kadar o dinamik kütüphaneyi tekrar tekrar belleğe yüklemezler. Bu kınun bazı detayları vardır.
|
||
|
||
Bugün Microsoft C derleyicileri ve gcc derleyicileri ve bunların linker programları default durumda standart C fonksiyonlarını dinamik kütüphanelerden almaktadır.
|
||
Tüm standart C fonksiyonları genellikle tek bir dinamik kütüphanede toplanmıştır. Ancak bu durum değişebilmektedir. Tabii bu sistemler aynı standart C fonksiyonlarını
|
||
statik kütüphanelere de yerleştirmişlerdir. Programcı isterse default durumu değiştirerek standarty C fonksiyonlaırnın statik kütüphanelerden alınmasını sağlayabilir.
|
||
|
||
Microsoft derleycilerinde standart C fonksiyonlarının statik kütüphanelerden alınmasını sağlamak için komut satırında /MT ya da /MTd komut satırı argümanları
|
||
kullanılır. IDE'debn bu ayar Proje seçenlerinden C-C++/Code Generation/Runtime Library combobox'ından ayarlanmaktadır. gcc sisteminde -static linker seçeneği
|
||
derleme link işlemine eklenmelidir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Biz kendimizin tanımalamadağı yani kaynak dosyasımızda olmayan bir fonksiyonu çağırmış olalım. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
void xxxxx(void);
|
||
|
||
int main()
|
||
{
|
||
xxxxx();
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada hata hangi aşamada ortaya çıkacaktır? İşte kaynak kodda olmayan bir fonksiyon çağrıldığında eğer derleyici fonksiyonu kaynak kodda bulamazsa
|
||
derlemeyi başarıyla sonuçlandırır. Ancak objecet modüle linker için bir not yazar. Bu notta adeta şöyle demektedir: "Sevgili linker ben xxxxx isimli
|
||
bir fonksiyonun çağrıldığını gördüm. Ancak onu kaynak kodda bulamadım. Onu sen diğer object modüllerde ve kütüphane dosyalarında ara ve bulmaya çalış.
|
||
Bulmazasan yapacak bir şey yok". İşte linker bu notu okuyarak fonksiyonu arar ve onu bulursa sorun çıkmaz. Ancak bulamazsa link aşamasında error
|
||
oluşur. O halde olmayan bir fonksiyon çağrıldığında hata derleme aşamasında değil link aşamasında linker'ın bu fonksiyonu bulamaması biçiminde
|
||
ortaya çıkmaktadır.
|
||
|
||
Aslında standart C fonksiyonlarının standart C fonksiyonu olduğunu ne derleyici ne linker bilmektedir. Örneğin derleyici printf fonksiyonunu
|
||
gördüğünde onu kaynak kodda bulamadığı için object modüle linker için benzer notu yazar. Linker de printf fonksiyonunu tanımamaktadır. Ancak onu kütüphanelerde
|
||
ararken bulur. Halbuki xxxxx fonksiyonu bulamayacktır. O halde standart C fonksiyonlarının standart C fonksiyonları olduğu yalnızca programcılar tarafından
|
||
bilinmektedir. Tabii biz derleyicileri install ederken bu standart C fonksiyonları statik ya da dinamik kütüphane dosyalarına yerleştirilmiş durumda olur.
|
||
En azından bu garanti edilmiştir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında C derleyicisi kendi içerisinde iki modülden oluşmaktadır: Önişlemci (Preprocessor) ve Derleme (Compile) Modülleri:
|
||
|
||
.c ----> Önişlemci Modülü -----> Derleme Modülü -----> Object Dosya
|
||
|
||
Kaynak kod önişlemci modülü tarafından alınır. Önişlemci kaynak kod üzerinde çeşitli düzenlemeleri yapar ve kodu derleme modülüne verir. Derleme işleminin
|
||
bütün faaliyetleri derleme modülü tarafından yapılmaktadır. Yani C derleyicisi dediğimiz şey aslında bu derleme modülüdür. Ancak önişlemci de derleyicinin
|
||
bir parçasıdır.
|
||
|
||
C'de # ile başlayan satırlar önişlemciye ilişkindir. Yani önişlemci #'li satırlarla uğraşmaktadır.
|
||
|
||
#'den sonra ismine "önişlemci komutu" denilen bir anahtar sözcük gelir. Önişlemci komutu önişlemciye ne yapması gerektiğini belirtmektedir. Pek çok
|
||
önişlemci komutu vardır. Ancak bunların arasında "include" ve "define" önişlemci komutları en çok kullanılanlardır. Biz de kursumuzun bu bölümünde
|
||
bu iki komutu inceleyeceğiz. Diğer önişlemci komutlarını kursumuzun son bölümlerinde ele alacağız.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
28. Ders 13/09/2022 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
En çok kullanılan önişlemci komutlarından biri #define komutudur. Bu komutun genel biçimi şöyledir:
|
||
|
||
#define STR1 STR2
|
||
|
||
Burada #define komutundan sonra boşluk karakterleri atılıp ilk boşlukuz yazı mümesi elde edilir. Buna STR1 diyelim. Sonra yeniden boşluk karakterleri atılıp
|
||
satır sonuna kadar tüm karakterler elde edilir. Buna da STR2 diyelim. Önişlemci kaynak kodda STR1 gördüğü yerlere STR2 yazısını yerleştirmektedir. Örneğin:
|
||
|
||
#define MAX_VAL (10 + 20)
|
||
|
||
Burada STR1 "MAX_VAL" yazısını STR2 ise "(10 + 20)" yazısını temsil eder. İşte önişlemci kaynak kodda "MAX_VAL" gördüğü yere "(10 + 20)" yerleştirecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define MAX_VAL (10 + 20)
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
a = MAX_VAL * 2; /* derleme modülü burada a = (10 + 20) * 2 yazısını görecek */
|
||
|
||
printf("%d\n", a);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Önişlemcinin derleme yapmadığına yalnızca kaynak kod üzerinde yazısal düzenlemeler yaptığına dikkat ediniz. Önişlemciden geçirilmiş kod derleme modülüne
|
||
geldiğinde artık # ile başlayan satırlar koddan silinmiş olacaktır. Yani kaynak kod önişlemciden geçtikten sonra artık #'li satırlardan arındırılmış
|
||
durumda olur.
|
||
|
||
#define komutunun hesap yapmadığına yalnızca yer değiştirme yaptığına dikkat ediniz. Bu nedenle aşağıdaki kodda ekranda 50 yazısını göreceksiniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define MAX_VAL 10 + 20
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
a = MAX_VAL * 2; /* a = 10 + 20 * 2 */
|
||
|
||
printf("%d\n", a); /* 50 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir program içerisindeki birtakım sabitler sayı olarak değil #define komutu ile yazı biçiminde ifade edilirse kodu inceleyen kişi onu daha iyi anlamlandırır.
|
||
Bu nedenle C programcıları birtakım sayıları programda böyle yazısal biçimde ifade ederler. İşte #define komutu ile bir yazıya bir sayı karşılık getirilmesi
|
||
durumunda yazıya "sembolik sabit (symbolic constant)" denilmektedir. Örneğin:
|
||
|
||
#define MAX_SIZE 100
|
||
#define LINE_LENGTH 1024
|
||
#define NITEMS 12
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Önişlemciler tipik olarak geçici bir dosya açarak #'li satırlar üzerindeki düzenlemeyi bu geçici dosyada yaparlar. Sonra derleme modülüne önişlemden
|
||
geçirilmiş bu geçici dosyayı verirler. Derleme işleminden sonra da bu geçici dosyayı silerler. Bu nedenle biz bu geçici dosyayı görmeyiz. Ancak derleyicilerin
|
||
çoğunda önişlemcinin yarattığı dosyayı görebilmemin yolları da vardır. Microsost'un C derleyicisinde /P seçeneği gcc ve clang derleyicilerinde -E
|
||
seçeneği bu amaçla kullanılabilir. Tabii Visual Studio IDE'sinde aynı işlem görsel olarak proje seçenekerlinde C-C++/Preprocessor/Preprocessor to File
|
||
seçeneği ile de yapılabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Sembolik sabitlerin derleme modülü için bir sabit statüsünde olduğuna dikkat ediniz. C'de bazı durumlarda sabit ifadelerinin zorunlu olduğunu anımsayınız.
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#define CMD_DEL 1
|
||
#define CMD_DIR 2
|
||
#define CMD_COPY 3
|
||
...
|
||
|
||
switch (a) {
|
||
case CMD_DEL:
|
||
break;
|
||
case CMD_DIR:
|
||
break;
|
||
case CMD_COPY:
|
||
break;
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Önişlemci genel olarak C'yi bilmemektedir. Dolısıyla önişlemci komutlarının bir faaliyet alanı yoktur. Örneğin #define komutu kaynak kodun herhangi
|
||
bir yerinde yazılabilir. Bir fonksiyonun içinde yazılması ile dışında yazılması arasında farklılık yoktur. Neerede yazılmışsa oradan kaynak kodun sonuna kadarki
|
||
bölgede etki göstermektedir.
|
||
|
||
#define önişlemci komutları için en iyi yer programın tepesi ya da bir başlık dosyasının içidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
#define komutunda komutun STR1 kısmı değişken ya da anahtar sözcük olabilir. Sabit, ayıraç ya da operatör olamaz. Örneğin aşağıdaki komutlar geçersizdir:
|
||
|
||
#define + -
|
||
#define 100 200
|
||
#define ; +
|
||
|
||
Ancak komutun STR2 kısmı herhangi bir yazı olabilir. Aşağıdaki komutlar geçerlidir:
|
||
|
||
#define TERMINATOR ;
|
||
#define ADD +
|
||
|
||
|
||
Aşağıdaki örnekte program önişlemciden geçtikten sonra C'ce anlamı duruma gelecektir.
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
#include <stdio.h>
|
||
|
||
#define tam int
|
||
#define ana main
|
||
#define bos void
|
||
#define eger if
|
||
#define degilse else
|
||
#define don return
|
||
#define yazf printf
|
||
|
||
tam ana(bos)
|
||
{
|
||
tam i = 0;
|
||
|
||
eger (i > 0)
|
||
yazf("pozitif\n");
|
||
degilse
|
||
yazf("negatif ya da sifir\n");
|
||
|
||
don 0;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#define komutunda genel olarak STR1 yazısına "makro (macro)" da denilmektedir. Örneğin:
|
||
|
||
#define MAX 10
|
||
|
||
Burada MAX için "sembolik sabit" de diyebiliriz "makro" da diyebiliriz. Makro daha genel bir isimdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Önişlemci #'li satırlar üzerinde değişiklik yapmaz. Ancak önişlemci açtığı bir maroyu yeniden önişleme sokmaktadır. Ta ki artık önişleme soktuğunda
|
||
değiştirilecek bir yazı kalmayana kadar. Örneğin:
|
||
|
||
#define MAX 100
|
||
#define MIN (MAX - 50)
|
||
|
||
...
|
||
|
||
x = MIN;
|
||
|
||
Burada önişlemci MIN için önce aşağıdaki gibi bir açım yapar:
|
||
|
||
(MAX - 50)
|
||
|
||
Açtığı kodu yeniden önişleme sokar:
|
||
|
||
(100 - 50)
|
||
|
||
yazısını elde eder. Artık değiştirilecek bir şey kalmadığı için işlemi bitirir. Yukarıdaki #define komutlarını ters sırada yazsaydık da bir şey değişmeeyecekti:
|
||
|
||
#define MIN (MAX - 50)
|
||
#define MAX 100
|
||
|
||
...
|
||
|
||
x = MIN;
|
||
|
||
Burada yine önce MIN için şıu açımı yapar:
|
||
|
||
(MAX - 50)
|
||
|
||
Sonra açtığı makroyu yeniden önişleme sokar:
|
||
|
||
(100 - 50)
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Önişlemci iki tırnak içerisindeki yazılar üzerinde değişiklik yapmaz. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#define MAX 100
|
||
|
||
int main(void)
|
||
{
|
||
printf("MAX"); /* MAX */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada MAX iki tırnak içerisind eolduğu için önişlemci bu yazı üzerinde değişiklik yapmamıştır. Dolayısıyla ekrana MAX yazısı çıkacaktır. Tabii
|
||
biz bir yazıyı iki tırnaklı bir ifadeyle yer değiştirebiliriz. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#define MSG "Success\n"
|
||
|
||
int main(void)
|
||
{
|
||
printf("MSG"); /* MSG */
|
||
printf(MSG); /* Success */
|
||
|
||
return 0;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'nin standart başlık dosyalarının içerisinde fonksiyon prototiplerinin yanı sıra #define ile oluşturulmuş çeşitli sembolik sabitler de bulunmaktadır.
|
||
Dolayısıyla biz bu başlık dosyalarını include ettiğimizde artık bu semboik sabitleri kullanabiliriz. Örneğin <stdio.h> içerisinde EOF isimli, BUFSIZ
|
||
ve çeşitli başka isimlerle sembolikler define edilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
a = EOF; /* EOF <stdio.h> içerisinde define edilmiş */
|
||
printf("%d\n", a);
|
||
|
||
a = BUFSIZ; /* BUFSIZ <stdio.h> içerisinde define edilmiş */
|
||
printf("%d\n", a);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#define komutunda komutun STR2 kısmı hiç olmayabilir. Örneğin:
|
||
|
||
#define TEST
|
||
|
||
Bu durumda kaynak kodda STR1 görülen yer eboşluk atanır dolayısıyla STR1 yazıları silinmiş olur. Genel olarak STR2 kısmı olmayan #define komutları
|
||
bazen kodu inceleyen kişiler için ipucu vermek amacıyla bazen de diğer önişlemci komutları için kullanılmaktadır.
|
||
|
||
Aşağıdaki örnekte programın çalışmasında herhangi bir bozukluk olmayacaktır. Çünkü IN yazıları kod derleme modülüne verildiğinde silinmiş olacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define IN
|
||
|
||
void foo(IN int a)
|
||
{
|
||
printf("%d\n", a);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
IN IN
|
||
IN
|
||
|
||
IN
|
||
foo(10);
|
||
|
||
IN
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Makrolar parametreli olabilmektedir. Bunlara "parametreli makolar" ya da "fonksiyon gibi makrolar (function-like macros)" denilmektedir. Bir makroda
|
||
#define komutunun STR1 kısmında bir parantez açılırsa parantezin içerisindeki ',' ile ayrılmış isimlere "makro parametreleri" denilmektedir. Örneğin:
|
||
|
||
#define SQUARE(a) ...
|
||
|
||
Burada a makro parametreisidir. Örneğin:
|
||
|
||
#define MAX(a, b) ...
|
||
|
||
Burada a ve b makro parametreleridir.
|
||
|
||
Parametreli makrolar bir fonksiyon çağrısı gibi işleme sokulurlar. Zaten bu nedenle onlara C standartlarında "function-like macro" denilmektedir.
|
||
Bir makro çağrıldığında önişlemci parametreleri yerleştirerek makroyu açar. Örneğin:
|
||
|
||
#define square(a) a * a
|
||
...
|
||
|
||
result = square(10);
|
||
|
||
Burada 10 a parametresine karşı gelmektedir. O halde önişlemci kodu aşağıdkai gibi açacaktır:
|
||
|
||
result = 10 * 10;
|
||
|
||
Parametreleri makroların tamamen bir fonksiyon gibi kullanılması gerekir. Halbuki yukarıdaki square makrosu açıldığında tam bir fonksiyon etkisi yaratamaz. Örneğin:
|
||
|
||
result = square(10 - 2);
|
||
|
||
Eğer square bir fonksiyon olsaydı önce argümanın değeri hesaplanacaktı ve biz 8'in karesini elde edecektik. Ancak square gerçekte bir makrodur. Önişlemci kodu şöyle
|
||
açacaktır:
|
||
|
||
result = 10 - 2 * 10 - 2;
|
||
|
||
Bu açılmış hal derleme modülüne geldiğinde çarpma işleminin önceliği olduğu için istenileni yapamayacaktır. O halde fonksiyon gibi makro yazabilmek için
|
||
makro parametrelerinin paranzteze alınması gerekir:
|
||
|
||
#define square(a) (a) * (a)
|
||
...
|
||
result = square(10 - 2);
|
||
|
||
Artık önişlemci makroyu şöyle açacaktır:
|
||
|
||
result = (10 - 2) * (10 - 2);
|
||
|
||
Görüldüğü gibi şimdi makro fonksiyon gibi davranır hale gelmiştir. Ancak makro parametrelerinin pazarnteze alınması da yetmemektedir. Örneğin:
|
||
|
||
result = !square(1 - 1);
|
||
|
||
Eğer square bir fonksiyon olsaydı buradan 1 elde edilirdi. Ancak square yukarıdaki gibi makro olursa ! operatörünün önceliğinde dolayı farklı bir değer elde edilecektir.
|
||
|
||
#define square(a) (a) * (a)
|
||
...
|
||
result = !square(1 - 1);
|
||
|
||
Bu durumda açım şöyle yapılacaktır:
|
||
|
||
result = !(1 - 1) * (1 - 1)
|
||
|
||
Burada 0 elde edilecektir. O halde parametreli makro yazılırken makro ayrıca en dıştan da paranteze alınmalıdır:
|
||
|
||
#define square(a) ((a) * (a))
|
||
...
|
||
result = !square(1 - 1)
|
||
|
||
Artık açım şöyle yapılacaktır:
|
||
|
||
result = !((1 - 1) * (1 - 1))
|
||
|
||
Bu durumda foksiyon gibi makro yazabilmek için iki kuralı uygulamak gerekir:
|
||
|
||
1) Komutun STR2 kısmında makro parametreleri paranteze alınmalıdır.
|
||
2) Komutun STR2 kısmında makro en dıştan paranteze alınmalıdır.
|
||
|
||
Örneğin:
|
||
|
||
#define square(a) ((a) * (a))
|
||
|
||
Parametreli bir makro kodu izleyen tarafından tam bir fonksiyon taklidi yapabilmelidir. Parametreli makro bir fonksiyon değildir. Bir yer değiştirme
|
||
işlemini yapmakatradır. Ancak yer değiştirilen kod bir fonksiyon gibi etki göstermektedir. Önişlemci parametreli makro çağrıldığında argümanı makro
|
||
parametreleriyle eşler ve açımı ona göre yapar. Örneğin:
|
||
|
||
#define average(a, b, c) (((a) + (b) + (c)) / 3.0)
|
||
|
||
Biz bu makroyu şöyle işleme sokalım:
|
||
|
||
result = average(1 + 2, 3 + 4, 5 + 6);
|
||
|
||
Şimdi burada "1 + 2" a parametresi ile, "3 + 4" b parametresi ile ve "5 + 6" c parametresi ile eşleşecektir. Bu durumda önişlemci şöyle açım uygulaacaktır:
|
||
|
||
result = (((1 + 2) + (3 + 4) + (5 + 6)) / 3.0);
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define square(a) ((a) * (a))
|
||
|
||
int main(void)
|
||
{
|
||
int result;
|
||
|
||
result = square(10 - 2); /* result = ((10 - 2) * (10 - 2)); */
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
29. Ders 15/09/2022 Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi biz neden fonksiyon yazmıyoruz da fonksiyon etkisi yaratacak parametreli makro yazmaya çalışıyoruz? İşte fonksiyon çağırma işlemi bazı makine komutları
|
||
kullanılarak yapılmaktadır. Oysa makronun enjekte edilmesi fonksiyon çağırma işlemi anlamına gelmediği için fonksiyonun çağrılması sırasındaki
|
||
makine komutları elimine edilmş olur. Bir fonksiyon çağrıldığında çağrı koda eklenen makine komutlarının yarattığı dezavantaja İngilizce "function call overhead"
|
||
denilmektedir. Örneğin aşağıdaki gibi bir fonksiyon olsun:
|
||
|
||
int square(int a)
|
||
{
|
||
return a * a;
|
||
}
|
||
|
||
Biz bu fonksiyonu şöyle çağırmış olalım:
|
||
|
||
result = square(val);
|
||
|
||
32 bit Intel işlemcilerinde burada üretileck makine komutları şunlardır:
|
||
|
||
square:
|
||
push ebp
|
||
mov ebp, esp
|
||
|
||
mov eax, [ebp + 8]
|
||
imul eax, [ebp +8]
|
||
|
||
pop ebp
|
||
mov esp, ebp
|
||
|
||
...
|
||
push val
|
||
call square
|
||
add esp, 4
|
||
move result, eax
|
||
|
||
Aslında burada yalnızca iki makine komutu bu çarpma işlemini yapmaktadır:
|
||
|
||
mov eax, [ebp + 8]
|
||
imul eax
|
||
|
||
Diğer komutlar fonksiyon çağırma nedeniyle mecburen koda eklenen komutlardır. İşte bir iki satırlık fonksiyonların fonksiyon olarak değil de parametreli
|
||
makro biçiminde yazılması fonksiyon çağırma sırasında gereken makine komutlarının elimine edilmesine yol açar. Yani makro fonksiyon çağrısına göre
|
||
daha hızlı bir çalışmayı sağlar.
|
||
|
||
Uzun fonksiyonların makro olarak yazılması kötü bir tekniktir. Çünkü:
|
||
|
||
1) Uzun bir fonksiyonda birkaç makine komutunun elimine edilmesinin pratik bir faydası olmayabilir.
|
||
2) Uzun makroların yazılması zordur ve okunabilirliği azaltmaktadır.
|
||
3) Uzun makrolar her çağrılan yere enjekte edileceği için kodu büyütürler. Elde edilen hıza kodda yaşanan büyüme kar-zarar ilişkisi dikkate alındığında
|
||
toplamda zarar oluşturmaktadır.
|
||
|
||
|
||
Makroları çağırırken dikkat etmek gerekir. Makro argümanlarında ++ ve -- gibi operatörler "tanımsız davranış (undefined behavior)" oluşturabilirler. Örneğin:
|
||
|
||
#define square(a) ((a) * (a))
|
||
|
||
Bu makroyu şöyle çağırmış olalım:
|
||
|
||
result = square(++val);
|
||
|
||
Burada eğer square bir makro yerine fonksiyon olsaydı val önce artırılır, artırılmış değerin karesi alınırdı. Halbuki square bir makro olduüunda
|
||
önişlemci şöyle bir açım uygulayacaktır:
|
||
|
||
result = ((++val) * (++val))
|
||
|
||
Bu da "tanımsız davranış" oluşturacaktır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında küçük bazı standart C fonksiyonları derleyicileri yazanlar tarafından birer fonksiyon olarak değil de makro olarak yazılabilmektedir. Örneğin
|
||
<ctype.h> içerisindeki karakter test fonksiyonları pek çok derleyici tarafından makro olarak yazılmaktadır. Tabii programcı bunu bilmek zorunda değildir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir C programının yazıldığı editör dar ise biz uzun atomları nasıl yazabiliriz? Örneğin string'lerin tek bir satırda yazılması gerekir. Benzer biçimde
|
||
#define komutunun da tek bir satırda yazılması gerekir. Pekiyi ya editörümüzün genişliği yeterli değilse? Ya da tek satıra yazmak okunabilirliği bozuyorsa?
|
||
İşte C'de üst satır ile alt satırı sanki tek bir satırmış gibi derleyiciye göstermenin bir yolu vardır: Eğer bir satırda \ karakterinden hemen sonra LF (Line Feed)
|
||
karakteri gelirse (yani \ karakterinden hemen sonra ENTER tuşuna basılırsa) önişlemci tarafından işin başında bu iki satır \ ve LF karakterleri silinerek sanki tek
|
||
satır haline dönüştürülür. Böylece biz istersek bir satırı kesip aşağıdan bu yöntemle devam edebiliriz. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
printf("Hello\
|
||
World\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
Ya da örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int number_of_students;
|
||
|
||
number_of_\
|
||
students = 10;
|
||
|
||
printf("%d\n", number_of_students);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Bu sayede bir satırdan büyük olan makrolar daha okunabilir biçimde yazılabilirler. C standratlarına göre \ birleştirmesi yapıldıktan sonra derleyicilerin
|
||
en az 4095 karakterlik satırları desteklemesi gerekmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
#define error_check(result) \
|
||
{ \
|
||
if (!result) { \
|
||
printf("Error!\n"); \
|
||
exit(1); \
|
||
} \
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int test = 0;
|
||
|
||
error_check(0);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Çok satırlı makro yazarken programcı makroyu blok içerisine alır. Ancak blok içerisine alma yine de makronun tam olarak fonksiyon taklidi yapmasına
|
||
olanak sağlamaz. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
#define error_check(result) \
|
||
{ \
|
||
if (!result) { \
|
||
printf("Error!\n"); \
|
||
exit(1); \
|
||
} \
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int val = 10;
|
||
int status = 0;
|
||
|
||
if (val > 0)
|
||
error_check(status); /* compile time error */
|
||
else
|
||
printf("Everything is ok\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
Buradaki kodu önişlemci aşağıdaki gibi açacaktır:
|
||
|
||
if (val > 0)
|
||
if (!status) {
|
||
printf("Error!\n");
|
||
exit(1);
|
||
};
|
||
else
|
||
printf("Everything is ok\n");
|
||
|
||
Burada error_check(status) ifadesinin sonundaki noktalı virgül başımıza bela açmaktadır. Çünkü önişlemci kodu açtığında artık açılan kodda bu ';'
|
||
boş deyim olarak elşe alınacak ve bloklama yapılmadığı için sentaks hatası oluşacaktır. Tabii biz dıştaki if deyimini bloklarsak sorun çözülür ancak
|
||
makromuzun da tam bir fonksiyon taklisi yapamadığı açıktır. İşte bu tür durumlarda do-while deyimi imdadımıza yetişmektedir. Yukarıdaki makroyu şöyle ayzmış olalım:
|
||
|
||
#define error_check(result) \
|
||
do { \
|
||
if (!result) { \
|
||
printf("Error!\n"); \
|
||
exit(1); \
|
||
} \
|
||
} while (0)
|
||
|
||
Burada while parantezinin sonuna ';' yerleştirmediğimize dikkat ediniz. Bu while döngüsü aslında hiç dönmeyecektir. while döngüsünün sonundaki ';'
|
||
bir boş deyim değildir. Olması gereken bir atomdur. O halde makroyu aşağıdaki gibi kullanan kişi gerçekten de koyduğu ';' ile sentaksı tamamlar. Böylece de
|
||
do-while tek deyim olarak ele alınır:
|
||
|
||
if (val > 0)
|
||
error_check(status); /* Burada ';' artık boş deyim olmayacak, do-while deyimini tamamlayan ';' haline gelecek
|
||
else
|
||
printf("Everything is ok\n");
|
||
|
||
Önişmeci makroyu açtığında şu durum olşacaktır:
|
||
|
||
if (val > 0)
|
||
do {
|
||
if (!status) {
|
||
printf("Error!\n");
|
||
exit(1);
|
||
}
|
||
} while (0);
|
||
else
|
||
printf("Everything is ok\n");
|
||
|
||
Artık if deyiminin doğru kısmında tek bir deyim vardır.
|
||
|
||
Bu nedenle çok satırlı makroların bu biçimde do-while tekniği ile yazıldığını görürseniz şaşırmayınız.
|
||
|
||
Tabii çok satırlı makrolar if gibi deyimlerin içerisine yerleştirilemezler. Çok satırları makrolar geri dönüş değeri void olan fonksiyonlar gibi düşünülmeliedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
#define error_check(result) \
|
||
do { \
|
||
if (!result) { \
|
||
printf("Error!\n"); \
|
||
exit(1); \
|
||
} \
|
||
} while (0)
|
||
|
||
int main(void)
|
||
{
|
||
int val = 10;
|
||
int status = 0;
|
||
|
||
if (val > 0)
|
||
error_check(status);
|
||
else
|
||
printf("Everything is ok\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii makrlar başka amaçlarla da kullanılabilir. Biz her ne kadar henüz dizileri görmesek de aşağıdaki örnekte tüm elemanları 1 olan bir diziyi
|
||
makrolar yardımıyla kolay bir biçimde oluşturabilmekteyiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define FILL10(val) val, val, val, val, val, val, val, val, val, val
|
||
#define FILL100(val) FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val), FILL10(val)
|
||
#define FILL1000(val) FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val), FILL100(val)
|
||
|
||
int main(void)
|
||
{
|
||
int a[1000] = {FILL1000(1)};
|
||
|
||
for (int i = 0; i < 1000; ++i)
|
||
printf("%d ", a[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C99 ile birlikte C'ye de "inline fonksiyonlar" eklenmiştir. Inline fonksiyonlar fonksiyon gibi makroların güvenli bir alternatifidir. Kursumuzda bu konu
|
||
ileride ele alınacaktır. Bu nedenle fonksiyona benzer makroların #define ilke değil inline fonksiyonlar yoluyla yazılması C99 ve sonrasında daha uygun
|
||
olabilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
En çok kullanılan diğer bir önişlemci komutu da #include komutudur. #include komutunu açısal parantezler içerisinde ya da iki tırnak içerisinde
|
||
bir dosya ismi izler. Yani komutun genel biçimi şöyledir:
|
||
|
||
#include <dosya_ismi>
|
||
#include "dosya_ismi"
|
||
|
||
Biz şimdiye kadar hep include işleminde açısal parantezleri kullandık. Önişlemci #include komutunu gördüğünde belirtilen dosyayı açar. Geçici dosya yoluyla
|
||
dosyanın içerisiğini komutun yerleştirildiği yere yapıştırır. Böylece artık kod derleme modülüne geldiğinde derleme modülü #include komutunu değil o dosyanın içeriğini
|
||
görecektir.
|
||
|
||
include edilecek dosyanın uzantısı ".h" olmak zorunda dğeildir. Herhangi bir dosya da örneğin bir .c dosyası da include edilebilir.
|
||
|
||
Aşağıdaki örnekte "sample.c" dosyası "test.c" dosyasını include etmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/* sample.c */
|
||
|
||
#include <stdio.h>
|
||
#include "test.c"
|
||
|
||
int main(void)
|
||
{
|
||
foo();
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* test.c */
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#include komutu kaynak kodun herhangi bir yerine yerleştirilebilir. Tabii yerleştirme yerine göre yerleştirilen dosya içeriğinin anlamlı olması
|
||
gerekir. #include komutu da tek bir satıra yazılmak zorundadır.
|
||
|
||
Aşağıdaki örnekte #include komutu yerel bir blokta bulundurulmuştur. İçerik itibari ile bulundurulanyer geçerli bir kod oluşturur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/* sample.c */
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a =
|
||
#include "test.c"
|
||
;
|
||
|
||
printf("%d\n", a);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* test.c */
|
||
|
||
10
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#include komutunda önişlemci include edilen dosyayı komutun bulunduğu yere yapıştırktan sonra yeniden önişlem işlemlerini açtığı üzerinde de yapar.
|
||
Böylece biz include dosyalarına önişlemci komutlarını yerleştirebiliriz. Örneğin include ettiğimiz dosyaların içerisinde #define önişlemci komutları da
|
||
olabilir. Bu durumda bu komutlar da etki gösterecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/* sample.c */
|
||
|
||
#include <stdio.h>
|
||
#include "test.h"
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < SIZE; ++i)
|
||
printf("%d\n", i);
|
||
|
||
printf("%d\n", square(10));
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* test.h */
|
||
|
||
#define SIZE 10
|
||
#define square(a) ((a) * (a))
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
include edilen dosyada başka include komutları da buılunabilir. Bu durumda yukarıda da belirtildiği gibi özyinelemeli bir biçimde include işlemi
|
||
uygulanır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/* sample.c */
|
||
|
||
#include "project.h"
|
||
|
||
int main(void)
|
||
{
|
||
printf("%f\n", sqrt(10));
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* project.h */
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'nin standart başlık dosyalarında "include koruması (include guard)" uygulanmıştır. Bu nedenle bu standart başlık dosyalarının doğrudan ya da dolaylı
|
||
olarak birden fazla kez include edilmiş olması bir soruna yol açmaz. include koruması ileride ele alınacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdio.h> /* soruna yol açmaz, ama gereksiz */
|
||
#include <stdio.h> /* soruna yol açmaz, ama gereksiz */
|
||
|
||
int main(void)
|
||
{
|
||
printf("ok\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
include işleminde "döngüsel (cyclic) durumlar" error oluşturmaktadır. Örneğin biz "a.h" dosyasını include etmiş olalım. Bu dosya da "b.h" dosyasını include
|
||
etmiş olsun. "b.h" dosyası da "a.h" dosyasını include etmiş olsun. Bu durum döngüsellik oluşturmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
30. Ders 20/09/2022 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
include işleminin açısal parantezlerle yapılması ile iki tırnak içerisinde yapılması arasında farklılık vardır. Ancak bu farklılık C standartlarında
|
||
açık bir biçimde belirtilmemiş daha çok "derleyicileri yazanların isteğine (implementation defined)" bırakılmıştır.
|
||
|
||
Standartlarda bu konuda şunlar söylenmiştir:
|
||
|
||
- Eğer include işlemi açısal parantezlerle yapılmışsa önişlemci dosyayı kendisinin belirlediği bazı diziblerde arar. Bu dizinlerin nasıl belirleneceği
|
||
derleyicileri yazanların isteğine bırakışmıştır.
|
||
|
||
- Eğer dosya ismi iki tırnak içerisinde belirtilmişse bu durumda önişlemci dosyayı kendisinin belirlediği bir biçimde ve dizinlerde arar. Ancak dosya
|
||
bu aramada bulunamazsa sanki include işlemi açlsal parantezlerle yapılmış gibi bu kez dosya açısal parantezlerle belirtildiğinde aranan dizinlerde de aranır.
|
||
|
||
Her ne kadar standartlar belirlemeleri oldukça gevşek bırakmışsa da uygulamada pek çok derleyici şu biçimde işlem yapmaktadır:
|
||
|
||
- Eğer dosya açısal parantezlerle include edilmişse dosya C'nin standart başlık dosyalarının yüklendiği dizinde aranmaktadır. Programcılar genellikle C'nin standart
|
||
başlık dosyalarını bu biçimde include ederler.
|
||
|
||
- Eğer dosya iki tırnak içerisinde include edilmişse bu durumda yaygın derleyiciler dosyayı önce "o anda bulunulan dizinde (current working directory)" aramaktadır.
|
||
Eğer dosya o anda bulunulan dizinde bulunamazsa bu kez dosya standart C başlık dosyalarının bulunduğu dizinde de aranmaktadır.
|
||
|
||
Bu durumda en yaygın kullanım programcının standart başlık dosyalarını açısal parantezlerle kendi başlık dosyalarını iki tırnak ile include etmesidir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include "project.h"
|
||
|
||
İki tırnak ile include işlemi yapıldığında eğer Visual Studio gibi IDE'lerde çalışıyorsanız. Bu durumda "içinde bulunulan dizin (current working directory)"
|
||
proje dizini olacaktır. Ancak komut satırından derleme işlemini yapıyorsanız içinde bulunulan dizinde promptta gördüğünüz dizin olacaktır.
|
||
|
||
C'nin standart başlık dosyalarının iki tırnak ile include edilmesinde bir sorun oluşmayacağına dikkat ediniz. Ancak kendi başlık dosyalarınızı açısal parantezlerle
|
||
include ederseniz muhtemelen önişlemci dosyayı bulamayacaktır.
|
||
|
||
C derleyicisi kurulurken (örneğin Visual Studio IDE'si kurulurken) başlık dosyalarının hangi dizine yerleştirileceği derleyiciden derleyiciye hatta aynı derleyicilerde
|
||
versyiondan versiyona değişebilmektedir. UNIX/Linux sistemleri geleneksel olarka standart başlık dosyalarını /usr/include dizinin içerisinde bulundurmaktadır.
|
||
Microsoft Visual Studio IDE'sinin kurulumu sırasında kurulum dizinini bize de sorabilmektedir. Ancak Microsoft versiyondan versiyona strateji değiştirebilmektedir.
|
||
|
||
Aslında açısal parantez ile include işlemi yapıldığında önişlemcinin arama dizinlerine ekler yapılabilmektedir. Microsft Visual Studio IDE'sinde
|
||
Proje seçeneklerinde "C-C++/General/Additional Include Directories" sekmesinde dizinler aralarında ';' konularak girilebilmektedir. Bu durumda
|
||
önişlemci açısal parantazlerle include işlemi yapıldığında burada girilen dizinlere de bakmaktadır. Hatta projeden bağımsız olarak bu dizinlere kalıcı eklemeler de
|
||
yapılabilmektedir. Microsoft cl.exe kmut satırı derleyicisinde gcc ve clang derleyicilerinde -I seçeneği de bu amaçla kullanılabilmekltedir. Örneğin:
|
||
|
||
gcc -o sample -I /home/kaan/Study/C sample.c
|
||
|
||
Birden fazla dizine bakılması için birden fazla kez -I seçeneği kullanmak gerekir. Örneğin:
|
||
|
||
gcc -o sample -I /home/kaan/Study/C -I /home/kaan/personal sample.c
|
||
|
||
Ayrıca derleyiciler bu dizinleri belirlemek için bazı çevre değişkenlerinden de faydalanabilmektedir. Örneğin gcc derleyicilerinde CPATH çevre değişkenine
|
||
derleyici önişlem aşamasında başvurmaktadır.
|
||
|
||
#include komutunda dosya isimlerinin yol ifadesi içerip içermeyeceği de derleyicileri yazanların isteğine bırakılmıştır. Genel olarak dosya isimlerinde yol ifadesi
|
||
kullanmayınız. Derleyicilerin çoğu en azından göreli yol ifadelerine izin vermektedir.
|
||
|
||
Standart C başlık dosyalarının include edilme sırasının hiçbir önemi yoktur. Genellikle programcılar çok kullanılan başlık dosyalarını daha daha yukarıda
|
||
include etme eğilimindedirler.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir derleyicinin standartlara uygunluğu "standartlar auygun programı başarılı bir biçimde derlemesi ile" ölçülmektedir. Daha önceden de belirtildiği gibi
|
||
standartlara uygun olmayab hatalı kodların derleyiciler tarafından derlenip derlenmemesi derleyicilerin bir tercihidir. İşte C derleyicileri standartlarda
|
||
olmayan ek birtakım özelliklere de sahip olabilmektedir. Bunlara "eklenti (extension)" denilmektedir. Örneğin bir C derleyicisi standartlarda olmayan ekstra
|
||
deyimlere, ekstra türlere, ekstra fonksiyonlara sahip olabilir. Burada önemli olan standartlara uygun programların başarılı bir biçimde derlenip
|
||
derlenmediğidir. Yani standartlar aslında asgariyi belirtmektedir. Tabii her derleyicinin kendine özgü eklentileri bulunabilmektedir. Bu durumda spesifik bir
|
||
derleyicinin eklentilerini kodumuzda kullanırsak bu kod başka derleyicilerde derlenmeyebilir. Bazı eklentiler oldukça yaygındır. Hatta pek çok programcı bunu
|
||
standart bir özellik sanmaktadır.
|
||
|
||
Örneğin Linux çekirdeğinin kaynak kodlarında çok sayıda gcc eklentisi kullanılmıştır. Bu durumda biz Linux kaynak kodlarını örneğin Microsft derleyicilerinde
|
||
derleyemeyiz. Ancak gcc derleyicilerinde derleyebiliriz. Burada da görüldüğü gibi eklentilerin yoğun kullanılması kaynak kodun belli bir derleyiciye bağlı
|
||
olmasına yol açmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'nin üç operand'lı (ternary) tek bir operatörü vardır. Bu operatöre "koşul operatörü (conditional operator)" denilmektedir. Koşul operatörü ?: ile
|
||
belirtilir ve gene kullanımı şöyledir:
|
||
|
||
ifade1 ? ifade2 : ifade3
|
||
|
||
Koşul operatörü if deyimini çağrıştıran ancak deyim olmayan bir operatördür. Her operatörde olduğu gibi koşul operatörü de bir değer üretir.
|
||
Koşul operatörü şöyle çalışır: Önce soru işaretinin solundaki ifade yapılır. Bu ifade sıfır dışı bir değerse (yani doğruysa) yalnızca soru işareti ve iki nokta üst üste
|
||
arasındaki ifade yapılır. Eğer bu ifade sıfır ise (yani yanlış ise) bu durumda da yalnızca iki nokta üst üstenin sağındaki ifade yapılır. Koşul operatörünün
|
||
çalışması if deyimine benziyor olsa da koşul bir değer üretmektedir. Programcı koşul operatörünün ürettiği değeri genellikle bir nesneye atar.
|
||
Koşul operatörü soru işaretinin solundaki ifade sıfır dışı bir değerdeyse soru işareti ve iki nokta üst üste arasındaki ifadenin değerini üretir,
|
||
soru işaretinin solundaki ifade sıfır ise iki nokta üst üstenin sağındaki ifadenin değerini üretir.
|
||
|
||
Örneğin:
|
||
|
||
result = val % 2 == 0 ? 100 : 200;
|
||
|
||
Burada val çift ise koşul operatöründen 100, tek ise 200 elde edilecektir. Bu durumda result değişkenine 100 ya da 200 atanacaktır. Yukarıdaki kodun işlevsel eşdeğeri
|
||
if deyimi ile de oluşturulabilir:
|
||
|
||
if (val % 2 == 0)
|
||
result = 100;
|
||
else
|
||
result = 200;
|
||
|
||
Burada da görüldüğü gibi koşul operatörü ile yapılan her şey aslında if deyimiyle de yapılabilmektedir. Ancak koşul operatörü bazı durumlarda kompakt bir
|
||
görünüm sunduğu için daha kısa yazımlara olanak sağlamaktadır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val, result;
|
||
|
||
printf("Bir degere giriniz:");
|
||
scanf("%d", &val);
|
||
|
||
result = val % 2 == 0 ? 100 : 200;
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Koşul operatöründe operatörün ürettiği değerin bir biçimde kullanılması gerekir. Eğer operatörün ürettiği değer kullanılmazsa her ne kadar kod geçerli olsa da
|
||
kötü bir teknik uygulanmış olur. Örneğin:
|
||
|
||
val % 2 == 0 ? ++x : ++y; /* kötü teknik */
|
||
|
||
Koşul operatörünün kullanılması gerekn üç durum vardır.
|
||
|
||
1) Bir karşılaştırmanın sonucuna göre elde edilen değerin bir nesneye atanması gerektiği durumlar. Örneğin:
|
||
|
||
result = val % 2 == 0 ? 100 : 200;
|
||
|
||
Bu işlemin eşdeğer if karşılığı şöyledir:
|
||
|
||
if (val % 2 == 0)
|
||
result = 100;
|
||
else
|
||
result = 200;
|
||
|
||
2) Fonksiyon çağırırken argüman ifadelerinde koşul operatörü kullanılabilir. Örneğin:
|
||
|
||
foo(val % 2 == 0 ? 100 : 200);
|
||
|
||
Bu işlemin eşdeğer if karşılığı şöyledir:
|
||
|
||
if (val % 2 == 0)
|
||
foo(100);
|
||
else
|
||
foo(200);
|
||
|
||
3) return ifadelerinde de koşul operatörü kullanılabilir. Örneğin:
|
||
|
||
return val % 2 == 0 ? 100 : 200;
|
||
|
||
Bu ifadenin de eşdeğer if karşılığı şöyledir:
|
||
|
||
if (val % 2 == 0)
|
||
return 100;
|
||
else
|
||
return 200;
|
||
|
||
|
||
Aşağıdaki örnekte sayının çift ya da tek olduğu koşul operatörü sayesinde pratik bir biçimde ekrana yazdırılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val, result;
|
||
|
||
printf("Bir degere giriniz:");
|
||
scanf("%d", &val);
|
||
|
||
printf(val % 2 == 0 ? "cift\n" : "tek\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Örneğin 0'dan 100'e kadar sayıları beşer beşer aşağıdkai gibi yazdırmak isteylim:
|
||
|
||
0 1 2 3 4
|
||
5 6 7 8 9
|
||
10 11 12 13 14
|
||
...
|
||
|
||
Çözümlerden biri aşağıdaki gibi olabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < 100; ++i) {
|
||
printf("%d", i);
|
||
putchar(i % 5 == 4 ? '\n' : ' ');
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında yukarıdaki kodu aşağıdaki gibi daha kompakt biçimde de yazabilirdik. Bu koddaki kritik bölüm şudur:
|
||
|
||
printf("%d%c", i, i % 5 == 4 ? '\n' : ' ');
|
||
|
||
Burada %d format karakteri i ile, %c format karakteri ise '\n' ya da ' ' ile eşleşmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < 100; ++i)
|
||
printf("%d%c", i, i % 5 == 4 ? '\n' : ' ');
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte max fonksiyonu iki parametresinin büyük olanına geri dönmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int max(int a, int b)
|
||
{
|
||
return a > b ? a : b;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int result;
|
||
|
||
result = max(3, 7); /* 7 */
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Daha önceden de belirttiğimiz gibi birer satırlık fonksiyonların makro olarak yazıalması genel bir hız kazancı sağlamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define MAX(a, b) ((a) > (b) ? (a) : (b))
|
||
|
||
int main(void)
|
||
{
|
||
int result;
|
||
|
||
result = MAX(3, 7); /* result = ((3) > (7) ? (3) : (7)) */
|
||
printf("%d\n", result); /* 7 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Koşul operatörü öncelik tablosunda atama operatörünün hemen yukarısında sağdan sola grupta bulunmaktadır:
|
||
|
||
() Soldan-Sağa
|
||
+ - ++ -- ! (tür) Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Koşul operatörü iç içe (nested) kullanılabilir. İç içe kullanımda parantez kullanmaya gerek yoktur. Örneğin üç sayının en büyüğünü bulmaya çalışalım:
|
||
|
||
result = a > b ? a > c ? a : c : b > c ? b : c;
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a, b, c;
|
||
int result;
|
||
|
||
printf("a:");
|
||
scanf("%d", &a);
|
||
|
||
printf("b:");
|
||
scanf("%d", &b);
|
||
|
||
printf("c:");
|
||
scanf("%d", &c);
|
||
|
||
result = a > b ? a > c ? a : c : b > c ? b : c;
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki gibi iç içe koşul operatöründe gerekmese bile okunabilirliği artırmak için parantez kullanılmalıdır. Örneğin:
|
||
|
||
result = a > b ? (b > c ? b : c) : (b > c ? b : c);
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a, b, c;
|
||
int result;
|
||
|
||
printf("a:");
|
||
scanf("%d", &a);
|
||
|
||
printf("b:");
|
||
scanf("%d", &b);
|
||
|
||
printf("c:");
|
||
scanf("%d", &c);
|
||
|
||
result = a > b ? (a > c ? a : c) : (b > c ? b : c);
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
31. Ders 22/09/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Koşul operatörünün öncelik tablosunda hemen atama operatörlerinin yukarısında olduğunu anımsayınız. Örneğin:
|
||
|
||
x = a % 2 == 0 ? 100 + 200 : 300 + 400;
|
||
|
||
Burada derleyiciye göre iki operatör vardır: Koşul operatörü ve atama operatörü. Diğer operatörler aslında koşul operatörünün operand'larını oluşturmaktadır.
|
||
Pekiyi neden yukarıdaki örnekte soru işaretinin solundaki her şey koşul operatörünün ilk operandını oluşturmamaktadır? İşte ayrıştırma (parsing) şöyle yapılmaktadır:
|
||
Derleyici soru işaretinin solunda koşul operatöründen daha düşük öncelikli bir operatör görene kadar ilerler (örmeğimizde atama operatörüne kadar).
|
||
O kısım koşul operatörünün birinci operandını oluşturmaktadır. Soru işareti ile ':' arasındaki kısım koşul operatörünün ikinci operandını ve ':' den koşul operatöründen
|
||
daha düşük öncelikli operatöre kadar kısım koşul operatörünün üçüncü kısmını oluşturmaktadır.
|
||
|
||
Bazen bir operatörü koşul operatörünün operandı olmaktan çıkartmak isteyebiliriz. Bunun için parantezlerin kullanılması gerekir. Örneğin:
|
||
|
||
x = (a % 2 == 0 ? 100 : 200) + 300;
|
||
|
||
Burada a çift ise x'e 100 + 300, tek ise 200 + 300 atanacaktır. Çünkü artık '+' operatörü koşul operatörünün üçüncü operandı olmaktan çıkarrılmıştır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
int result;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
result = (a % 2 == 0 ? 100 : 200) + 300;
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Koşul operatörünün ürettiği değerin tür ikinci ve üçün operand'ların işlem öncesi otomatik tür dönüştürmesine sokulmasıyla
|
||
elde edilen türdür. Örneğin sebolik olarak:
|
||
|
||
koşul ? double : int
|
||
|
||
Bu durumda koşul operatörü double türden değer üremektedir. Örneğin:
|
||
|
||
koşul ? char : short
|
||
|
||
Bu durumda koşul operatörü int türden değer üretecektir. Koşul operatörünün operand'ları aynı türden yapı ya da birlik
|
||
olabilir. Ya da aynı türden adresler de olabilir. Bu konudaki ayrıntılar ileride ele alınacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdki örnekte belli bir tarihin hangi gün olduğunu ekrana (stdout dosyasına) yazan disp_day isimli fonksiyon yazılmıştır.
|
||
Bu örneği anlayabilmek için şu noktalara dikkat ediniz:
|
||
|
||
- Bir yılın artık olup olmadığı şöyle belirlenmektedir: 4'e tam bölünüp 100'e tam bölünmeyen ya da 400'e tam bölünen yıllar artıktır.
|
||
- Önce 01/01/1900'den ilgili tarihe kadar geçen gün sayısı hesaplanmıştır. Sonra bu değerin 7'ye bölümünden elde edilen kalana bakılmıştır.
|
||
- 01/01/1900 güneü Pazar günüdür.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define isleap(year) ((year) % 4 == 0 && (year) % 100 != 0 || (year) % 400 == 0 )
|
||
|
||
long total_days(int day, int month, int year)
|
||
{
|
||
long tdays = 0;
|
||
|
||
for (int i = 1900; i < year; ++i)
|
||
tdays += isleap(i) ? 366 : 365;
|
||
|
||
switch (month - 1) {
|
||
case 12: /* unreachable */
|
||
tdays += 31;
|
||
case 11:
|
||
tdays += 30;
|
||
case 10:
|
||
tdays += 31;
|
||
case 9:
|
||
tdays += 30;
|
||
case 8:
|
||
tdays += 31;
|
||
case 7:
|
||
tdays += 31;
|
||
case 6:
|
||
tdays += 30;
|
||
case 5:
|
||
tdays += 31;
|
||
case 4:
|
||
tdays += 30;
|
||
case 3:
|
||
tdays += 31;
|
||
case 2:
|
||
tdays += isleap(year) ? 29 : 28;
|
||
case 1:
|
||
tdays += 31;
|
||
}
|
||
|
||
tdays += day;
|
||
|
||
return tdays;
|
||
}
|
||
|
||
void disp_day(int day, int month, int year)
|
||
{
|
||
long tdays;
|
||
|
||
tdays = total_days(day, month, year);
|
||
|
||
switch (tdays % 7) {
|
||
case 0:
|
||
printf("Pazar\n");
|
||
break;
|
||
case 1:
|
||
printf("Pazartesi\n");
|
||
break;
|
||
case 2:
|
||
printf("Sali\n");
|
||
break;
|
||
case 3:
|
||
printf("Carsamba\n");
|
||
break;
|
||
case 4:
|
||
printf("Persembe\n");
|
||
break;
|
||
case 5:
|
||
printf("Cuma\n");
|
||
break;
|
||
case 6:
|
||
printf("Cumartesi\n");
|
||
break;
|
||
}
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
disp_day(23, 4, 1920);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bilgisayar sistemlerinde ""ana belleğin (main memory)" (RAM olarak da bilinir) her bir byte'ına ilk byte 0 olmak üzere artan sırada bir sayı karşılık
|
||
getirilmiştir. Bu sayıya ilgili byte'ın "doğrusal adresi (linear address)" denilmektedir. Doğrusal adres terimi yerine "fiziksel adres (physical address)" terimi de
|
||
kullanılabilmektedir. Ancak doğrusal adres terimi "sayfalama (paging)" mekanizmasının bulundurğu işlemcilerde daha çok tercih edilmektedir.
|
||
|
||
Doğrusal adresler bilgisayarın çalışma prensibinde mikroişlemciler tarafından elektriksel düzeyde kullanılmaktadır. Çünkü mikroişlemciler RAM'de belli bir yere
|
||
onun doğrusal adresini bilerek erişirler. Her byte'ın ayrı bir doğrusal adresi vardır. Anımsanacağı gibi C Programlama Dilinde her byte 8 bit olmak zorunda değildir.
|
||
Dolayısıyla char türü de 8 bit olmak zorunda değildir. Ancak neredeyse sistemlerin hemen hepsinde bir byte 8 bitten oluşmaktadır.
|
||
|
||
Doğrusal adresler geleneksel olarak 16'lık sistemde belirtilirler. Ancak böyle bir zorunluluk yoktur.
|
||
|
||
Bir programdaki her nesne bellekte yer kaplayacağına göre onların birer doğrusal adresleri vardır. Bir byte'tan büyük nesnelerin doğrusal adresleri
|
||
onların en düşük adres değeriyle ifade edilmektedir. Örneğin int bir nesne bellekte aslında 4 byte oturmuş durumdadır. O halde bu int nesnenin 4 adresi olması gerekir.
|
||
Ancak biz bu int nesnenin doğrusal adresini ifade ederken onun en düşük adres değerini kullanırız. Örneğin int türden a nesnesi bellekte aşağıdaki gibi bulunuyro olsun:
|
||
|
||
...
|
||
1FC14 --|
|
||
1FC15 |
|
||
1FC16 | a
|
||
1FC17---|
|
||
...
|
||
|
||
Biz burada a'nın doğrsal adresini 1FC14 olarak ifade ederiz. Gerçekten de aslında işlemci de bir nesneyi RAM'den alırken ya da RAM'e yazarken
|
||
eğer nesne 1 byte'tan uzunsa onun en düşük adresini belirtir. Yani işlemce RAM'e şunu söylemektedir: "1FC14 doğrusal adresinden başlayan 4 byte'lık değeri bana ver".
|
||
Tabii aslında işlemci de derleyici tarafından üretilmiş olan makine komutlarını çalıştırmaktadır. Eğer kod derlendikten sonra bellekte uygun yere yüklenirse
|
||
tüm program sorunsuz çalışabilmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de adresler de ayrı bir tür belirtmektedir. Ancak yazılımsal adres iki bileşenli bir bilgidir. Yazılımsal adresin bileşenleri "tür bileşeni" ve "sayısal bileşendir."
|
||
Sayısal bileşen bir doğrusal adres numarası belirtir. Tür bileşeni ise o doğrusal adres numarasından başlayan bilginin türünü belirtmektedir.
|
||
donanımsal olarak adres yalnızca bir sayıdan oluşmaktadır. Yazılımsal adresin sayısal bileşeni bir doğrusal adres numarası belirtir. Tür bileşeni ise
|
||
o doğrusal adresten başlayan nesnenin türüne ilişkindir.
|
||
|
||
Bundan sonra yalnızca "adres" denildiğinde "yazılımsal adres" belirtilecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
32. Ders 27/09/2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Adres bilgileri C'de ayrı bir tür belirtmektyedir. Bir adres sabiti oluşturmanın genel biçimi şöyledir:
|
||
|
||
(tür_bileşeni *) saysal_bileşen
|
||
|
||
Örneğin:
|
||
|
||
(int *) 0x1FC140
|
||
|
||
Burada bu adres sabitinin sayısal bileşeni 0X1FC140 biçimindedir. Bu bir doğrusal adres belirtir. Bu adres sabitinin tür bileşeni int biçiminmdedir.
|
||
Adres sabitlerinin tür bileşenlerinin 16'lık sistemde belirtilmesi zorunluı değildir. Ancak yaygın bir gösterimdir. Aslında yukarıdaki adres sabiti
|
||
bir tür dönüştürme işlemidir. Bu işlemin (int *) kısmıdaki int adresin tür bileşenini belirtir. Buradaki * ise adres kavramı için kullanılmaktadır.
|
||
|
||
C'de adres bilgileri adresin tür belişeni belirtilerek ifade edilir. Yani örneğin "adres" denmez, "int türden adres, double türden adres vs." denir.
|
||
İngilizce "adres" kavramı "pointer" sözcüğü ile ifade edilmektedir. "int türden adres" ise "pointer to int", "long türden adres" "pointer to long"
|
||
biçiminde belirtilir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aralarında fiziksel ya da mantıksal ilişki bulunan bir grup nesnenin oluşturğu topluluğa "veri yapısı (data structure)" denilmektedir. Veri yapısı
|
||
kavramı bir grup nesneyi çağrıştırmalıdır. Örneğin diziler, bağlı listeler, kuyruk sistemleri bir grup nesneden oluşan topluluklardır. Bunlar birer veri yapısıdır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Diziler (arrays) elemanları aynı türden olan ve bellekte ardışıl bir biçimde bulunan veri ytapılarıdır. Dizilerin elemanları aynı türdendir. Elemanlar arasında
|
||
hiç boşluk yoktur. Buradaki "ardışıl (contiguous)" bir elemandan sonra hemen diğerinin geldiği yani arada hiç boşluk olmadığı anlamına gelmektedir.
|
||
|
||
Dizi tanımlamanın genel biçimi şöyledir:
|
||
|
||
<tür> <dizi_ismi><[<uzunluk_ifadesi>]>;
|
||
|
||
Örneğin:
|
||
|
||
int a[10];
|
||
double b[20];
|
||
|
||
C90'da dizi tanımlamasında dizi uzunluklarının sabit ifadesi biçiminde belirtilmesi zorunludur. Ancak C99 ile birlikte yerel diziler için dizi uzunluklarının
|
||
sabit ifadesi yerine değişken içeren ifadelerle de belirtilmesine olanak sağlanmıştır. Örneğin:
|
||
|
||
{
|
||
int n = 10;
|
||
int a[n]; /* C90'da geçersiz, C99 ve ötesinde geçerli */
|
||
...
|
||
}
|
||
|
||
C++ her ne kadar C'yi kapsıyor olsa da C99 ile eklenen bu özelliği hiçbir zaman benimsememiştir. Microsoft C derleicileri de dil ayarı C99, C11, C17 yapılsa
|
||
bile halen bu özelliği desteklememktedir.
|
||
|
||
Bir diziyi dizi yapan iki özellik vardır:
|
||
|
||
1) Dizinin tüm elemanları aynı türdendir.
|
||
2) Elemanlar arasında hiç boşluk yoktur. Yani elemanlar bbellekte ardışıl bir biçimde tutulur.
|
||
|
||
Biz bir grup nesneyi tanımladığımızda bunların ardışıullığı konusunda C'de hiçbir garanti verilmemektedir. Örneğin:
|
||
|
||
int x, y, z;
|
||
|
||
Burada x, y ve z'nin bellekteki yerleşimleri herhangi bir biçimde olabilir. Ardışıllığı garanti edilmemektedir. Oysa örneğin:
|
||
|
||
int a[3];
|
||
|
||
Buradaki 3 int eleman kesinlikle ardışıl bir biçimde tutulur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bir dizi bütünsel olarak işleme sokulamaz. Dizinin elemanlarına erişlilir. Dizinin elemanları bağımısız nesneler biçiminde işleme sokulur.
|
||
Dizi elemanlarıne erişmek için [...] operatörü kullanılır. Elemana erişmenin genel biçimi şöyledir:
|
||
|
||
dizi_ismi[ifade]
|
||
|
||
Dizinin ilk elemanı 0'ıncı indeksli elemandır. Bu durumda n elemanlı bir dizinin son elemanı n - 1'inci indeksli elemanıdır. Dizi elemanlarına erişilirken
|
||
köşeli parantez içerisindeki ifade sabit ifadesi olmak zorunda değildir. Ancak index belirten ifadenin tamsayı türlerine ilişkin olması zorunludur.
|
||
Aslında eleman erişmekte kullanılan köşeli parantezler "tek operandlı sonek (unary postfix)" operatör belirtmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dizilerin en önemli kullanılma nedeni bir döngü içerisinde onların tüm elemanlarının işleme sokulmasıdır. Örneğin:
|
||
|
||
int a[1000];
|
||
|
||
for (int i = 0; i < 1000; ++i)
|
||
a[i] = 0;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10];
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
a[i] = i * i;
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d\n", a[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Global bir dizinin tüm elemanlarında başlangıçta 0 değerleri bulunur. Ancak yerel dizilerin içerisinde başlangıçta çöp değerler bulunmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir bildirimde tür belirten sözüğün dışındaki atomlara "dekleratör (declarator)" denilmektedir. Örneğin:
|
||
|
||
int a, b, c;
|
||
|
||
Burada int tür, a, b, ve c dekleratörlerdir. Örneğin:
|
||
|
||
double a[10];
|
||
|
||
Burada double tür a[10] dekleratördür. C'de bildirimdeki tür tüm dekleratörlerin ortak türüdür. Örneğin:
|
||
|
||
int a[10], b;
|
||
|
||
Bu tanımlama geçerlidir. Burada a 10 elemanlı int bir dizi b ise int bir nesnedir. Yani dizilerle normal nesneler beraber tanımlanabilirler.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir diziye tanımlar tanımlamaz küme parantezleri içerisinde ilkdeğer verebiliriz. Bju durumda verilen ilkdeğerler sırasıyla dizi elemanlarına
|
||
yerleştirilir. Örneğin:
|
||
|
||
int a[5] = {10, 20, 30, 40, 50};
|
||
|
||
Babii bu işlem daha sonra yapılamaz. Örneğin:
|
||
|
||
intr a[5];
|
||
|
||
a = {10, 20, 30, 40, 50}; /* geçersiz! */
|
||
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[5] = {10, 20, 30, 40, 50};
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dizinin az sayıda elemanına ilkdeğer verilebilir. Bu durumda gerei kalan elemanlar dizi yerel de olsa, global da olsa kesinlikle derleyici tarafından
|
||
sıfırlanmaktadır. Ancak dizinin fazla sayıda elemanına ilkdeğer vermek geçersizdir. Örneğin:
|
||
|
||
int a[5] = {10, 20, 30}; /* geri kalan 2 eleman 0 */
|
||
int b[5] = {10, 20, 30, 40, 50, 60}; /* error! dizi 5 elemanlık ancak 6 elemana ilkdeğer verilmiş */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[5] = {10, 20};
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
printf("%d ", a[i]); /* 10 20 0 0 0 */
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dizilere ilkdeğer verme işleminde küme parantezlerinin içi boş bırakılamaz. Örneğin:
|
||
|
||
int a[10] = {}; /* geçersiz! */
|
||
|
||
Yerel dizinin tüm elemanlarının 0 olmasını istiyorsak bunu en yalın ancak aşağıdaki gibi sağlayabiliriz:
|
||
|
||
int a[10] = {0}; /* a'nın tüm elemanları 0 */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dizilere ilkdeğer verilirken dizi uzunlukları belirtilmeyebilir. Bu durumda derleyici verilen ilkdeğerleri syar ve dizinin o uzunlukta açılmış olduğunu kabul eder.
|
||
Örneğin:
|
||
|
||
int a[] = {10, 20, 30}; /* burada dizi 3 uzunlukta */
|
||
int b[]; /* geçersiz! dizi uzunluğu belirtilmek zorunda */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C99 ile birlikte dizilere ilkdeğer vermede "designated initializer" denilen bir sentaks da fdile eklenmiştir. Bu sentaks sayesinde dizinin
|
||
ardışıl olmayan elemanlarına ilkdeğerverilebilmektedir. Örneğin biz 100 elemanlı int bir dizide dizinin yalnızca 25, 50, 75 ve 99'uncu elemanlarına değer
|
||
atamak isteyebiliriz. "Designated initializer" sentaksı şöyledir:
|
||
|
||
[<sabit ifadesi>] = değer
|
||
|
||
Örneğin:
|
||
|
||
int a[100] = {[25] = 100, [50] = 200, [75] = 300, [99] = 400};
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[100] = {[25] = 100, [50] = 200, [75] = 300, [99] = 400};
|
||
|
||
for (int i = 0; i < 100; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Designated initilizer sentaksından sonra normal ilkdfeğer vermelere devam edilebilir. Bu durumda sonra verilen ilkdeğerler designated initilizer'da
|
||
belirtilen indeksi izlemektedir. Örneğin:
|
||
|
||
int a[10] = {1, 2, 3, [6] = 100, 4, [8] = 200};
|
||
|
||
Burada 4 değeri 7'inci elemana yerleştirilecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {1, 2, 3, [6] = 100, 4, [8] = 200};
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]); /* 1 2 3 0 0 0 100 4 200 0 */
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Designated initlizer sentaksında köşeli parantez içerisindeki indeks değerlerinin artan sırada olma zorunluluğu yoktur. Örneğin:
|
||
|
||
int a[10] = {[5] = 100, 200, [1] = 300};
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {[5] = 100, 200, [1] = 300};
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]); /* 0 300 0 0 0 100 200 0 0 0 */
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Designated initializer sentaksında dizinin aynı elemanına birden fazla kez değer atama durumu oluşabilir. Bu tür işlemler anlamsız olsa dayasaklanmamıştır.
|
||
Örneğin:
|
||
|
||
int a[10] = {10, 20, 30, [0] = 100, 200}; /* geçerli ama anlamsız */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {10, 20, 30, [0] = 100, 200};
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]); /* 100 200 30 0 0 0 0 0 0 0 */
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Designated initializer sentaksında dizi uzunluğu yine belirtilmeyebilir. Bu durumda sentaksta belirtilen en yüksek indeks temel alınarak dizi uzunluğu
|
||
belirlenmektedir. Örneğin:
|
||
|
||
int a[] = {10, 20, 30, [90] = 100, 200};
|
||
|
||
Burada dizi 92 eleman uzunluğunda açılacaktır.
|
||
|
||
Ancak dizi uzunluğu belirtilmişse designated initializer sentaksında indeks değeri dizinb uzunluğuna eşit ya da ondan büyük olamaz. Örneğin:
|
||
|
||
int a[50] = {10, 20, 30, [90] = 100}; /* geçersiz! */
|
||
|
||
Tabii köşeli parantez içerisindeki indeks belirten ifadenin sabit ifadesi olması zorunludur:
|
||
|
||
int i = 20;
|
||
int a[50] = {10, 20, 30, [i] = 100}; /* geçersiz! i sabit ifadesi değil */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizinin en büyük elemanı şöyle bulunur: Önce ilk eleman en büyük kabul edilir ve bir değişkende saklanır. Sonra diğer tüm elemanlar tek tek gözden geçirilir.
|
||
Daha büyük eleman bulununca o eleman saklanır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
|
||
int main(void)
|
||
{
|
||
int a[SIZE] = {3, 5, 7, 7, 12, 67, 3, 34, 11, 23};
|
||
int max;
|
||
|
||
max = a[0];
|
||
|
||
for (int i = 1; i < SIZE; ++i)
|
||
if (a[i] > max)
|
||
max = a[i];
|
||
|
||
printf("%d\n", max);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıda bir dizinin aritmetik ortalamasını bulan bir program örneği verilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
|
||
int main(void)
|
||
{
|
||
int a[SIZE] = {3, 5, 7, 7, 12, 67, 3, 34, 11, 23};
|
||
int total;
|
||
double avg;
|
||
|
||
total = 0;
|
||
for (int i = 0; i < SIZE; ++i)
|
||
total += a[i];
|
||
|
||
avg = (double)total / SIZE;
|
||
|
||
printf("%f\n", avg);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir diziyi ters çevirmek için baştaki sondaki elemanları yer değiştirebiliriz. Tabii bu işlemi dizi uzunluğunun yarısı kadar yapmak gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
|
||
int main(void)
|
||
{
|
||
int a[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||
int temp;
|
||
|
||
for (int i = 0; i < SIZE / 2; ++i) {
|
||
temp = a[i];
|
||
a[i] = a[SIZE - 1 - i];
|
||
a[SIZE - 1 - i] = temp;
|
||
}
|
||
|
||
for (int i = 0; i < SIZE; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizide bir elemanı arayıp onu bulduğunda bulduğu yerin indeksini ekrana yazdıran bir program örneği aşağıda verilmiştir. Bir dizinin tüm elemanlarını
|
||
kontrol ederek arama işlemine "sıralı arama (sequential search)" denilmektedir. ASıralı aramda eğer arama başarılı ise (successful search)
|
||
ortalama karşılaştırma sayısı n / 2'dir. Ancak arama başarısız olursa n karşılaştırma yapılır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
|
||
int main(void)
|
||
{
|
||
int a[SIZE] = {23, 12, 76, 45, 23, 65, 11, 98, 42, 81};
|
||
int val;
|
||
int i;
|
||
|
||
printf("Bir değer giriniz:");
|
||
scanf("%d", &val);
|
||
|
||
for (i = 0; i < SIZE; ++i)
|
||
if (a[i] == val)
|
||
break;
|
||
|
||
if (i == SIZE)
|
||
printf("bulunamadi!\n");
|
||
else
|
||
printf("%d. indekste bulundu\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
33. Ders 29/09/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dizilerin sıraya dizilmesine İngilizce "sorting" denilmektedir. Dizileri sıraya dizmek için pek çok algoritma vardır. Bunlardan en yalını
|
||
"kabarcık sıralaması (bubble sort)" denilen yöntemdir. Bu yöntemde yan yana iki eleman karşılaştırılır. Duruma göre yer değiştirilir. Bu işlem bir kez
|
||
yapıldığında dizi sıraya dizilmiş olmaz. Ancak en büyük eleman (ya da en küçük eleman) sona gider. O halde bu işlemi diziyi daraltarak tekrar tekrar yapmak
|
||
gerekir. Algoritmanın döngü yapısı şöyledir: Dizinin uzunluğu n olmak üzere iç içe iki döngü vardır. Dıştaki döngü n - 1 kez, içteki döngü n - 1 - i kez
|
||
döndürülür.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
|
||
int main(void)
|
||
{
|
||
int a[SIZE] = {23, 12, 76, 45, 23, 65, 11, 98, 42, 81};
|
||
int temp;
|
||
|
||
for (int i = 0; i < SIZE - 1; ++i)
|
||
for (int k = 0; k < SIZE - 1 - i; ++k)
|
||
if (a[k + 1] < a[k]) {
|
||
temp = a[k];
|
||
a[k] = a[k + 1];
|
||
a[k + 1] = temp;
|
||
}
|
||
|
||
for (int i = 0; i < SIZE; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Kabarcık sıralamasının değişik gerçekleştirimi yapılabilir. Örneğin eğer yan yana elemanlar karşılaştırılıp hiç yer değiştirme yapılmıyorsa
|
||
dizi zaten sıraya dizilmiş demektir. Döngünün devam ettirilmesine gerek yoktur.
|
||
|
||
Aşağıdaki gerçekleştirimde eğer dizi zaten sıraya dizilmişse dış döngü devam ettirilmemektedr.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
|
||
#define TRUE 1
|
||
#define FALSE 0
|
||
|
||
int main(void)
|
||
{
|
||
int a[SIZE] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
|
||
int temp;
|
||
int replace_flag;
|
||
|
||
for (int i = 0; i < SIZE - 1; ++i) {
|
||
replace_flag = FALSE;
|
||
for (int k = 0; k < SIZE - 1 - i; ++k)
|
||
if (a[k + 1] < a[k]) {
|
||
temp = a[k];
|
||
a[k] = a[k + 1];
|
||
a[k + 1] = temp;
|
||
replace_flag = TRUE;
|
||
}
|
||
if (!replace_flag)
|
||
break;
|
||
}
|
||
|
||
for (int i = 0; i < SIZE; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki biçim do-while döngüsüyle de ifade edilebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
|
||
#define TRUE 1
|
||
#define FALSE 0
|
||
|
||
int main(void)
|
||
{
|
||
int a[SIZE] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
|
||
int temp;
|
||
int replace_flag;
|
||
int n = SIZE - 1;
|
||
|
||
do {
|
||
replace_flag = FALSE;
|
||
for (int k = 0; k < n; ++k)
|
||
if (a[k + 1] < a[k]) {
|
||
temp = a[k];
|
||
a[k] = a[k + 1];
|
||
a[k + 1] = temp;
|
||
replace_flag = TRUE;
|
||
}
|
||
--n;
|
||
} while (replace_flag);
|
||
|
||
for (int i = 0; i < SIZE; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Diğer çok bilinen bir sıralama yöntemine "seçerek sıralama (selection sort)" denilmektedir. Bu yöntemde dizinin en küçük elemanı bulunur. İlk elemanla
|
||
yer değiştirilir. Sonra dizi daraltılır. Aynı daraltılmış dizi için yapılır. İşlemler böyle böyle devam ettirilir. Örneğin:
|
||
|
||
| 8 3 6 1 5
|
||
|
||
1 | 3 6 8 5
|
||
1 3 | 6 8 5
|
||
1 3 5 | 8 6
|
||
1 3 5 6 | 8
|
||
|
||
Bu algoritmada iç içe iki döngü kullanılır. Dıştaki döngü diziyi daraltmakta kullanılır. İçteki döngü ise daraltılmış dizinin en küçük elemanını
|
||
bulup daraltılmış dizinin ilk elemanı ile yer değiştirir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
|
||
int main(void)
|
||
{
|
||
int a[SIZE] = {10, 9, 8, 7, 6, 5, 4, 3, 2, 1};
|
||
int min, min_index;
|
||
|
||
for (int i = 0; i < SIZE - 1; ++i) {
|
||
min = a[i];
|
||
min_index = i;
|
||
for (int k = i + 1; k < SIZE; ++k)
|
||
if (a[k] < min) {
|
||
min = a[k];
|
||
min_index = k;
|
||
}
|
||
a[min_index] = a[i];
|
||
a[i] = min;
|
||
}
|
||
|
||
for (int i = 0; i < SIZE; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizinin olmayan bir elemanına erişmeye çalışmak "tanımsız davranışa" yol açar. Örneğin:
|
||
|
||
int a[10];
|
||
|
||
for (int i = 0; i <= 10; ++i) /* tanımsız davranış! */
|
||
a[i] = 0;
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Daha önceden de belirttiğimiz gibi aslında bir yazı karakterlerden oluşan bir dizi belirtmektedir. Karakterler ise aslında o karakterlerin karakter tablosundaki
|
||
sıra numarasını belirtir. O halde aslında bir yazı bir sayı dizisi gibi ele alınabilir. Pekiyi mademki bir yazı bir sayı dizisi gibidir. O halde yazınıun karakterlerine
|
||
karşı gelen sayıları hangi türden dizide tutmalıyız? Tabii bunun için en uygun tür char türüdür. Çünkü zaten C'de char bir kadarkterin sıra numarasını tutabilecek
|
||
büyüklüğü temsil etmektedir. C'de karakterler 1 byte içerisnde tutulmaktadır. Karakterlerin sıra numaralarını tutmak için en uygun tür char türüdür.
|
||
O halde bir yazı char türden bir dizide tutulmalıdır. Yazının her bir karakteri char türden dizinin bir elemanında tutulursa dizi yazıyı tutar hale gelir.
|
||
|
||
Genel olarak yazıyı tutan char dizi yaznının uzunluğundan büyük olur. Yani bir char dizinin içerisindeki yazı onun başından itibaren belli bir kısmındadır.
|
||
Bu char diziyi alan programcı yazının bu dizinin başından başladığını bilir ancak nerede bittiğini de anlaması gerekir. İşte C'de char bir dizi
|
||
içerisindekiş yazının bitişl yeri özel bir karakterle belirtilmektedir. Bu karaktere "null kartakter" denir. Programcılar ve C'nin bazı semantik kuralları
|
||
bir yazının sonunda null karakter olması gerektiği konusunda anlaşmış durumdadırlar. null karakter karakter tablosunun ilk karakteridir ve sayısal değeri
|
||
0'dır. (Bunu '0' karakteri ile karıştırmayınız.) Null karakter '\0' ile temsil edilir. Aslında '\0' teknik olarak 0 sabiti ile aynı anlamdadır.
|
||
Ancak programcılar null karakter için '\0' gösterimini tercih ederler. Çünkü '\0' gösterini bir karakter görüntüsünde olduğu için okunabilirliği daha fazladır.
|
||
|
||
Programcı char türden bir dizinin içerisine bir yazı yerleştirecekse null karakteri yazının sonuna yerleştirmek onun sorumlulupundadır. Örneğin:
|
||
|
||
char s[10];
|
||
|
||
s[0] = 'a';
|
||
s[1] = 'l';
|
||
a[2] = 'i';
|
||
a[3] = '\0';
|
||
|
||
Null karakter dizinin içerisinde bir yer kaplar. Bu durumda n eleman uzunluğundaki bir char diziye biz en fazla n - 1 karakterli bir yazı yerleştirebiliriz.
|
||
Örneğin elimizde 10 elemanlı bir char dizi varsa biz onun içerisine en fazla 9ı karakterli bir yazı yerleştirebiliriz. Çünkü son elemanda null karakter olmak zorundadır.
|
||
Null karakterin dizinin sonunda değil dizinin içerisindeki yazının sonunda olması gerektiğine dikkat ediniz.
|
||
|
||
Biz bir char diziye ilkdeğer verme sentaksıyla da bir yazı yerleştirebiliriz. Örneğin:
|
||
|
||
char s[100] = {'a', 'n', 'k', 'a', 'r', 'a', '\0'};
|
||
|
||
Tabii burada aslında null karakteri hiç belirtmeseydik de dizinin geri kalan elemanları sıfırlanacağından dolayı, null karakter de 0 olduğu için
|
||
sanki null karakter yazının sonuna eklenmiş gibi olacaktı. Örneğin:
|
||
|
||
char s[100] = {'a', 'n', 'k', 'a', 'r', 'a'}; /* geçerli zaten 0 ile null karakter aynı */
|
||
|
||
Tabii null karakterin açıkça belirtilmesi daha anlaşılabilir bir görüntü sunmaktadır. Örneğin:
|
||
|
||
char s[] = {'a', 'l', 'i'}; /* geçerli ancak null karakter yazının sonuna eklenmemiş */
|
||
|
||
Burada dizi uzunluğu belirtilmediği için verilen ilkdeğerler kadar dizi açılır. Ancak null karakter yazının sonuna eklenmemiştir. Programcının
|
||
yazının sonuna null karakter eklenmesini sağlaması gerekir:
|
||
|
||
char s[] = {'a', 'l', 'i', '\0'};
|
||
|
||
C'de char, unsigned char ya da signed char türünden bir diziye iki tırnak ile bir yazı pratik bir biçimde de yerleştirilebilmektedr. Örneğin:
|
||
|
||
char s[100] = "ankara";
|
||
|
||
C'de char, unsigned char ve signed char türünden bir diziye iki tırnak ile ilkdeğer verilmişse bu durumda derleyici bu iki tırnak içerisindeki
|
||
karakterleri tek tek diziye yerleştirir. Yazının sonuna null karakteri kendisi ekler. Örneğin:
|
||
|
||
char s[] = "ankara";
|
||
|
||
Burada derleyici null karakteri kendisi ekleyeceği için dizinin 7 eleman uzunluğunda açıldığını kabul eder. İki tırnak ile yalnızca char, signed char ve
|
||
unsigned char türünden dizilere ilkdeğer verilebilmektedir. Örneğin:
|
||
|
||
int s[] = "ankara"; /* geçersiz! iki tırnak ile int bir diziye ilkdeğer verilemez! */
|
||
|
||
Daha sonra bir diziye iki tırnak ile atama yapamayız. İki tıornak sentaksının ilkdeğer verme sırasında geçerli olduğuna dikkat ediniz. Örneğin:
|
||
|
||
char s[100];
|
||
|
||
s = "ankara"; /* geçersiz! */
|
||
|
||
Bir diziye fazla sayıda elemanla ilkdeğer veremediğimizi belirtmiştik. Örneğin:
|
||
|
||
char s[3] = "ankara"; /* geçersiz! */
|
||
|
||
Özel bir durum olarak eğer iki tırnak içerisindeki karakter sayısı dizi unluğu kadar ise bu durum C'de geçerli kabul edilmektedir. Ancak derleyici bu duurmda
|
||
null karakteri yazının sonuna eklememektedir. Örneğin:
|
||
|
||
char s[3] = "ali"; /* geçerli ama dikkat null karakter yazının sonuna eklenmeyecek */
|
||
|
||
Bu durumda hata kaynağı oluşturabileceği gerekçesiyle C++'ta geçersiz kabul edilmektedir.
|
||
|
||
char, signed char ve unsigned char türünden dizilere iki tırnak ile ilkdeğer veridliğinde diziningeri kalan elemanlarının hepsi yine sıfırlanmaktadır. Örneğin:
|
||
|
||
char s[100] = "ali"; /* null karakter eklendikten sonra geri kalan elemanların hepsi sıfırlanır */
|
||
|
||
İlkdeğer verilirken iki tırnağın içi boş olabilir. Örneğin:
|
||
|
||
char s[100] = "";
|
||
|
||
Burada diziye null karakter yerleştirilir. Sonra geri kalan tüm elemanlar sıfırlanır. Tabii null karakterin numarası 0 olduğuna göre aslıunda tüm dizi sıfırlanmaktadır.
|
||
Örneğin:
|
||
|
||
char s[] = "";
|
||
|
||
Burada dizi 1 eleman olarak açılır ve o bir elemana null karakter yerleştirilir.
|
||
|
||
char türdne bir dizinin içerisinde sonu null karakterle biten bir yazı bulunuyor olsun. Bu yazıyı nasıl ekrana (stdout dosyasına) yazdırabiliriz?
|
||
|
||
char s[100] = "ankara";
|
||
|
||
for (int i = 0; s[i] != '\0'; ++i)
|
||
putchar(s[i]);
|
||
|
||
Tabii madem ki null karakterin sayısal değeri 0'dır. o zaman dönü şöyle de ifade edilebilirdi:
|
||
|
||
for (int i = 0; s[i]; ++i)
|
||
putchar(s[i]);
|
||
|
||
Ancak okunabilirlik bakımından önceki biçim tercih edilebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[100] = "ankara";
|
||
|
||
for (int i = 0; s[i] != '\0'; ++i)
|
||
putchar(s[i]);
|
||
putchar('\n');
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir char dizisinin başındaki yazının karakter uzunluğu aşağıdaki gibi bulunabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[100] = "istanbul";
|
||
int i;
|
||
|
||
for (i = 0; s[i] != '\0'; ++i)
|
||
;
|
||
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
char türden bir dizinin içerisindeki yazıyı tersten yazdırmaya çalışalım. Bunun tek yolu önce yazının sonuna gitmek sonra oradan başa doğru giderek
|
||
karakterleri yazdırmak olabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "istanbul";
|
||
int i;
|
||
|
||
for (i = 0; s[i] != '\0'; ++i)
|
||
;
|
||
|
||
for (--i; i >= 0; --i)
|
||
putchar(s[i]);
|
||
putchar('\n');
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bu tür durumlarda indis değişkenini işaretsiz tamsayı türünden almak istiyorsanız dikkatli olmalısınız. Örneğin aşağıdaki
|
||
döngüde i'nin unsigned int türden olduğunu varsayalım. Bu durumda i >= 0 koşulu her zaman sağlanacak ve dizi taşmasından
|
||
kaynaklanan bir tanımsız davranış oluşacaktır:
|
||
|
||
for (--i; i >= 0; --i)
|
||
putchar(s[i]);
|
||
|
||
unsigned bir nesnedeki 0 değerini -- operatörü ile azaltmaya çalışırsak -1 değeri o nesne ifade edilebilcek en büyük
|
||
tamsyaı değer haline gelmektedir. Pekiyi bu durumda yukarıdaki döngü nasıl oluşturulmalıdır? Çözüm için en basit
|
||
düzenleme koşulda sonek -- operatörü kullanmaktır. Örneğin:
|
||
|
||
while (i-- > 0)
|
||
putchar(s[i]);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
char bir dizinin içerisinedeki yazıyı ters çevirmek isteyelim. Biz daha önce int bir diziyi ters çevirmiştik. Aynı algoritmayı uygulayabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[100] = "ankara";
|
||
int n;
|
||
char temp;
|
||
|
||
for (n = 0; s[n] != '\0'; ++n)
|
||
;
|
||
|
||
for (int i = 0; i < n / 2; ++i) {
|
||
temp = s[i];
|
||
s[i] = s[n - 1 - i];
|
||
s[n - 1 - i] = temp;
|
||
}
|
||
|
||
for (int i = 0; s[i] != '\0'; ++i)
|
||
putchar(s[i]);
|
||
putchar('\n');
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
34. Ders 04/10/2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'nin prototipi <stdio.h> içerisinde bulunan standart puts isimli fonksiyonu bir yazıyı ekrana (stdout dosyasına) yazdırmak için kullanılmaktadır.
|
||
Fonksiyonun genel kullanımı şöyledir:
|
||
|
||
puts(<dizi_ismi>);
|
||
|
||
Aslında ileride görüleceği gibi puts fonksiyonu char türden bir adres almaktadır. Ancak biz şimdilik bu fonksiyonun yazının içinde bukunduğu char türden dizinin
|
||
ismini parametre olarak alacağını belirtelim. C'de programcının dışında başkaları tarafından yazılmış olan (standart fonksiyonlar da dahil) hiçbir fonksiyon
|
||
bir dizinin uzunluğunu bilemez. Dizinin uzunluğunu yalnızca onu açan programcı biliyor durumdadır.
|
||
|
||
puts fonksiyonu dizinin başından başlayarak null karakter görene kadar tüm karakterleri yan yana yazdırır. En sonunda imleci aşağı satırın başına geçirerek
|
||
orada bırakır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[100] = "ankara";
|
||
|
||
puts(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
puts fonksiyonu dizinin başından null karakter görene kadar karakterleri yazdırmaktadır. Eğer null karakter bir biçimde ezilmişse puts durmaz
|
||
ilk null karakter görene kadar dizinin elemanlarını karakter olarak yazdırır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[100];
|
||
|
||
s[0] = 'a';
|
||
s[1] = 'l';
|
||
s[2] = 'i';
|
||
s[3] = '\0';
|
||
|
||
puts(s);
|
||
|
||
s[3] = 'x';
|
||
puts(s); /* alix'ten sonra tuhaf karakterler çıkabilir */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yazılar tipik olarak char türden dizilerin içerisine yerleştirilirler. Ama bunun tersi doğru değildir. Yani char türden dizilere yazı yerleştirmek
|
||
zorunda değiliz. Pekala biz char türden bir diziyi az yer kaplayan bir tamsayı dizisi olarak kullanabiliriz. Bu durumda dizinin sonuna null karakter yerleştirmenin
|
||
bir anlamı yoktur. Örneğin:
|
||
|
||
char s[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||
|
||
Burada s dizisi bir byte yer kaplayan bir tamsayı dizisi gibi kullanılmak üzere oluşturulmuştur. Bu kullanımın null bir ilgisi yoktur. Biz char türden
|
||
dizilere yazı yerleştireceksek null karakteri yazının sonuna yerleştirmeliyiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = {1, 2, 3, 4, 5};
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
printf("%d ", s[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Null karakter ile '0' karakterinin ilgisi olmadığına dikkat ediniz. Null karakter gerçekten 0 değerine ilişkin karakterdir. Yani null karakterin sayısal değeri
|
||
0'dır. Ancak '0' karakterinin sayısal değeri ASCII tablosundan 48'dir. Daha önceden de belirttiğimiz gibi '\0' gösterimi ile 0 gösterimi arasında teknik
|
||
bir farklılık yoktur. Ancak karakter vurgusu yapmak için null karakteri '\0' biçiminde göstermek iyi bir tekniktir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char c;
|
||
|
||
c = '0';
|
||
|
||
printf("%d\n", c); /* 48 */
|
||
|
||
c = '\0';
|
||
printf("%d\n", c); /* 0 */
|
||
|
||
c = 0;
|
||
printf("%d\n", c); /* 0 */
|
||
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bellekte karakter diye bir bilginin olmadığına dikkat ediniz. Karakterler aslında birer sayı olarak bellek bulunurlar. Biz C'de bir karakteri tek tırnak
|
||
içerisine aldığımızda o karakterin ilgili karakter tablosundaki sıra numarısını belirtmiş oluruz. Yani örneğin ASCII tablosunun kullanıldığı bir C derleyicisinde
|
||
'a' ile 97 arasında bir farklılık yoktur. Bellekte her şeyin aslında ikilik sistemde sayılar biçiminde bulunduğuna onun nasıl yorumlanacağına programcının karar verdiğine
|
||
dikkat ediniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[10] = {97, 98, 99, 0};
|
||
|
||
puts(s); /* abc */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C standartlarında bir özelliğin "deprecated" olması "ileride kaldırabileceği, ancak şimdilik muhafaza edildiği" anlamına gelmektedir. Mademki deprecated
|
||
özellikler ileride kaldırılabilecek özelliklerdir. O halde programcıların deprecated özellikleri kullanmaması gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
gets isimli standart C fonksiyonu C'nin ilk günlerinden beri vardı. Ancak bir tasarım bozukluğundan dolayı C99'da deprecated yapıldı ve C11'de
|
||
standart fonksiyon listesindne çıkartıldı. Bugünkü derleyiclerde gets halen desteklenmektedir. Ancak bu fonksiyon kullanılırken derleme ya da link aşamasında
|
||
uyarı oluşabilmektedir. Her ne kadar gets fonksiyonu C11 ile C'den çıkartılmışsa da kursumuzda eğitim amaçlı nedenlerle bu fonksiyonu kullanacağız.
|
||
C11 gets yerine gets_s isimli yeni bir fonksiyonu kütüphaneye eklemiştir. Ancak maalesef bu fonksiyon da standartlarda "optional" yapılmıştır. Optional
|
||
özellik demek ilgili derleyicinin barındırp barındırmayacağı derleyiciyi yazanlara bağlı demektir. Gerçekten de gets_s fonksiyonu gcc derleyicilerinde henüz
|
||
bulunmamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
gets fonksiyonu klavyeden (stdin dosyasından) bir yazı okumak için kullanılmaktadır. tipik kullanımı şöyledir:
|
||
|
||
gets(dizi_ismi);
|
||
|
||
gets aslında char türdne bir adres almaktadır. Ancak ileride bu konu ele alınacaktır. gets fonksiyonu ENTER tuşuna basılana kadar girilen karakterleri
|
||
(yani onların sayısal karşılıklarını) diziye tek tek yerleştirir. Yazının sonuna null karakteri ekler ve işlemini sonlandırır. Örneğin:
|
||
|
||
char s[100];
|
||
|
||
gets(s);
|
||
|
||
gets fonksiyonun kusuru kullanıcı uzun yazı girerse diziyi taşırabilmesidir. Biz gets ile n eleman uzunluğundaki bir char diziye en fazla n - 1 karakterli bir
|
||
yazı girmeliyiz. Çünkü gets null karakteri de yazının sonuna eklemektedir. Null karakter de diznin içerisinde kalmak zorundadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[1024];
|
||
|
||
printf("Bir yazi giriniz:");
|
||
gets(s);
|
||
|
||
puts(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bir dizinin taşırılması "tanımsız davranış (undefined behavior)" oluşturmaktadır. Eğer biz gets fonksiyonu için küçük bir dizi açarsak ve klavyeden
|
||
çok karakter girersek dizi taşar ve programımız çökebilir. n elemanlı bir char diziye gets ile en fazla n - 1 karakterli bir yazı girebiliriz.
|
||
Aşağıdaki örnekte uzun bir yazı girerek sonucu gözleyniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[5]; /* dikkat dizi çok kğçük, taşabibilir! */
|
||
|
||
printf("Bir yazi giriniz:");
|
||
gets(s);
|
||
|
||
puts(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir char dizi içerisindeki karakterleri null karakter görene kadar yazdırmak için puts fonksiyonun yanı sıra printf fonksiyonu da kullanılabilir.
|
||
printf fonksiyonunda %s format karakterine char türdne bir dizi ismi (aslında bir adres) karşı getirilirse printf null karakter görene kadar
|
||
dizinin içerisindeki karakterleri yan yana ekrana (stdout dosyasına) yazar. Tabii printf imleci aşağı satıra otomatik geçirmemektedir. O halde:
|
||
|
||
puts(char_dizi_ismi);
|
||
|
||
işleminin eşdeğeri:
|
||
|
||
printf("%s\n", char_dizi_ismi);
|
||
|
||
biçimindedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[1024];
|
||
|
||
printf("Bir yazi giriniz:");
|
||
gets(s);
|
||
|
||
printf("%s\n", s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizi içerisindeki yazıyı başka bir dizye nasıl kopyalayabiliriz? İlk akla gelen yöntem null karakter görene kadar kopyalama yapmaktır. Tabii
|
||
null karakterin de hedef diziye kopyalanması gerekir.
|
||
|
||
char s[100] = "this is a test";
|
||
char d[100];
|
||
int i;
|
||
|
||
for (i = 0; s[i] != '\0'; ++i)
|
||
d[i] = s[i];
|
||
d[i] = '\0';
|
||
|
||
Aslında bu işlem daha kısa şöyle yapılabilir:
|
||
|
||
for (int i = 0; (d[i] = s[i]) != '\0'; ++i)
|
||
;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[100];
|
||
char d[100];
|
||
|
||
printf("Bir yazi giriniz:");
|
||
gets(s);
|
||
|
||
for (int i = 0; (d[i] = s[i]) != '\0'; ++i)
|
||
;
|
||
|
||
puts(s);
|
||
puts(d);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'nin en önemli konularından biri "göstericiler (pointers)" konusudur. C bir gösterici dilidir. Kurusumuzun bu
|
||
bölümünde göstericiler konusunu aytıntılı bir biçimde ele alacağız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Adres bilgilerinin yerleştirildiği nesnelere "gösterici (pointer)" denilmektedir. Bir adres bilgisi int bir nesneye, double bir nesneye yerleştirilemez.
|
||
Adresler iki bileşenli özel türlerdir. Elimizde bir adres bilgisi varsa biz onu ancak bir göstericiye atayabiliriz. Yani adres tutan nesnelere
|
||
gösterici denilmektedir. C'de bir gösterici bildiriminin genel biçimi şöyledir:
|
||
|
||
<tür> *<gösterici_ismi>;
|
||
|
||
Örneğin:
|
||
|
||
int *pi;
|
||
long *pl;
|
||
|
||
Burada '*' atomunun çarpmayla bir ilgisi yoktur. Buradaki '*' gösteriyi belirtmektedir. Atomlar arasında istenildiği kadar boşluk karakteri bırakılabileceğine
|
||
göre bu bildirimler örneğin aşağıdaki gibi de yazılabilir:
|
||
|
||
int
|
||
*
|
||
pi;
|
||
|
||
long* pl;
|
||
|
||
Ritchie/Kernighan yazım biçiminde * atomu gösterici ismine bitiştirilmektedir. Biz de bu yazım biçimi kullanacağız.
|
||
|
||
Bir göstericiye herhangi bir adres bilgisi atanamaz. Ancak tür bileşeni uygun olan bir adres bilgisi atanabilir. Örneğin:
|
||
|
||
int *pi;
|
||
|
||
Burada pi göstericisine biz ancak tür bileşeni int olan bir adres bilgisi atayabiliriz. Örneğin:
|
||
|
||
pi = (int *)0x1FC14; /* geçerli pi int türdne gösterici, ona int türden bir adres bilgisi atanmış */
|
||
|
||
Örneğin:
|
||
|
||
int *pi;
|
||
|
||
pi = (double *)0x1B12C0; /* geçersiz! pi'ye int türdne bir adres bilgisinin atanması gerekirdi. Halbuki double türden bir adres bilgisi atanmıştır */
|
||
|
||
Bir adres bilgisi gösterici olmayan bir nesneye de atanamaz. Örneğin:
|
||
|
||
int a;
|
||
|
||
a = (int *) 0x1FC90; /* geçersiz! adres bilgileri temel türlere atanamaz, göstericilere atanabilir */
|
||
|
||
Bir göstericiye bir tamsayı da atayamayız. Ancak aynı türden bir adres bilgisi atayabiliriz. Örneğin:
|
||
|
||
int *pi;
|
||
|
||
pi = 0x1FC10; /* geçersiz! int türden göstericiye adi bir int atanamaz, int türden adres bilgisinin atanması gerekir */
|
||
|
||
Yani özetle bir göstericiye aynı türden bir adres bilgisi atanabilir. Bir adres bilgisi de yalnızca aynı türden bir göstericiye atanabilir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir adres bilgisi yanı türden bir göstericiye atandığında göstericiye adresin yalnızca sayısal bileşeni yerleştirilir. Çünkü tür bileşeni
|
||
zaten bildirimde derleyici tarafından bilinmektedir. Örneğin:
|
||
|
||
int *pi;
|
||
|
||
pi = (int *) 0x1FC14;
|
||
|
||
Burada pi'nin içerisinde 1FC14 değeri bulunur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tek operandlı önek & operatörüne C'de "address of" operatör denilmektedir. Bu operatörün operandı bir nesne olmak zorundadır. Bu operatör ilgili nesnenin
|
||
bellek adresini elde eder. & operatörü ile elde edilen adresin tür bileşeni operandı olan nesnenin türüyle aynı olan türdendir. Sayısal bileşeni ise
|
||
nesnenin bellekteki doğrual adresidir. Tabii nesne bellekte bir byte'tan daha uzun yer kaplıyorsa onun en düşük anlamlı adresi doğrusal adresi olur.
|
||
|
||
Bir nesnenin adresini aldığımızda biz onu aynı türden bir göstericiye yerleştirebiliriz. Örneğin:
|
||
|
||
int a;
|
||
int *pi;
|
||
|
||
pi = &a; /* geçerli */
|
||
|
||
Burada &a ile elde edilen adres bilgisinin tür bileşeni int biçimdedir. O zaman bizim bu adresi int türden bir göstericiye atamamız gerekir. Tabii bu
|
||
atamadan sonra pi göstericisi aslında adresin sayısal bileşenini tutar durumda olur. Örneğin:
|
||
|
||
char a;
|
||
int *pi;
|
||
|
||
pi = &a; /* geçersiz! char türden bir adres bilgisi int türden göstericiye atanmış */
|
||
|
||
Örneğin:
|
||
|
||
int a;
|
||
int b;
|
||
|
||
a = &b; /* geçersiz! bir adres bilgisi int bir nesneye atanamaz! */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir göstericinin içerisinde bir adresin bulunması o göstericinin o adresi gösterdiği anlamına gelmektedir. Yani biz "falanca gösterici şu adresi gösteriyor"
|
||
dediğimizde anlaşılması gereken şey o göstericinin içerisinde o adresin bulunduğudur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de adres bilgileri sembolik olarak "tür *" biçiminde gösterilir. Örneğin:
|
||
|
||
int a;
|
||
|
||
&a ifadesinin türü "int *" biçiminde ifade edilir. "int *" demek int türden bir adres bilgisi demektir. Örneğin:
|
||
|
||
int a;
|
||
int *pi;
|
||
|
||
Burada a'nın türü int, pi'nin türü int * biçimindedir. Buradaki '*' adres anlamına gelmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
35. Ders 06/10/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de diziler onların bütün elemanlarından oluşan bileşik bir nesne gibi ele alınmaktadır. Örneğin:
|
||
|
||
char s[10];
|
||
|
||
Burada s dizisi 10 elemalı ve char türdendir. C'de bir dizinin ismi bir ifade içeerisinde kullanıldığında otomatik olarak derleyici tarafından
|
||
o dizinin başlangıç adresine dönüştürülür. Yani dizi isimleri C'de dizilerin başlangıç adreslerini belirtmektedir. Dizi isimleri ile belirtilen
|
||
adreslerin tür bileşeni dizi türü ile aynı olan türdendir. Sayısal bileşeni ise dizinin bellekteki başlangıç adresidir. Diziler elemanları ardışıl bulunduğuna
|
||
göre ve dizinin elemanları ilk eleman düşük adreste olacak biçimde yerleştirildiğine göre aslında bir dizinin ismi yani dizinin adresi aynı zamanda
|
||
dizinin ilk elemanının adresidir. Örneğin:
|
||
|
||
int a[10];
|
||
|
||
Burada a ifadesi tamamen &a[0] aynı anlamdadır. a adresi int türden bir adres belirtir. O halde C'de dizi isimleri aynı türden göstericlere atanabilir.
|
||
Örneğin:
|
||
|
||
int a[10];
|
||
int *pi;
|
||
|
||
pi = a; /* geçerli */
|
||
|
||
Örneğin:
|
||
|
||
char s[10];
|
||
int *pi;
|
||
|
||
pi = s; /* geçersiz! */
|
||
|
||
Burada göstericiye farklı türden bir adres bilgisi atanmıştır. Normal nesnelerin adreslerini & operatörüyle almaktayız. Ancak dizi isimleri zaten
|
||
adres belirtmektedir. Dolayısıyla dizi isimlerine & operatörünü uygulamamalıyız. (Aslında bu başka bir anlama gelmektedir.)
|
||
|
||
C'de dizi isimleri nesne belirtmemektedir. Biz bir dizi ismini kullandığımızda adeta derleyici o dizi ismini bir adres sabitine dönüştürmektedir.
|
||
Örneğin:
|
||
|
||
int a[3];
|
||
|
||
Burada a[0], a[1], a[2] birer nesne belirtir. Ancak a bir nesne belirtmez. Yani a için bir yer ayrılmamaktadır. a ifadesi tüm diziyi temsil etmektedir.
|
||
|
||
a = 10; /* geçersiz! a bir nesne belirtmiyor */
|
||
|
||
Örneğin:
|
||
|
||
char s[3];
|
||
char *pc;
|
||
|
||
Burada s dizisinin bellekte 1B12C0 adresinden itibaren yerleştirildiğini varsayalım:
|
||
|
||
pc = s;
|
||
|
||
Aslında bu işlemde derleyici aşağıdaki gibi bir kod üretmektedir:
|
||
|
||
pc = (char *)0x1B12C0;
|
||
|
||
|
||
char *pc;
|
||
char *p2;
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de önemli bir adres operatörü de "* (indirection)" operatördür. Bu operatörün çarpma işlemini yapan * operatörü ile bir ilgisi yoktur. Tamamen
|
||
farklı bir operatördür. * operatörü tek operandlı önek bir adres operatörüdür. * operatörünün operandı bir adres bilgisi olmak zorundadır. * operatörü
|
||
openadı olan adresteki nesneye erişimi sağlar. * operatörü ile erişilen nesnenin türü operand olarak kullanılan nesnenin türü ile aynı türdendir.
|
||
Örneğin:
|
||
|
||
int a = 10;
|
||
int *pi;
|
||
|
||
pi = &a;
|
||
|
||
Burada pi'nin içerisinde a nesnesinin adresi vardır. Şimdi biz *pi dediğimizde pi adresindeki int nesneye erişmiş oluruz. Yani *pi ile a tamamen
|
||
aynı nesneyi belirtmektedir. *pi ifadesi burada int türdendir. Çünkü pi adresi int türden bir adres bilgisidir. Böylece biz bir nesnenin adresini
|
||
alıp onu bir göstericiye yerleştirdikten sonra o göstericiyi * operatörü ile kullandığımızda adresini aldığımız nesneye erişmiş oluruz. Örneğ.n:
|
||
|
||
int a;
|
||
int *pi;
|
||
|
||
pi = &a;
|
||
|
||
Burada artık *pi ile a aynı nesnelerdir. Bu nesneye a ifadesi ile erişmekle *pi ifadesi ile erişmek arasında hiçbir farklılık yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10;
|
||
int *pi;
|
||
|
||
pi = &a;
|
||
|
||
printf("%d\n", *pi); /* 10 */
|
||
*pi = 20;
|
||
printf("%d\n", a); /* 20 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizinin ismi o dizinin bellekteki adresini (yani ilk elemanının adresini) belirtiyordu. O halde biz dizinin ismini aynı türden bir gösteriye atayıp
|
||
o göstericiyi * operatörü ile kullanırsak dizinin ilk elemanına erişmiş oluruz. Örneğin:
|
||
|
||
int a[] = {10, 20, 30};
|
||
int *pi;
|
||
|
||
pi = a;
|
||
|
||
Burada *pi aslında a[0] nesnesidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[] = {10, 20, 30};
|
||
int *pi;
|
||
|
||
pi = a;
|
||
|
||
printf("%d\n", *pi); /* 10 */
|
||
|
||
*pi = 100;
|
||
|
||
printf("%d\n", a[0]); /* 100 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
* adres operatörüne İngilizce "indirection" operatörü denilmektedir. Biz bu operatöre Türkçe "içerik operatörü de diyeceğiz". İçerik operatörünün
|
||
operandının bir adres bilgisi olması gerekir. Örneğin:
|
||
|
||
int a = 0x1FC12D;
|
||
|
||
*a = 10; /* geçersiz! * operatörünün operandı adi bir int, bir adres bilgisi değil */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bir adres bilgisi ile tamsayı türlerine ilişkin bir bilgi toplanabilir. Bir adres bilgisinden tamsayı türlerine ilişkin bir bilgi çıkartılabilir.
|
||
Yani p bir adres bilgisi i de bir tamsayı belirtmek üzere p + i ya da i + p işlemi geçerlidir. p - i işlemi geçerlidir, ancak i - p işlemi geçerli değildir.
|
||
Adres bilgileriyle tamsayı türlerine ilişkin bilgiler çarpılıp bölünemezler. Bir adres bilgisi bir tamsayı ile toplandığında ya da çıkartıldığında elde edilen ürün
|
||
aynı türden bir adres bilgisi olur. Bir adres bilgisi 1 artırılıdğında adresin sayısal bileşeni adresin türünün uzunluğu kadar artmektedir. Benzer biçimde bir adres
|
||
bilgisinden 1 çıkartıldığında adresin sayısal bileşeni adresinin türünün uzunluğu kadar eksiltilir. Örneğin pi int türden bir adres bilgisini temsil etsin.
|
||
İlgili sistemde de int türünün 4 byte olduğunu varsayalım. pi + 1 ifadesi ile edilen adresin sayısal bileşeni pi'nin sayısal bileşeninden 4 fazla olacaktır.
|
||
Örneğin pc char türden bir adres belirtiyor olsun. pc + 1 işleminden elde edilen adresin sayısal bileşeni pc adresinin sayısal bileşeninden 1 fazla olacaktır.
|
||
Çünkü char 1 byte uzunluktadır. Benzer biçimde pi bir gösterici ise ++pi işlemi sonucunda pi'nin içerisinde adresin sayısal bileşeni 4 artacaktır. Yani pi bir
|
||
sonraki int nesneyi gösterir duruma gelecektir. Örneğin:
|
||
|
||
int a[] = {10, 20, 30};
|
||
int *pi;
|
||
|
||
pi = a;
|
||
|
||
Burada pi dizinin ilk elemanını göstermektedir. Biz *pi'yi yazdırırsak 10 görürüz. pi'yi 1 artıralım:
|
||
|
||
++pi;
|
||
|
||
Şimdi pi'nin içerisindeki adresin sayısal bileşeni 4 artmış olacaktır. Şimdi *pi'yi yazdırırsak 20'yi göreceğiz. Çünkü dizi elemanları ardışıl olmak zorunddır.
|
||
Yani bizim pi'yi 1 artırdığımızda dizinin sonraki elemanına erişebilmemiz için dizi elemanları arasında hiç boşluk olmayacağını garanti etmiş olmamız gerekir. Örneğin:
|
||
|
||
int a = 10, b = 20, c = 30;
|
||
int *pi;
|
||
|
||
pi = &a;
|
||
|
||
Burada a, b ve c nesnelerinin bellekte peşi sıra dizilmelerinin hiçbir garantisi yoktur. Dolayısıyla burada biz pi'yi artırarak b ve c'ye erişemeyiz.
|
||
Ancak dizilerde bu ardışıllık garanti edilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[] = {10, 20, 30};
|
||
int *pi;
|
||
|
||
pi = a;
|
||
|
||
printf("%d\n", *pi); /* 10 */
|
||
++pi;
|
||
printf("%d\n", *pi); /* 20 */
|
||
++pi;
|
||
printf("%d\n", *pi); /* 30 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yazıyı char türden bir dizinin içerisine yerleştirip dizinin başlangıç adresini de char türden bir göstericiya atayabiliriz. Bu durumda göstericiyi
|
||
artıra artıra yazının karakterlerine erişebiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char *pc;
|
||
|
||
pc = s;
|
||
|
||
putchar(*pc); /* a */
|
||
++pc;
|
||
putchar(*pc); /* n */
|
||
++pc;
|
||
putchar(*pc); /* k */
|
||
++pc;
|
||
putchar(*pc); /* a */
|
||
++pc;
|
||
putchar(*pc); /* r */
|
||
++pc;
|
||
putchar(*pc); /* a */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii yukarıdaki örneği bir döngü içerisinde de yapabilirdik.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char *pc;
|
||
|
||
pc = s;
|
||
while (*pc != '\0') {
|
||
putchar(*pc);
|
||
++pc;
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de adres ile ilgili işlem yapan 4 operatör vardır: &, *, [] ve -> operatörleri. Biz burada biraz daha ayrıntılı olarak bu operatörleri inceleyeceğiz.
|
||
Ancak -> operatörü "yapılar (structures)" konusu ile ilgili olduğu için onu yapılar konusunda göreceğiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
& operatörü tek operandlı önek (unary prefix) bir adres operatörüdür. Bu operatör operandı olan nesnenin bellek adresini verir. Daha önceden de belirtildiği gibi
|
||
& operatörü ile elde edilen adresin tür bileşeni operand olan nesnenin türü ile aynı olan türden, sayısal bileşeni ise operand olan nesnenin bellekteki doğrusal adresinden
|
||
oluşmaktadır. Biz bir nesnenin adresini aldığımızda onu aynı türden bir göstericiye yerleştirebiliriz. Örneğin:
|
||
|
||
int a;
|
||
int *pi;
|
||
char *pc;
|
||
|
||
pi = &a;
|
||
pc = &a; /* geçersiz! */
|
||
|
||
& operatörü öncelik tablosunda tablonun ikinci düzeyinde sağdan sola grupta bulunmaktadır:
|
||
|
||
|
||
() Soldan-Sağa
|
||
+ - ++ -- ! & (tür) Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
Örneğin a bir nesne belirtmek üzere &a + 1 gibi bir ifadede önce a'nın adresi alınır, sonra bu adrese 1 toplanır. & operatörünün operandının bir nesne
|
||
belirtmesi gerekir. Çünkü yalnızca nesnelerin adresleri vardır. Örneğin &10 ifadesi geçersizdir. Çünkü operand olan 10 bir nesne belirtmez. Örneğin &(a + 1)
|
||
Burada a + 1 ifadesi bir nesne belirtmez yani sol taraf değeri (lvalue) değildir. Bu nedenle biz a'nın adresini alabiliriz ancak a + 1'in adresini alamayız.
|
||
Dizi isimleri zaten adres belirtmektedir. Dizi isimlerine yeniden & operatörü uygulanmaz. (Ancak C'de aslında dizi isimlerinin adresleri alınabilir. Ancak bu durum
|
||
tamamen farklı bir anlam ifade etmektedir. İleride ele alınacaktır.)
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
* (indirection) operatörü tek operandlı önek bir adres operatörüdür. Bu operatörün operandı bir adres bilgisi olmak zorundadır. Operatör operandı olan
|
||
adresteki nesneye erişmekte kullanılır. * operatörü ile erişilen nesnenin türü operandı olan adresin türüyle aynı türdendir. Yani örneğin *p işleminde
|
||
elde edilen nesne p adresi hangi türdense o türden olacaktır. * operatörü de öncelik tablosunun ikinci düzeyinde sağdan sola grupta bulunmaktadır.
|
||
|
||
() Soldan-Sağa
|
||
+ - ++ -- ! & * Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
* operatörünün operandı bir adres bilgisi olmak zorunadır. Örneğin göstericiler, dizi isimleri birer adres belirtmektedir:
|
||
|
||
int a[] = {10, 20, 30}
|
||
|
||
Bırada *a bu dizinin ilk elemanını belirtir.
|
||
|
||
a bir nesne belirtmek üzere *&a işleminde öce & operatörü sonra * operatörü yapılacaktır. Çünkü bu iki operatör sağdan sola aynı öncelik grubundadır.
|
||
O halde *&a ile a arasında hiçbir farklılık yoktur. Yani biz bir nesnenin adresini alıp ona nesneye erişirsek aynı nesneyi elde ederiz. Örneğin:
|
||
|
||
int a = 0x1FC20D;
|
||
|
||
printf("%d\n", *a); /* geçersiz! * operatörünün operandı adres bilgisi değil adi bir int */
|
||
|
||
Tabii adres sabitleri de adres belirttiğine göre onlara da * operatörü uygulanabilir. Örneğin *(int *)0x1FCD0 burada bellekte 1FCD0 adresinden başlayan
|
||
4 byte (int türünün 4 byte olduğunu varsayıyoruz) int olarak değerlendirilip oryaa erişilecektir. Tabii aslında bizim bellekte rastegele bölgelere bu yolla
|
||
erişmememiz gerekir. Bu konu ileride "gösteri hataları" başlığı ile ele alınacaktır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10;
|
||
int b[] = {10, 20, 30};
|
||
|
||
printf("%d\n", *&a); /* 10 */
|
||
|
||
*&a = 20;
|
||
|
||
printf("%d\n", a); /* 20 */
|
||
|
||
printf("%d\n", *b); /* 10 */
|
||
*b = 100;
|
||
printf("%d\n", b[0]); /* 100 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dizi elemanlarına erişmekte kullandığımız [] aslında bir adres operatörüdür. Köşeli parantez operatörü tek operandlı sonek (unary postfix) bir operatördür.
|
||
p[n] ifadesi tamamen *(p + n) ile eşdeğerdir. Yani p[n] "p adresinden n ilerinin içeriği" anlamına gelmektedir. Tabii burada p adresinden n ileri demekle
|
||
p adresinden n byte ileriyi kastetmiyoruz. p adresinden n * p'nin türünün uzunluğu kadar byte ilerinin içeriğini kastediyoruz. [] operatöründe köşeli parantez
|
||
içerisindeki ifadenin tamsayı türlerine ilişkin olması gerekir. [] opeatörü öncelik tablosunun en yukarısında soldan öncelikli grupta bulunmaktadır:
|
||
|
||
() [] Soldan-Sağa
|
||
+ - ++ -- ! & * (tür) Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
[] operatörünün operandı bir adres bilgisi olmak zorundadır. Yani operand örneğin bir gösterici olabilir, bir dizi ismi olabilir. Biz daha önce []
|
||
operatörünü dizi elemanlarına erişmekte kullanmıştık. Örneğin a[i] ifadesini a dizisinin i'inci indisli elemanına erişmek için kullanmıştık. a dizi ismi
|
||
dizinin başlangıç adresi anlamına geldiğine göre a[i] ifadesi tamamen *(a + i) ile eşdeğerdir. Tabii [] operatörünü biz daha önce hep dizi ismiyle kullanıştık.
|
||
Aslında bu operatörün operandı herhangi bir adres bilgisi olabilir. Örneğin [] operatörünü bir gösterici ile kullanabiliriz:
|
||
|
||
int a[] = {10, 20, 30, 40, 50};
|
||
int *pi;
|
||
|
||
pi = a;
|
||
|
||
Örneğin burada a[3] ile pi[3] arasında hiçbir farklılık yoktur.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[] = {10, 20, 30, 40, 50};
|
||
int *pi;
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
printf("%d %d\n", a[i], *(a + i));
|
||
|
||
pi = a;
|
||
|
||
printf("%d\n", pi[3]); /* 40 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii aslında [] operatöründe köşeli parantezler içerisindeki ifade negatif olabilir. Örneğin pi[-2] gibi bir ifade tamamen normaldir. Bu işlem *(pi - 2)
|
||
anlamına gelmektedir. Yani biz burada pi'nin belirttiği adresten iki önceki elemana erişmiş oluruz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[] = {10, 20, 30, 40, 50};
|
||
int *pi;
|
||
|
||
pi = a + 3;
|
||
printf("%d\n", *pi); /* 40 */
|
||
|
||
printf("%d\n", pi[-2]); /* 20 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
p bir adres belirtmek üzere p[0] ile *(p + 0) ve *p tamamen aynı anlamdadır. Yani örneğin biz a isimli bir dizinin ilk elemanına a[0] ifadesi ile de *a
|
||
ifadesi ile de erişebiliriz.
|
||
|
||
p bir adres belirtmek üzere *(p + n) ile *p + n tamaen farklı anlamlara gelmektedir. *(p + n) ifadesinde önce parantez içi yapılacak ve p adresinden n ilerideki
|
||
adres elde edilecektir. Sonra * operatörü ile bu adresin içeriği elde edilecektir. Halbuki *p + n ifadesinde önce *p ile p adresindeki nesneye erişilecek
|
||
o nesnenin değeri n ile toplanacaktı.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de aslında [] operatörünün operand'ları yer değiştirebilmektedir. Yani p[n] ifadesi aslında n[p] biçiminde de yazılabilmektedir. Bu çok az bilinen
|
||
bir özelliktir. Zaten programcılar tarafından hiç kullanılmaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[] = {10, 20, 30, 40, 50};
|
||
|
||
printf("%d\n", a[2]); /* 30 */
|
||
printf("%d\n", 2[a]); /* 30 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
p bir adres belirtmek üzere örneğin ++*p gibi bir ifadede biz asılında *p'yi bir artırmış oluruz. Yani bu ifade *p = *p + 1 ile aynı anlamdadır.
|
||
Ancak *++p ifadesinde biz önce p göstericisinin içerisinde adresi bir artırıp (bir byte değil) sonra artırılmış adresteki nesneye erişiriz. p bir adres
|
||
belirtmek üzere &*p ifadesinde önce p adresindeki nesneye erişilip sonra onun adresi alınmıştır. Bu da tabii p adresiyle aynıdır. a bir nesne belirtmek üzere
|
||
*&a ifadesi de daha önce belirttiğimiz gibi a ile aynı anlamdadır.
|
||
|
||
[] operatörünün & operatöründen daha öncelikli olduğuna dikkat ediniz. Örneğin &a[n] ifadesi a adresinden n ilerinin içeriğinin adresi anlamına gelmektedir. Bu ifade
|
||
&*(a + n) ifadesi ile eşdeğer olduğuna göre aslında a + n ile de eşdeğerdir. Yani a adresinden n ilerinin içeriğinin adresi aslında a adresinden n ilerinin adresi aynı anlamdadır.
|
||
Örneğin:
|
||
|
||
int a = 10;
|
||
|
||
&a[0] = 20; /* geçersiz */
|
||
|
||
Buarad [] operatörü önceliklidir. Dolayısıyla [] operatörünün operandı adres bilgisi olmadığı için ifade geçersizdir. İfadeyi şöyle düzeltelim:
|
||
|
||
(&a)[0] = 20; /* geçerli */
|
||
|
||
Bu ifade geçerlidir. Burada a'ya 20 atanmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
36. Ders 11/10/2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki gibi bir gösterici bildirimi olsun:
|
||
|
||
int *pi;
|
||
|
||
Bu bildirimden iki şey anlaşılmaktadır. Birincisi pi nesnesi int * türündendir. Burada "int *" int türden adres bilgisi anlamına gelir. (pi'yi
|
||
parmağınızla kapatıp sola bakın). İkincisi *pi yani pi'nin gösterdiği yer int türdendir. (*pi'yi parmağınızla kapatıp sola bakın).
|
||
|
||
T türünden adres türü C'de T * biçiminde temsil edilmektedir. T1 türünden T2 türüne otomatik dönüştürme olması T1 * türünden T2 * türüne otomatik dönüştürme olacağı
|
||
anlamına gelmez. Örneğin int türünden double türüne otomatik dönüştürme vardır. Ancak int * türünden double * türüne otomatik dönüştürme
|
||
yoktur. Bir adres bilgisini ancak aynı türden bir göstericiye atayabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir göstericiye ilkdeğer verebiliriz. Tabii verilen ilkdeğerin göstyerici ile aynı türden bir adres bilgisi olması gerekir. Örneğin:
|
||
|
||
int a;
|
||
int *pi = &a; /* geçerli */
|
||
|
||
Tabii burada verilen ilkdeğer pi'nin içerisine yerleştirilmektedir. *pi'ye yerleştirilmemektedir. Zaten buradaki * bir operatör görevinde
|
||
değildir dekleratörün bir parçasıdır. Örneğin:
|
||
|
||
int a[] = {10, 20, 30, 40, 50};
|
||
int *pi = a; /* geçerli */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonun parametre değişkeni bir gösterici olabilir. Bu durumda fonksiyon aynı türden bir adres bilgisi ile çağrılmalıdır. Örneğin:
|
||
|
||
void foo(int *pi)
|
||
{
|
||
/* ... */
|
||
}
|
||
...
|
||
int a;
|
||
foo(&a); /* geçerli */
|
||
foo(a); /* geçersiz */
|
||
|
||
Bir fonksiyonun parametre değişkeni bir gösterici ise biz de o fonksiyonu aynı türden bir nesnenin adresi ile çağırmışsak fonksiyonun içerisinde
|
||
* operatörü kullanıldığında biz aslında adresini aldığımız nesneye erişiriz. İşte bir fonksiyonun başka bir fonksiyonun yerel değişkenini değiştirebilmesi
|
||
için onun adresiniş alması gerekir.
|
||
|
||
Bir fonksiyonu bir değrle çağırmaya İngilizce "call by value", bir adresle açğırmaya "calal by reference" denilmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(int *pi)
|
||
{
|
||
*pi = 20;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10;
|
||
|
||
printf("%d\n", a); /* 10 */
|
||
|
||
foo(&a);
|
||
|
||
printf("%d\n", a); /* 20 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İki değişken içerisindeki değeri yer değiştiren swap isimli bir fonksiyon yazmak isteyelim. Bu fonksiyonu aşağıdaki gibi yazamayız:
|
||
|
||
void swap(int x, int y)
|
||
{
|
||
int temp = x;
|
||
x = y;
|
||
y = temp;
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void swap(int x, int y)
|
||
{
|
||
int temp = x;
|
||
x = y;
|
||
y = temp;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10, b = 20;
|
||
|
||
printf("a = %d b = %d\n", a, b);
|
||
|
||
swap(a, b);
|
||
|
||
printf("a = %d b = %d\n", a, b);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bu fonksiyonu yazabilmek için nesnelerin adreslerini parametre olarak almak gerekir. Örneğin:
|
||
|
||
void swap(int *x, int *y)
|
||
{
|
||
int temp = *x;
|
||
*x = *y;
|
||
*y = temp;
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void swap(int *x, int *y)
|
||
{
|
||
int temp = *x;
|
||
*x = *y;
|
||
*y = temp;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10, b = 20;
|
||
|
||
printf("a = %d b = %d\n", a, b);
|
||
|
||
swap(&a, &b);
|
||
|
||
printf("a = %d b = %d\n", a, b);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Şimdi artık scanf fonksiyonun neden neden nesnenin adresini aldığını anlayabiliriz. Eğer scanf nesnenin adresini almasaydı o nesnenin içerisine
|
||
bir şey yerleştiremezdi. Bir fonksiyonun bizim yerel değişkenimize bir şey yerleştirebilmesi için bizim değişkenimizin adresini
|
||
alması gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &a); /* scanf a'nın adresini alarak oraya değeri yerleştirmektedir */
|
||
|
||
printf("%d\n", a * a);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bir dizinin fonksiyona parametre yoluyla aktarılması tipik olarak iki parametre ile yapılmaktadır. Fonlsiyonun parametrelerinden biri bir gösterici
|
||
olur. Bu gösterici dizinin başlangıç adresini alır. Diğer parametre de int, unsigned int gibi tamsayı türlerine ilişkin bir türden olur. Bu parametre de
|
||
dizinin uzunlupunu alır. Böylece fonksiyon dizinin başlangıç adresini ve uzunluğunu aldığında o göstericiyi artırarak dizinin elemanlarının hepsine
|
||
erişebilir. Tabii bu biçiminde aktarımın mümkün olmasının asıl nedeni dizi elemanlarının ardışıllığıdır. Fonksiyona dizinin uzunluğunun da geçirilmesinin
|
||
nedeni fonksiyonun dizinin sonunu tespit eddebilmesi içindir.
|
||
|
||
Aşağıdaki int bir dizinin elemanlarını yazdıran disp isimli fonksiyon örnek olarak verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void disp(int *pi, int size)
|
||
{
|
||
for (int i = 0; i < size; ++i) {
|
||
printf("%d ", *pi);
|
||
++pi;
|
||
}
|
||
printf("\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
|
||
int b[5] = {100, 200, 300, 400, 500};
|
||
|
||
disp(a, 10);
|
||
disp(b, 5);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki örnekte dizi elemanlarına [] operatörü ile de erişebiliriz. Aslında bu tür fonksiyonlarda * yerine daha çok [] operatörü tercih edilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void disp(int *pi, int size)
|
||
{
|
||
for (int i = 0; i < size; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
|
||
int b[5] = {100, 200, 300, 400, 500};
|
||
|
||
disp(a, 10);
|
||
disp(b, 5);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizinin uzunluğunun int bir tür ile ifade edilmesi bazı sistemlerde yetersiz kalabilir. Örneğin bir sistemin bellek büyüklüğü int sınırlarını
|
||
aşıyor olabilir. Bu durumda çok büyük dizilerin uzunluklarını int türü ile ifade edemeyiz. Bu tür durumlarda unsigned int, long, unsigned long gibi türler
|
||
denenebilir. Tabii bir bir sistemde açılabilecek maksimum dizi uzunluğu o sistemde bellek büyüklüğü ile ilgilidir. Bunu da ancak derleyicileri yazanlar
|
||
biliyor olabilirler. İşte C'de ilgili sistemdeki bellek büyüklüğünü etkin bir biçimde temsil edebilmek için size_t isimli bir tür ismi düşünülmüştür.
|
||
size_t aslında bir tür değildir. Başka bir türün alternatif bir ismidir. Bu biçimdeki alternatif isimler typedef bildirimi ile oluşturulurlar.
|
||
Biz kursumuzda ileride typedef bildirimlerini göreceğiz. size_t türünün typedef bildirimleri <stdio.h>, <stdlib.h>, <stddef.h> , <string.h> gibi
|
||
dosyalarda yapılmış durumdadır. Yani bu tür ismini kullanabilmemeiz için bu dosyalardan birini include etmiş olmamız gerekir. size_t bir anahtar sözcük değildir.
|
||
Bir değişken olan sembolik bir isimdir. C standartlarına göre size_t işaretsiz bir tamsayı türü olmak üzere derleyicileri yazanlar tarafından
|
||
typedef edilmiş bir tür olmak zorundadır. Aslında programcının size_t türünün hangi tür olarak belirlediğini programcının bilmesine gerek yoktur.
|
||
|
||
İşte C'de dizi uzunlukları da genellikle programcılar tarafından size_t türü ile temesil edilmektedir. Biz de kursumuzda her ne kadar henüz typedef
|
||
işlemlerini görmemiş olsak da bu size_t türünü kullanacağız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void disp(int *pi, size_t size)
|
||
{
|
||
for (size_t i = 0; i < size; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
|
||
int b[5] = {100, 200, 300, 400, 500};
|
||
|
||
disp(a, 10);
|
||
disp(b, 5);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıda int bir dizinin en büyük elemanını bulan bir fonksiyon örneği verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int getmax(int *pi, size_t size)
|
||
{
|
||
int max = pi[0];
|
||
|
||
for (size_t i = 1; i < size; ++i)
|
||
if (pi[i] > max)
|
||
max = pi[i];
|
||
|
||
return max;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {45, 23, 11, 67, 21, 7, 32, 76, 22, 47};
|
||
int max;
|
||
|
||
max = getmax(a, 10);
|
||
printf("%d\n", max);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıda double bir dizinin ortalamasına geri dönen mean isimli bir fonksiyon örneği verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
double mean(double *pd, size_t size)
|
||
{
|
||
double total;
|
||
|
||
total = 0;
|
||
for (size_t i = 0; i < size; ++i)
|
||
total += pd[i];
|
||
|
||
return total / size;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
double a[5] = {1, 2, 3, 4, 5};
|
||
double result;
|
||
|
||
result = mean(a, 5);
|
||
printf("%f\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir göstericiye aynı türden bir adres bilgisinin atanabildiğini anımsayınız. Bu durumda int bir dizinin en büyük elemanına geri dönen aşağıdaki
|
||
gibi bir fonksiyon olsun:
|
||
|
||
int getmax(int *pi, size_t size);
|
||
|
||
Burada bu fonksiyon int bir dizinin en büyük elemanını bulabilir, long, double gibi türşere ilişkin dizilerin en büyük elemanlarını bulamaz.
|
||
Çünkü örneğin double bir türden dizinin adresi double türden adres belirtir. Halbuki fonksiyonun parametresi int türden bir göstericidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki int türden bir diziyi bubble sort algoritmasıyla sıraya dizen bir fonksiyon örneği verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void bsort(int *pi, size_t size);
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {2, 56, 11, 1, 58, 23, 32, 43, 67, 15};
|
||
|
||
bsort(a, 10);
|
||
|
||
for (size_t i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
void bsort(int *pi, size_t size)
|
||
{
|
||
int temp;
|
||
int flag;
|
||
size_t i;
|
||
|
||
i = 0;
|
||
do {
|
||
flag = 0;
|
||
for (size_t k = 0; k < size - 1 - i; ++k) {
|
||
if (pi[k] > pi[k + 1]) {
|
||
flag = 1;
|
||
temp = pi[k];
|
||
pi[k] = pi[k + 1];
|
||
pi[k + 1] = temp;
|
||
}
|
||
}
|
||
++i;
|
||
} while (flag == 1);
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıda int bir diziyi ters çeviren bir fonksiyon örneği verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void reverse(int *pi, size_t size);
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {2, 56, 11, 1, 58, 23, 32, 43, 67, 15};
|
||
|
||
for (size_t i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
reverse(a, 10);
|
||
|
||
for (size_t i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
void reverse(int *pi, size_t size)
|
||
{
|
||
for (size_t i = 0; i < size / 2; ++i) {
|
||
int temp = pi[i];
|
||
pi[i] = pi[size - 1 - i];
|
||
pi[size - 1 - i] = temp;
|
||
}
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki örneği yazarken iki int değeri yer değiştiren swap fonksiyonundan da faydalanabilirdik.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void reverse(int *pi, size_t size);
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {2, 56, 11, 1, 58, 23, 32, 43, 67, 15};
|
||
|
||
for (size_t i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
reverse(a, 10);
|
||
|
||
for (size_t i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
void swap(int *pi1, int *pi2)
|
||
{
|
||
int temp = *pi1;
|
||
*pi1 = *pi2;
|
||
*pi2 = temp;
|
||
}
|
||
|
||
void reverse(int *pi, size_t size)
|
||
{
|
||
for (size_t i = 0; i < size / 2; ++i)
|
||
swap(&pi[i], &pi[size - 1 - i]);
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
[] operatörünün * ve & operatörlerinden daha öncelikli olduğunu anımsayınız. Bu durumda p bir adres belirtmek üzere &p[n] ile p + n aynı anlamdadır.
|
||
Yani p adresinden n ilerinin içeriğinin adresi aslında p adresindne n ilerinin adresidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yazıların fonksiyonlara parametre yoluyla aktarılması içimn tipik olarak fonksiyonun parametre değişkeni char tüdrden bir gösterici olur. Fonksiyon da
|
||
yazının başlangıç adresiyle çağrılır. Yazının uzunluğunun fonksiyona geçirilmesine gerek yoktur. Çünkü yazının sonunda zaten null karakter vardır.
|
||
Fonksiyon da null karakter görene kadar yazının tüm karakterlerini elde edebilir.
|
||
|
||
Örneğin aslında puts fonksiyonun prototipi şöyledir:
|
||
|
||
void puts(char *str);
|
||
|
||
Fonksiyon yazının başlangıç adresini alır null karakter görene kadar tüm karakterleri yan yana yazdırır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void myputs(char *str)
|
||
{
|
||
while (*str != '\0') {
|
||
putchar(*str);
|
||
++str;
|
||
}
|
||
putchar('\n');
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
|
||
myputs(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
37. Ders 13/10/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
str bir yazıyı gösteren bir gösterici olmak üzere null karakter görene kadar ilerleyen döngü iki biçimde oluşturulabilir:
|
||
|
||
1) Göstericiyi artırarak
|
||
|
||
while (*str != '\0') {
|
||
/* ... */
|
||
++str;
|
||
}
|
||
|
||
2) [] operatör ile
|
||
|
||
for (size_t i = 0; str[i] != '\0'; ++i) {
|
||
/* ... */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void myputs(char *str);
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
|
||
myputs(s);
|
||
myputs(s + 2);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void myputs(char *str)
|
||
{
|
||
for (size_t i = 0; str[i] != '\0'; ++i)
|
||
putchar(str[i]);
|
||
putchar('\n');
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Şimdi bir yazıyı tersten yazdıran putsrev isimli bir fonksiyon yazalım. Biz yazıda önce null karakter görene kadar ilerleriz. Sonra geri geri giderek
|
||
karakterleri yazdırırız. Ancak kullanacağımız indisin türü konusunda dikkat ediniz. size_t türü her ne kadar dizi uzunlukları, indeksleri için
|
||
uygun bir türse de C standartlarına göre size_t işaretsiz bir tamsayı türü olarak typedef edilmektedir. İşaretsiz bir tamsayı türünden bir nesnenin içerisinde
|
||
0 varsa biz bu değerden 1 çıkartırsak o türün en büyük pozitif tamsayı değerini elde ederiz.
|
||
|
||
Aşağıdaki örneği int yerine size_t kullanarak deneyiniz ve problemi belirlemeye çalışınız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void putsrev(char *str);
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
|
||
putsrev(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void putsrev(char *str)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; str[i] != '\0'; ++i)
|
||
;
|
||
for (--i; i >= 0; --i)
|
||
putchar(str[i]);
|
||
|
||
putchar('\n');
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki örnekte i değişkeni int değil de size_t türünden yapılırsa oluşacak sorun aşağıdaki gibi giderilebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void putsrev(char *str);
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
|
||
putsrev(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void putsrev(char *str)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; str[i] != '\0'; ++i)
|
||
;
|
||
if (i > 0) {
|
||
for (--i; i > 0; --i)
|
||
putchar(str[i]);
|
||
|
||
putchar(str[i]);
|
||
}
|
||
putchar('\n');
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İşaretsiz bir tamsayı türü ile işaretli bir tamsayı türünü iki operandlı bir operatörler (karşılaştırma operatörleri dahil) işleme soktuğumuzda
|
||
dönüştürmenin işaretsiz türe doğru yapılacağını belirtmiştik. O halde yukarıdaki problem aşağıdaki gibi de çözülebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void putsrev(char *str);
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
|
||
putsrev(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void putsrev(char *str)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; str[i] != '\0'; ++i)
|
||
;
|
||
|
||
for (--i; i != -1; --i)
|
||
putchar(str[i]);
|
||
|
||
putchar('\n');
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki problem sonek eksiltim ile de çözülebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
#include <stdio.h>
|
||
|
||
void putsrev(char *str);
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "";
|
||
|
||
putsrev(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void putsrev(char *str)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; str[i] != '\0'; ++i)
|
||
;
|
||
|
||
while (i-- > 0)
|
||
putchar(str[i]);
|
||
|
||
putchar('\n');
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonun geri dönüş değerinin türü yerine T bir tür belirtmek üzere T * kullanılırsa bu durum fonksiyonun bir adresle geri döndüğü anlamına
|
||
gelmektedir. Örneğin:
|
||
|
||
int *foo(void)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Burada foo fonksiyonu int türden bir adres bilgisi ile geri dönmektedir. Ritchi/Kernighan yazım stilinde * atomu fonksiyon ismine bitiştirilmektedir.
|
||
Ancak bazı programcılar bu tür durumlarda * atomunu tür ile bitiştirirler.
|
||
|
||
Örneğin:
|
||
|
||
char *bar(void)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Burada bar fonksiyonun geri dönüş değeri char değildir. char türden bir adres bilgisidir. Tabii böyle fonksiyonları çağırdıktan sonra onların geri
|
||
dönüş değerlerini aynı türden bir göstericiye atayabiliriz. Örneğin:
|
||
|
||
int *pi;
|
||
char *pc;
|
||
|
||
pi = foo();
|
||
pc = bar();
|
||
|
||
Bir fonksiyonun geri dönüş değerinin bir adres olması demek aslında return ifadesinin atanacağı geçici değişkenin bir gösterici olması demektir.
|
||
O halde geri dönüş değeri adres olan fonksiyonlara aynı türden bir adres değeir ile return uygulamak gerekir.
|
||
|
||
Aşağıdaki bir dizinin en büyük elemanının dizi içerisindeki adresine geri dönen bir fonksiyon örneği verilmiştir. Bu örnekte önce dizinin ilk elemanı en büyük
|
||
varsayılmıştır. Onun adresi pmax isimli göstericide tutulmuştur. Sonra daha büyük elemanla karşılaşıldığında pmax adresi bu elemanı gösterecek biçimde değiştirilmiştir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int *getmax_addr(int *pi, size_t size);
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {34, 23, 12, 67, 25, 12, 89, 11, 26, 67};
|
||
int *pi;
|
||
|
||
pi = getmax_addr(a, 10);
|
||
printf("%d\n", *pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
int *getmax_addr(int *pi, size_t size)
|
||
{
|
||
int max = pi[0];
|
||
int *pmax = pi;
|
||
|
||
for (size_t i = 1; i < size; ++i)
|
||
if (pi[i] > max) {
|
||
max = pi[i];
|
||
pmax = &pi[i];
|
||
}
|
||
|
||
return pmax;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii aslında yukarıdaki örnekteki getmaxx_addr fonksiyonu daha kolay yazılabilir. Şöyle ki, biz zaten en büyük elemanın adresini tutuyorsak
|
||
en büyük elemanı ayrıca tutmamıza gerek yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int *getmax_addr(int *pi, size_t size);
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {34, 23, 12, 67, 25, 12, 89, 11, 26, 67};
|
||
int *pi;
|
||
|
||
pi = getmax_addr(a, 10);
|
||
printf("%d\n", *pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
int *getmax_addr(int *pi, size_t size)
|
||
{
|
||
int *pmax = &pi[0];
|
||
|
||
for (size_t i = 1; i < size; ++i)
|
||
if (pi[i] > *pmax)
|
||
pmax = &pi[i];
|
||
|
||
return pmax;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de hiçbir nesnenin ve fonksiyonun adresi olamayacak kullanılmayan boş bir byte'ın adresi NULL adres olarak belirlenmiştir. Ancak C standartlarına göre
|
||
NULL adresin sayısal bileşeninin ne olacağı derleyicileri yazanların isteğine bırakılmıştır. Yani değişik sistemlerde NULL adresin sayısal bileşeni
|
||
farklı olabilir. Derleyicileri yazanlar işletim sistemi tarafından boş bırakılmış olan bir alandaki adresi NULL adres olarak belirleyebilirler.
|
||
Her ne kadar standartlar NULL adresin sayısal bileşeninin ne olacağını derleyicileri yazanların isteğine bırkmışsa da yaygın sistemlerin hemen hepsinde
|
||
NULL adres belleğin tepesindeki 0 numaralı adrestir. Windows, UNIX/Linux, ve macOS sistemlerindeki tüm C derleyicileri NULL adresi 0 numaralı adres olarak
|
||
kullanmnaktadır.
|
||
|
||
C'de NULL adres düz bir 0 sabiti ile ya da 0 değerini veren tamsayı türlerine ilişkin bir sabit ifadesi ile temsil edilmektedir. (Aynı zamanda 0 değerini veren
|
||
(void *) türüne dönüştürülmüş tamsayı türlerine ilişkin sabit ifadeleri de NULL adres anlamına gelmektedir.) Biz C'de 0 sabitini bir adresle ilişkilendirdiğimizde
|
||
artık bu 0 sabiti int olan 0 sabiti değil o sistemde derleyicinin belirlediği NULL adres anlamına gelmektedir. NULL adres herhangi türden bir göstericiye
|
||
atanabilir. Biz C'de bir göstericiye 0 atadığımızda o göstericiye int olan 0'ı atamış olmamaktayız. O sistemde NULL adres neyse onu atamış olmaktayız. Örneğin:
|
||
|
||
int *pi;
|
||
|
||
pi = 0; /* geçerli */
|
||
|
||
Burada pi'ye 0 sayısı atanmamıştır. 0 adresi de atanmamıştır. Çalışılan sistemde NULL adres olarak hangi adres temsil edildiyse o adres atanmıştır.
|
||
Yukarıda da belirtitğimiz gibi yaygın sistemlerin hepsinde NULL adres gerçekten 0 adresi olarak seçilmiştir. Tabii aslında standartlara göre NULL adres
|
||
yalnızca 0 sabiti ile değil 0 değerini veren tamsayı türlerine ilişkin sabit ifadeleriyle de oluşturulabilir. Örneğin:
|
||
|
||
int *pi = 3 - 3; /* geçerli, pi'ye o sistemdeki NULL adres atanıyor */
|
||
|
||
Tabii programcılar tipik olarak NULL adresi düz 0 sabiti olarak kullanırlar. Örneğin:
|
||
|
||
int a = 0;
|
||
int *pi = a; /* geçersiz! göstericiye int bir değer atanmış */
|
||
|
||
Burada göstericiye NULL adres atanmamıştır. Çünkü standartlara göre 0 değerini veren sabit ifadesi NULL adresi temsil etmektedir. Oysa bu örnekte
|
||
göstericiye bir sabit ifadesi atanmamıştır.
|
||
|
||
Benzer biçimde bir gösterici 0 ile (ya da 0 değerini veren tamsayı türlerine ilişkin bir sabit ifadesi ile) karşılaştırıldığında aslında
|
||
karşılaştırma göstericinin içerisinde o sistemdeki NULL adresin olup olmadığını anlamaya yönelik yapılmaktadır. Örneğin:
|
||
|
||
if (pi == 0) {
|
||
/* ... */
|
||
}
|
||
|
||
Burada pi göstericisinin içerisinde 0 adresi olup olmadığına bakılmamaktadır. pi agöstericisinin içerisinde o sistemdeki NULL adresin olup olmadığına bakılmaktadır.
|
||
Örneğin falanca sistemde NULL adres FFFFFFFF adresi olsun. Ve pi göstericisinin içerisinde bu adresin olduğunu düğünelim bu durumda pi == 0 karşılaştırması
|
||
doğru yani 1 değerini verecektir. Benzer biçimde bir gösterici != operatörü kullanılarak 0 ile karşılaştırılabilir:
|
||
|
||
if (pi != 0) {
|
||
/* ... */
|
||
}
|
||
|
||
Burada pi'nin içerisinde o sistemdeki NULL adres yoksa if deyimi doğrudan sapacaktır.
|
||
|
||
if deyiminde (while deyimde de) parantez içerisindeki ifade yalnızca bir adres bilgisinden oluşabilir. Bu durumda karşılaştırma o adres bilgisinin
|
||
o sistemdeki NULL adres olup olmadığına göre yapılır. Örneğin p bir gösterici olsun:
|
||
|
||
if (p) {
|
||
/* p NULL adres değilse bu kısım yapılacak */
|
||
}
|
||
else {
|
||
/* p NULL adres ise bu kısım yapılacak */
|
||
}
|
||
|
||
Burada p'nin içerisinde o sistemdeki NULL adres varsa if deyimi yanlıştan, yoksa doğrudan sapar. Örneğin falanca sistemde NULL adres FFFFFFFF olsun.
|
||
p'nin içerisinde de FFFFFFFF değerinin olduğunu varsayalım. Bu durumda if deyimi yanlıştan sapacaktır. Görüldüğü gibi if parantezi içerisinde bir adres
|
||
bilgisi varsa burada o adresin 0 adresi olup olmadığına değil o sistemdeki NULL adres olup olmadığına bakılmaktadır. Başka bir deyişle:
|
||
|
||
if (p) {
|
||
/* ... */
|
||
}
|
||
|
||
ile
|
||
|
||
if (p != 0) {
|
||
/* ... */
|
||
}
|
||
|
||
aynı anlamdadır.
|
||
|
||
Bir gösterici (genel olarak adres bilgisi) ! operatörü ile kullanılabilir. Örneğin p bir gösterici olsun !p ifadesi geçerlidir. Bu durumda eğer
|
||
göstericisinin içerisinde NULL adres varsa bu ifade 1 değerini, yoksa 0 değerini üretir. Örneğin:
|
||
|
||
if (!p) {
|
||
/* p NULL ise bu kısım yapılacak */
|
||
}
|
||
|
||
Burada p'nin içerisinde NULL adres varsa birtakım şeyler yapılmak istenmiştir.
|
||
|
||
C99 ile birlikte C'ye _Bool isimli bir bool türünün eklendiğini belirtmiştik. İşte bir adres türü doğrudan bool türüne atanabilmektedir. Bu durumda
|
||
adres NULL adres değilse 1 değeri NULL adres ise 0 değeri _Bool türünden değişkene atanmış olur.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
_Bool b;
|
||
int *pi;
|
||
|
||
pi = NULL;
|
||
|
||
b = pi;
|
||
printf("%d\n", b); /* 0 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
NULL adres sabiti okunabilirliği artırmak için bazı başlık dosyalarında da define edilmiştir. Böylece biz NULL adres için 0 kullanmak yerine
|
||
NULL sözcüğünü kullanabiliriz:
|
||
|
||
#define NULL 0
|
||
|
||
<stdio.h>, <stdlib.h>, <stddef.h>, <string.h> dosyalarında NULL sembolik sabiti NULL adresin okunabilir kullanımı için define edilmiş durumdadır. Bu sayede
|
||
örneğin biz:
|
||
|
||
p = 0;
|
||
|
||
yerine:
|
||
|
||
p = NULL;
|
||
|
||
gibi bir ifade yazabiliriz. Ya da örneğin:
|
||
|
||
if (p == NULL) {
|
||
/* ... */
|
||
}
|
||
|
||
Burada yine p'nin NULL adres içerip içermediğine bakılmaktadır. NULL sembolik sabiti int 0 olarak kullanılmamalıdır. NULL sembolik sabitinden amaçlanan
|
||
NULL adresin okunabilir bir biçimde ifade edilmesidir. Aslında standartlara göre yukarıda da belirttiğimiz gibi NULL sembolik sabiti aşağıdaki gibi
|
||
define edilmiş de olabilir:
|
||
|
||
#define NULL ((void *)0)
|
||
|
||
Biz henüz void adresleri görmediğimiz için (void *)0 ifadesini açıklamayacağız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
NULL adres sabitinin tamsayısal sıfır değeri ile temsil edilmesi C'de programcıların kafasını karıştıran bir durumdur.
|
||
Örneğin 0 sabitinin hem int türden sabit olması hem de göstericilerle ilişkilendirildiğinde NULL adres sabiti anlamına
|
||
gelmesi özellikle C'yi yeni öğrenenlerde karışıklık duygusu uyandırmaktadır. İşte C++'a C++11 ile birlikte NULL adres
|
||
sabiti için nullptr isimli ayrı bir anahtar sözcük de eklenmiştir. Bu anahtar sözcük C'nin henüz basılmayan ama içerik
|
||
olarak oluşturulmuş olan C23 versiyonuna da sokulmuştur. Yani C23 ile birlikte artık C'de de bir göstericiye NULL
|
||
adres sabiti aşağıdaki gibi atanabilecektir:
|
||
|
||
int *pi = nullptr;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
38. Ders 18/10/2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de prototipleri <string.h> içerisinde bulunan yazılar üzerinde işlem yapan, ismi str ile başlayan bir grup standart C fonksiyonu vardır. Bunlara string
|
||
fonksiyonları denilmektedir. Bu bölümde bu string fonksiyonlarının önemli olanlarını tanıtacağız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strlen fonksiyonu bir yazının uzunluğu ile geri dönen bir string fonksiyondur. Fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
size_t strlen(const char *str);
|
||
|
||
Fonksiyonun protipindeki const anahtar söczüğünü henüz görmedik. Bu anahtar sözcüğe şimdilik dikkat etmeyiniz. Fonksiyonun geri dönüş değeri size_t türündendir.
|
||
size_t işaretsiz bir tamsayı türü olmak üzere standart türlerden birini temsil etmektedir. Ancak bu türün hangi işaretsiz tamsayı türünü temsil ettiği derleyicileri
|
||
yazanların isteğine bırakılmıştır. size_t türü printf fonksiyonu ile yazdırılırkan %z format karakteri kullanılır. Buradaki z'nin yanında d (decimal), x (hex), o (octal)
|
||
karakterlerinden biri getirilmelidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
size_t result;
|
||
|
||
result = strlen(s);
|
||
printf("%zd\n", result); /* 6 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strlen fonksiyonunu biz de yazabiliriz. Tek yapacağımız şey null karakter görene kadar karakterlerin sayısını hesaplamaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
size_t mystrlen(char *str)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; str[i] != '\0'; ++i)
|
||
;
|
||
|
||
return i;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara"; /* dikkat Türkçe karakterleri editörünüz UTF-8 olarak iki byte halinde kodluyor olabilir */
|
||
size_t result;
|
||
|
||
result = mystrlen(s);
|
||
printf("%zd\n", result); /* 6 görebilirsiniz */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte klavyeden (stdin dosyasından) girilen bir yazının uzunluğu ekrana (stdout dosyasına) yazdırırlmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[1024];
|
||
|
||
printf("Bir yazi giriniz:");
|
||
gets(s);
|
||
|
||
printf("%zd\n", strlen(s));
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bu tür örneklerde Türkçe karakterleri kullanmayınız. Çünkü Türkçe karakterleri günümüz editörlerinin çoğu UTF-8 olarak kodlamaktadır. Türkçe karakterler
|
||
UTF-8 kodlamasında iki byte yer kaplarlar. Bu nedenle örneğin "ağrı" gibi bir yazı için strlen fonksiyonu uygulanırsa 4 değil 6 değeri bulabilirsiniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ağrı"; /* dikkat Türkçe karakterleri editörünüz UTF-8 olarak iki byte halinde kodluyor olabilir */
|
||
size_t result;
|
||
|
||
result = strlen(s);
|
||
printf("%zd\n", result); /* 6 görebilirsiniz */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strcpy fonksiyonu char türden bir dizi içerisindeki yazıyı başka bir diziye kopyalamak için kullanılır. Orijinal prototipi şöyledir:
|
||
|
||
char *strcpy(char *dest, const char *source);
|
||
|
||
Burada const anahtar sözcüğünü henüz görmedik. Fonksiyon ikinci parametresi ile belirtilen adresten başlayarak birincici parametresiyle belirtilen adrese
|
||
null karakter görene kadar (null karakter de dahil) kopyalama yapar. Fonksiyon birinci parametresiyle verilen adresin aynısına geri dönmektedir. Tabii
|
||
genellikle bu geri dönüş değerine gereksinim duyulmaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[1024];
|
||
|
||
strcpy(d, s);
|
||
|
||
puts(d); /* ankara */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strcpy fonksiyonu birinci parametresiyle belirtilen yani kopyalamanın yapıldığı hedef adresin aynısına geri dönmektedir. Genellikle programcılar
|
||
bu geri dönüş değerini kullanmazlar. Ancak bazen aşağıdaki gibi kodlarda bu geri dönüş değerinin kullanıldığını görebilirsiniz:
|
||
|
||
printf("%s\n", strcpy(d, s));
|
||
|
||
Burada strcpy s adresindeki yazıyı d adresinden itibaren kopyalar. d adresinin aynısına geri döndüğü için buradaki yazı aynı zamanda printf ile yazdırılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[1024];
|
||
|
||
printf("%s\n", strcpy(d, s)); /* ankara */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strcpy fonksiyonu genellikle düz bir mantıkla programcılar tarafından aşağıdaki gibi yazılmaktadır:
|
||
|
||
char *mystrcpy(char *dest, char *source)
|
||
{
|
||
char *temp = dest;
|
||
|
||
while (*source != '\0') {
|
||
*dest = *source;
|
||
++source;
|
||
++dest;
|
||
}
|
||
*dest = '\0';
|
||
|
||
return temp;
|
||
}
|
||
|
||
Aslında atama işlemi while parantezi içerisinde yapılabilir. Böylece null karakter de atanmış olur:
|
||
|
||
char *mystrcpy(char *dest, char *source)
|
||
{
|
||
char *temp = dest;
|
||
|
||
while ((*dest = *source) != '\0') {
|
||
++source;
|
||
++dest;
|
||
}
|
||
|
||
return temp;
|
||
}
|
||
|
||
Tabii aslında en sade yazım aşağıdaki gibi olabilir:
|
||
|
||
char *mystrcpy(char *dest, char *source)
|
||
{
|
||
for (size_t i = 0; (dest[i] = source[i]) != '\0'; ++i)
|
||
;
|
||
|
||
return dest;
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
char *mystrcpy(char *dest, char *source);
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[1024];
|
||
|
||
mystrcpy(d, s);
|
||
printf("%s\n", d);
|
||
|
||
return 0;
|
||
}
|
||
|
||
char *mystrcpy(char *dest, char *source)
|
||
{
|
||
for (size_t i = 0; (dest[i] = source[i]) != '\0'; ++i)
|
||
;
|
||
|
||
return dest;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte s adresinden iki ilerinin adresi fonksiyona gönderilmiştir. Bu durumda fonksiyon "kara" yazısını hedef diziye kopyalayacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[1024];
|
||
|
||
strcpy(d, s + 2);
|
||
printf("%s\n", d); /* kara */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strcat fonksiyonu bir yazının sonuna başka bir yazıyı eklemek için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
char *strcat(char *dest, const char *source);
|
||
|
||
Buradaki const anahtar sözcüğünü henüz görmedik. Fonksiyon birinci parametresiyle belirtilen adreste bulunan yazının sonuna oradaki null karakteri ezerek
|
||
ikinci parametresiyle belirtilen adresten başlayarak null karakter görene kadar (null karakter dahil) karakterleri kopyalar. Birinci parametresiyle
|
||
belirtilen hedef adresin aynısına geri döner. (Buradaki "cat" sözcüğü "concatenate" sözcüğünden gelmektedir.) Burada programcı eklemenin yapılacağı hedef dizinin
|
||
yeterince büyük olmasına dikkat etmelidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[1024] = "istanbul";
|
||
|
||
strcat(d, s);
|
||
|
||
puts(d); /* istanbulankara */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki gibi bir kullanım tanımsız davranışa yol açar:
|
||
|
||
char s[1024] = "ankara";
|
||
char d[1024];
|
||
|
||
strcat(d, s);
|
||
|
||
Çünkü burada d dizisinin içerisinde çöp değerler olduğu için o çöp değerlerin sonunda tesadüfen bulunan bir null karakterden sonra karakter kopyalanacaktır. Örneğin:
|
||
|
||
char s[1024] = "ankara";
|
||
char d[1024] = "";
|
||
|
||
strcat(d, s);
|
||
|
||
Burada d dizisinin tüm elemanları sıfırlanacaktır. Tabii 0 aynı zamanda null karakter anlamındadır. Bu durumda yapılan işlemin strcpy işleminden bir farkı kalmayacaktır.
|
||
|
||
Aşağıdaki örnekte 5 kez klavyeden (stdin dosyasından) girilen yazı diğer bir dizinin içerisindeki yazının sonuna eklenmiştir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[1024];
|
||
char d[1024] = "";
|
||
|
||
for (int i = 0; i < 5; ++i) {
|
||
printf("Bir yazi giriniz:");
|
||
gets(s);
|
||
strcat(d, s);
|
||
}
|
||
printf("%s\n", d);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strcat fonksiyonunu da kolay bir biçimde yazabiliriz. İlk akla gelen yazım biçimi şöyle olabilir:
|
||
|
||
char *mystrcat(char *dest, char *source)
|
||
{
|
||
char *temp = dest;
|
||
|
||
while (*dest != '\0')
|
||
++dest;
|
||
|
||
strcpy(dest, source);
|
||
|
||
return temp;
|
||
}
|
||
|
||
Burada dest göstericisi artırılarak null karakteri gösterir hale getirilmiştir. Ondan o adrese kopyalama yapılmıştır. Tabii buradaki stcpy kısmını da
|
||
kod olarak yazabilirdik:
|
||
|
||
char *mystrcat(char *dest, char *source)
|
||
{
|
||
char *temp = dest;
|
||
|
||
while (*dest != '\0')
|
||
++dest;
|
||
|
||
while ((*dest = *source) != '\0') {
|
||
++source;
|
||
++dest;
|
||
}
|
||
|
||
return temp;
|
||
}
|
||
|
||
Ya da [] operatörü ile de aynı fonksiyonu yazabilirdik:
|
||
|
||
|
||
char *mystrcat(char *dest, char *source)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; dest[i] != '\0'; ++i)
|
||
;
|
||
for (size_t k = 0; (dest[i + k] = source[k]) != '\0'; ++k)
|
||
;
|
||
|
||
return dest;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
char *mystrcat(char *dest, char *source);
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[1024] = "istanbul";
|
||
|
||
mystrcat(d, s);
|
||
|
||
puts(d); /* istanbulankara */
|
||
|
||
return 0;
|
||
}
|
||
|
||
char *mystrcat(char *dest, char *source)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; dest[i] != '\0'; ++i)
|
||
;
|
||
for (size_t k = 0; (dest[i + k] = source[k]) != '\0'; ++k)
|
||
;
|
||
|
||
return dest;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strchr fonksiyonu bir yazı içerisinde bir karakteri aramak için kullanılır. Fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
char *strchr(const char *str, int ch);
|
||
|
||
Buradaki const anahtar sözcüğünü henüz görmedik. Fonksiyon birinci parametresiyle belirtilen adresten başlayarak null karakter görene kadar ikinci parametresiyle belirtilen
|
||
karakteri arar. Eğer bulursa ilk bulduğu yerin yazı içerisindeki adresiyle geri döner. Eğer bulmazsa NULL adresle geri döner. Fonksiyon null karakterin
|
||
kendisini de arayabilmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char *str;
|
||
|
||
str = strchr(s, 'k');
|
||
if (str != NULL)
|
||
printf("%s\n", str); /* kara */
|
||
else
|
||
printf("cannot find!");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
foo fonksiyonu bir adres bilgisine geri dönüyor olsun. *foo() gibi bir işlem geçerlidir. Burada önce foo fonksiyonu çağrılır. Ele edilen adrese *
|
||
operatörü uygulanmıştır. Benzer biçimde foo()[n] ifadesi de geçerlidir. Burada fonksiyon çağırma operatörü ile [] operatörü soldan sağa eşit öncelikli
|
||
gruptadır. Bu durumda önce fonksiyon çağrılır. Fonksiyonun geri döndürdüğü adresten n ilerinin içeriği elde edilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char *str;
|
||
|
||
*strchr(s, 'k') = 'x';
|
||
|
||
puts(s); /* anxara */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıda benzer bir örnek verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char *str;
|
||
|
||
strchr(s, 'k')[2] = 'x';
|
||
|
||
puts(s); /* ankaxa */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strchr fonksiyonu ile null karakterin kendisi de aranabilir. Örneğin biz null karakterin adresini bulmak için şöyle yapabiliriz:
|
||
|
||
str = strchr(s, '\0');
|
||
|
||
Örneğin aslında strcat aşağıdaki ile eşdeğerdir:
|
||
|
||
strcpy(strchr(dest, '\0'), source);
|
||
|
||
s char türden bir adres olmak üzere biz s adresinde bulunan yazının sonundaki null karakterin adresini s + strlen(s) biçiminde ya da strchr(s, '\0')
|
||
biçiminde elde edebiliriz.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
char *mystrcat(char *dest, char *source)
|
||
{
|
||
strcpy(strchr(dest, '\0'), source);
|
||
|
||
return dest;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[1024] = "istanbul";
|
||
|
||
mystrcat(d, s);
|
||
puts(d);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strchr fonksiyonunu basit bir biçimde yazabiliriz. Ancak gerçekleşirimde null karakterin de aranabileceğini göz önünde bulundurmalısınız:
|
||
|
||
char *mystrchr(char *str, int ch)
|
||
{
|
||
while (*str != '\0') {
|
||
if (*str == ch)
|
||
return str;
|
||
++str;
|
||
}
|
||
|
||
return ch == '\0' ? str : NULL;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
char *mystrchr(char *str, int ch);
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char *str;
|
||
|
||
str = mystrchr(s, 'k');
|
||
puts(str); /* kara */
|
||
|
||
return 0;
|
||
}
|
||
|
||
char *mystrchr(char *str, int ch)
|
||
{
|
||
while (*str != '\0') {
|
||
if (*str == ch)
|
||
return str;
|
||
++str;
|
||
}
|
||
|
||
return ch == '\0' ? str : NULL;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strchr fonksiyonunun karakteri son blduğu yerin adresiyle geri dönen (yani başka bir deyişle aramayı sondan başa doğru yapan) strrchr isimli bir
|
||
benzeri de vardır. strrchr fonksiyonunun orijinal prototipi şöyledir:
|
||
|
||
char *strrchr(const char *str, int ch);
|
||
|
||
Biz henüz buradaki const anahtar sözcüğünü görmedik.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "izmir";
|
||
char *str;
|
||
|
||
str = strrchr(s, 'i');
|
||
if (str)
|
||
puts(str); /* ir */
|
||
else
|
||
printf("cannt find!..\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte UNIX/Linux işletim sistemlerinde bir yol ifadesinin (path) sonundaki dosya ismi yazdırılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char path[] = "/home/kaan/study/test.c";
|
||
char *fname;
|
||
|
||
fname = strrchr(path, '/');
|
||
if (fname)
|
||
puts(fname + 1);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii aslında bir yol ifadesi hiç '/' karakteri içermeyebilir. O halde yukarıdaki programı bu durumu da ele alacak biçimde aşağıdaki gibi düzeltebiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char path[4096];
|
||
char *fname;
|
||
|
||
printf("Bir yol ifadesi giriniz:");
|
||
gets(path);
|
||
|
||
fname = strrchr(path, '/');
|
||
if (fname)
|
||
puts(fname + 1);
|
||
else
|
||
puts(path);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki gibi durumlarda programcılar genellikle daha kompakt bir yazım sağlamak için atama işlemini if parantezi içerisinde yaparlar. Tabii bu durumda
|
||
atama operatörünün paranteze alınarak önceliklendirilmesi gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char path[4096];
|
||
char *fname;
|
||
|
||
printf("Bir yol ifadesi giriniz:");
|
||
gets(path);
|
||
|
||
if ((fname = strrchr(path, '/')) != NULL)
|
||
puts(fname + 1);
|
||
else
|
||
puts(path);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strrchr fonksiyonunu şöyle yazabiliriz:
|
||
|
||
char *mystrrchr(char *str, int ch)
|
||
{
|
||
char *result = NULL;
|
||
|
||
while (*str != '\0') {
|
||
if (*str == ch)
|
||
result = str;
|
||
++str;
|
||
}
|
||
|
||
return ch == '\0' ? str : result;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
char *mystrrchr(char *str, int ch);
|
||
|
||
int main(void)
|
||
{
|
||
char path[4096];
|
||
char *fname;
|
||
|
||
printf("Bir yol ifadesi giriniz:");
|
||
gets(path);
|
||
|
||
if ((fname = mystrrchr(path, '/')) != NULL)
|
||
puts(fname + 1);
|
||
else
|
||
puts(path);
|
||
|
||
return 0;
|
||
}
|
||
|
||
char *mystrrchr(char *str, int ch)
|
||
{
|
||
char *result = NULL;
|
||
|
||
while (*str != '\0') {
|
||
if (*str == ch)
|
||
result = str;
|
||
++str;
|
||
}
|
||
|
||
return ch == '\0' ? str : result;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
39. Ders 18/10/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strcmp fonksiyonu iki yazıyı karşılaştırmak için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
int strcmp(const char *s1, const char *s2);
|
||
|
||
Biz henüz const anahtar sözcüğünü görmedik. Fonksiyonun iki parametresi karşılaştırılacak iki yazının adreslerini almaktadır. Fonksiyon "leksikografik"
|
||
bir karşılaştırma yapar. Leksikografik karşılaştırma demek "eşit olduğu sürece ilerle, ilk eşit olmayanın durumuna bak" demektir. Yani sözlükteki sıraya
|
||
göre karşılaştırma anlamına gelir. Örneğin:
|
||
|
||
- "ali" yazısı "alm" yazısından küçüktür.
|
||
- "aliye" yazısı "ali" yazısından büyüktür.
|
||
- "ali" yazısı "Ali" yazısından ASCII karakter tablosu kullanılıyorsa büyüktür.
|
||
- "ali" yazısı ile "ali" yazısı biribirine eşittir.
|
||
|
||
strcmp fonksiyonu birinci yazı ikinci yazıdan büyükse pozitif herhangi bir değere, ikinci yazı birinci yazıdan büyükse negatif herhangi bir değere
|
||
ve iki yazı eşitse sıfır değerine geri döner.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char passwd[] = "maviay";
|
||
char s[1024];
|
||
|
||
printf("Enter password:");
|
||
gets(s);
|
||
|
||
if (!strcmp(s, passwd))
|
||
printf("Ok\n");
|
||
else
|
||
printf("Incorrect password!..\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki programı üç kere denemeli hale getirebiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char passwd[] = "maviay";
|
||
char s[1024];
|
||
int i;
|
||
|
||
for (i = 0; i < 3; ++i) {
|
||
printf("Enter password:");
|
||
gets(s);
|
||
if (!strcmp(s, passwd))
|
||
break;
|
||
printf("Incorrect password!..\n");
|
||
}
|
||
|
||
if (i == 3)
|
||
printf("Sorry you must wait 5 minutes to try again...\n");
|
||
else
|
||
printf("Ok\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strcmp fonksiyonunu aşağıdaki gibi yazabiliriz:
|
||
|
||
int mystrcmp(char *s1, char *s2)
|
||
{
|
||
while (*s1 == *s2) {
|
||
if (*s1 == '\0')
|
||
return 0;
|
||
++s1;
|
||
++s2;
|
||
}
|
||
return *s1 - *s2;
|
||
}
|
||
|
||
if karşılaştırmasında return yerine break deyimini de kullanabilirdik:
|
||
|
||
int mystrcmp(char *s1, char *s2)
|
||
{
|
||
while (*s1 == *s2) {
|
||
if (*s1 == '\0')
|
||
break;
|
||
++s1;
|
||
++s2;
|
||
}
|
||
return *s1 - *s2;
|
||
}
|
||
|
||
Ya da örneğin if deyimi yerine while parantezinin içerisinde && operatörü de kullanılabilir.
|
||
|
||
int mystrcmp(char *s1, char *s2)
|
||
{
|
||
while (*s1 == *s2 && *s1 != '\0') {
|
||
++s1;
|
||
++s2;
|
||
}
|
||
return *s1 - *s2;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int mystrcmp(char *s1, char *s2);
|
||
|
||
int main(void)
|
||
{
|
||
char passwd[] = "maviay";
|
||
char s[1024];
|
||
|
||
printf("Enter password:");
|
||
gets(s);
|
||
|
||
if (!mystrcmp(s, passwd))
|
||
printf("Ok\n");
|
||
else
|
||
printf("Incorrect password!..\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
int mystrcmp(char *s1, char *s2)
|
||
{
|
||
while (*s1 == *s2) {
|
||
if (*s1 == '\0')
|
||
return 0;
|
||
++s1;
|
||
++s2;
|
||
}
|
||
return *s1 - *s2;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strcpy, strcat ve strcmp fonksiyonlarının n'li versiyonları da vardır. Bunlar strncpy, strncat ve strncmp ismindedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strncpy fonksiyonu bir yazının belli sayıda karakterini bir diziye kopyalamak için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
char *strncpy(char *dest, const char *source, size_t n);
|
||
|
||
Burada fonksiyon ikinci parametresiyle belirtilen adresten başlayarak birinci parametresiyle belirtilen adrese üçüncü parametresiyle belirtilen miktarda
|
||
karakteri kopyalar. Fonksiyon eğer n <= strlen(source) ise null karakteri hedefe yerleştirmez. Ancak n > strlen(source) ise n - strlen(source) kadar null karakteri
|
||
hedefe kopyalamaktadır. Örneğin "ankara" yazısını kopyalama istediğimizi ve n değerinin 3 olduğunu düşünelim. Fonksiyon "ank" karakterlerini kopyalar ve
|
||
null karakteri eklemez. Ancak örneğin n değeri 30 ise fonksiyon "ankara" yazısının hepsini kopyalar. 30 - 6 = 24 tane null karakteri hedefe ekler.
|
||
Yani fonksiyon her zaman n tane karakteri hedefe kopyalamaktadır. strncpy fonksiyonu genellikle bir yazının belli bir kısmını başka bir yazıyla yer değiştirmek
|
||
amacıyla kullanılmaktadır. Bu durumda:
|
||
|
||
strcpy(dest, source);
|
||
|
||
çağısının strncp ile eşdeğeri şöyledir:
|
||
|
||
strncpy(dest source, strlen(source) + 1);
|
||
|
||
strncpy fonksiyonu da kopyalamanın yapıldığı hedef adrese geri dönmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char d[] = "yenisehir";
|
||
char s[] = "eskihisar";
|
||
|
||
strncpy(d, s, 4);
|
||
|
||
puts(d); /* eskisehir */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strncpy fonksiyonu çeşitli biçimlerde yazılabilir. Örneğin:
|
||
|
||
char *mystrncpy(char *dest, char *source, size_t n)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; i < n && source[i] != '\0'; ++i)
|
||
dest[i] = source[i];
|
||
|
||
for (; i < n; ++i)
|
||
dest[i] = '\0';
|
||
|
||
return dest;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
char *mystrncpy(char *dest, char *source, size_t n);
|
||
|
||
int main(void)
|
||
{
|
||
char d[1024] = "yenisehir";
|
||
char s[] = "eski";
|
||
|
||
mystrncpy(d, s, 10);
|
||
|
||
puts(d); /* eskisehir */
|
||
|
||
return 0;
|
||
}
|
||
|
||
char *mystrncpy(char *dest, char *source, size_t n)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; i < n && source[i] != '\0'; ++i)
|
||
dest[i] = source[i];
|
||
|
||
for (; i < n; ++i)
|
||
dest[i] = '\0';
|
||
|
||
return dest;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strncat fonksiyonu bir yazının sonuna başka bir yazının ilk n karakterini eklemek için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
char *strncat(char *dest, const char *source, size_t n);
|
||
|
||
Fonksiyon ikinci parametresiyle belirtilen yazının ilk n karakterini birinci parametresiyle belirtilen yazının sonuna ekler. Fonksiyon her zaman hedefe
|
||
null karakteri eklemektedir. Eğer n değeir büyükse yani n > strlen(source) ise en son null karakteri ekler ve işlemini sonlandırır. Yani başka bir deyişle
|
||
eğer n değeri büyükse fonksiyon strcat gibi davranmaktadır. Fonksiyon yine birinci parametreyle girilen adresin aynısına geri dönmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char d[1024] = "eski";
|
||
char s[] = "hisarustu";
|
||
|
||
strncat(d, s, 5);
|
||
|
||
puts(d); /* eskihisar */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strncat fonksiyonu da değişik biçimlerde yazılabilir. Aşağıda bunlardan bir örnek verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
char *mystrncat(char *dest, char *source, size_t n);
|
||
|
||
int main(void)
|
||
{
|
||
char d[1024] = "eski";
|
||
char s[] = "hisarustu";
|
||
|
||
mystrncat(d, s, 5);
|
||
|
||
puts(d); /* eskihisar */
|
||
|
||
return 0;
|
||
}
|
||
|
||
char *mystrncat(char *dest, char *source, size_t n)
|
||
{
|
||
size_t i, k;
|
||
|
||
for (i = 0; dest[i] != '\0'; ++i)
|
||
;
|
||
|
||
for (k = 0; k < n && source[k] != '\0'; ++k)
|
||
dest[i + k] = source[k];
|
||
dest[i + k] = '\0';
|
||
|
||
return dest;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strncmp fonksiyonu iki yazının ilk n karakterini karşılaştırmak için kullanılmaktadır. Fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
int strncmp(const char *s1, const char *s2, size_t n);
|
||
|
||
Biz henüz const anahtar sözcüğünü görmedik. Burada n değeri strlen(s1) ya da strlen(s2)'den büyükse karşılaştırma sonlandırılmaktadır. Başka bir deyişle
|
||
iki yazının herhangi birinde null karakter görülürse karşılaştırma sonlandırılmaktadır. Yani n > strlen(s1) ya da n > strlen(s2) ise bu durumda fonksiyonun
|
||
strcmp'den bir farkı kalmaz. Fonksiyon yine birinci yazı ikinci yazdıdan büyükse pozitif herhangi bir değere, ikinci yazı birinci yazıdan büyükse negatif herhangi bir
|
||
değere ve iki yazı birbirine eşitse sıfır değerine geri dönmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s1[] = "eskis";
|
||
char s2[] = "eskihisar";
|
||
int result;
|
||
|
||
result = strncmp(s1, s2, 4);
|
||
|
||
if (result > 0)
|
||
printf("s1 > s2\n");
|
||
else if (result < 0)
|
||
printf("s1 < s2\n");
|
||
else
|
||
printf("s1 == s2\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strncmp fonksiyonunu aşağıdaki gibi yazabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
#include <stdio.h>
|
||
|
||
int mystrncmp(char *s1, char *s2, size_t n)
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; i < n && s1[i] == s2[i]; ++i)
|
||
if (s1[i] == '\0')
|
||
return 0;
|
||
|
||
if (i == n)
|
||
return 0;
|
||
|
||
return s1[i] - s2[i];
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "eskihisar";
|
||
char k[] = "eskisehir";
|
||
|
||
if (!mystrncmp(s, k, 4))
|
||
printf("Ok\n");
|
||
else
|
||
printf("Not Ok\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
40. Ders 25/10/2022 - Sali
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Sayısal karakterlerden oluşan bir yazıyı int, long ve double türlerine dönüştüren üç klasik standart C fonksiyonu vardır.
|
||
Fonksiyonların orijinal prototipleri şöyledir:
|
||
|
||
int atoi(const char *str);
|
||
long atol(const char *str);
|
||
double atof(const char *str);
|
||
|
||
Buradaki const anahtar sözcüğünü henüz görmedik.
|
||
|
||
Fonksiyonların prototipleri <stdlib.h> dosyası içerisinde bulunmaktadır. atoi (alphabetic to int), atol(alphabetic to long) ve atof (alphabetic to floating point)
|
||
fonksiyonları bizden sayısal karakterlerden oluşan yazının bulunduğu dizinin başlangıç adreslerini alırlar ve bize sırasıyla int, long ve double
|
||
değerler verirler. C99 ile birlikte C'ye long long türü eklenince atoll fonksiyonu da standartlara eklenmiştir:
|
||
|
||
long long atoll(const char *str);
|
||
|
||
Bu fonksiyonun da geri dönüş değerinin long long olduğuna dikkat ediniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "1234";
|
||
int val;
|
||
|
||
val = atoi(s);
|
||
printf("%d\n", val);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
atoi, atol ve atof fonksiyonları yazının başındaki boşluk karakterlerini (leading sapce) atarlar. İlk sayısal olmayan karakterde işlemini sonlandırırlar.
|
||
Tabii sayının başında "-" ve "+" sembolleri de bulunabilir. atof fonksiyonunda "." karakteri de kullanılabilir. Örneğin aşağıdaki yazıların
|
||
bu fonksiyonlarla dönüştürülmesinden elde edilen değerler yanlarına yazılmıştır:
|
||
|
||
" 123 " ---> 123
|
||
" 123ali456 " ---> 123
|
||
"ali" ---> 0
|
||
"" ---> 0
|
||
|
||
Bu fonksiyonlar bir taşma olduğunda (yani yazı içerisindeki sayılar ilgili türün sınırlarını negatif ya da pozitif bakımdan aştığında) errno değerini
|
||
ERANGE olarak set etmektedir. Ancak biz bu errno konusunu henüz bilmiyoruz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "123ali";
|
||
int val;
|
||
|
||
val = atoi(s);
|
||
printf("%d\n", val); /* 123 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Örneğin biz bu fonksiyonlar sayesinde klavyeden (stdin dosyasından) int, long ve double okuyan fonksiyonlar yazabiliriz. Önce yazıyı gets fonksiyonu ile
|
||
okuyup sonra bu fonksiyonlara sokabiliriz. gets fonksiyonunun C99'da "deprecated" yapıldığını ve C11'de C'den çıkartıldığını anımsayınız. Ancak derleyiciler
|
||
bu fonksiyonu halen bulundurmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int read_int(void)
|
||
{
|
||
char s[4096];
|
||
|
||
gets(s);
|
||
|
||
return atoi(s);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
printf("Bir sayi giriniz:");
|
||
val = read_int();
|
||
printf("%d\n", val);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
atoi ve atol fonksiyonları nasıl yazılabilir? Aslında yazının sonundan başa gitmeye gerek yoktur. Sayıyı 10'la çarpıp sağındakiyle ekleyerek
|
||
işlemler yürütülebilir. Örneğin:
|
||
|
||
"12345"
|
||
|
||
1 * 10 + 2 = 12
|
||
12 * 10 + 3 = 123
|
||
123 * 10 + 4 = 1234
|
||
1234 * 10 + 5 = 12345
|
||
|
||
Tabii yazının başındaki boşluk karakterlerini atıp ilk boşluksuz karakterin "+" ya da "-" karakteri olup olmadığına da bakmak gerekir. Tipik
|
||
bir gerçekleştirim aşağıdaki gibi yapılabilir. atol fonksiyonun gerçekleştirimi "çalışma sorusu" olarak verilecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <ctype.h>
|
||
|
||
int myatoi(char *str)
|
||
{
|
||
int total = 0;
|
||
int sign = 1;
|
||
|
||
while (isspace(*str))
|
||
++str;
|
||
|
||
if (*str == '-' || *str == '+') {
|
||
if (*str == '-')
|
||
sign = -1;
|
||
++str;
|
||
}
|
||
|
||
while (isdigit(*str)) {
|
||
total = total * 10 + *str - '0';
|
||
++str;
|
||
}
|
||
|
||
return sign * total;
|
||
}
|
||
|
||
int read_int(void)
|
||
{
|
||
char s[4096];
|
||
|
||
gets(s);
|
||
|
||
return myatoi(s);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
printf("Bir sayi giriniz:");
|
||
val = read_int();
|
||
printf("%d\n", val);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii yukarıdaki gerçekleştirim * operatörü yerine [] operatör ile de yapılabilirdi.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <ctype.h>
|
||
|
||
int myatoi(char *str)
|
||
{
|
||
int total = 0;
|
||
int sign = 1;
|
||
size_t i;
|
||
|
||
for (i = 0; isspace(str[i]); ++i)
|
||
;
|
||
|
||
if (str[i] == '-' || str[i] == '+') {
|
||
if (str[i] == '-')
|
||
sign = -1;
|
||
++i;
|
||
}
|
||
|
||
for (; isdigit(str[i]); ++i)
|
||
total = total * 10 + str[i] - '0';
|
||
|
||
return sign * total;
|
||
}
|
||
|
||
int read_int(void)
|
||
{
|
||
char s[4096];
|
||
|
||
gets(s);
|
||
|
||
return myatoi(s);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
printf("Bir sayi giriniz:");
|
||
val = read_int();
|
||
printf("%d\n", val);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
atoi, atol, atof ve atoll fonksiyonlarının biraz daha yetenekli versiyonları da vardır. Bunların listesi şöyledir: strtol, strtoul, strtoll, strtoull,
|
||
strtod strtof, strtold. Buradaki strtoll ve strtoull, strtof ve strtold fonksiyonları C99 ile eklenmiştir. Bu fonksiyonların prototipinde henüz görmediğimiz
|
||
göstericileri gösteren göstericiler geçmektedir. Bu nedenle biz burada kaba bir açıklama ile yetineceğiz.
|
||
|
||
strtol, strtoul, strtoll ve strtoull fonksiyonlarının yine birinci parametreleri yazının başlangıç adresini almaktadır. Bunların ikinci parametreleri
|
||
ilk geçersiz karakterin tespit edilmesi amacıyla bulundurulmuştur. Bu parametreyi şimdilik NULL adres geçebilirsiniz. Bu fonksiyonların diğer bir fazlalığı da
|
||
taban belirtmeleridir. Fonksiyonların üçüncü parametreleri girilen yazıdaki sayıların tabanını belirtmektedir.
|
||
|
||
strtol yazdıdaki sayıyı long türüne, strtoul unsigned long türüne, strtoll long lon türüne, strtoull unsigned long long türüne, strtof float türüne,
|
||
strtod double türüne ve strtold long double türüne dönüştürmektedir. Burada int ve unsigned int türüne dönüştüren bir strtoxxx fonksiyonu yoktur.
|
||
|
||
Ayrıca bu fonksiyonlarda taban belirten üçüncü parametre 0 girilirse bu durumda taban yazı biçiminde sayının öneklerinden anlaşılacaktır. Eğer önek
|
||
0x ya da 0X ile başlatılmışsa taban 16, 0 ile başlatılmışsa 8, hiçbir şey ile başlatılmamışsa 10 kabul edilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = " 100 ";
|
||
char k[] = " 0x100 ";
|
||
long val;
|
||
|
||
val = strtol(s, NULL, 10);
|
||
printf("%ld\n", val); /* 100 */
|
||
|
||
val = strtol(s, NULL, 2);
|
||
printf("%ld\n", val); /* 4 */
|
||
|
||
val = strtol(k, NULL, 0);
|
||
printf("%ld\n", val); /* 256 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strstr isimli standart C fonksiyonu bir yazının içerisinde başka bir yazıyı arar. Orijinal protipi şöyledir:
|
||
|
||
char *strstr(const char *str1, const char *str2);
|
||
|
||
Buradak const anahtar sözcüğünü henüz görmedik. Fonksiyon birinci parametresiyle belirtilen yazı içerisinde ikinci parametresi ile belirtilen yazıyı
|
||
aramaktadır. Eğer bulursa birinci parametre ile belirtilen yazıda bulduğu yerin adresiyle geri döner. Bulamazsa NULL adresle gei döner.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "this is a test yes!";
|
||
char k[] = "test";
|
||
char *str;
|
||
|
||
str = strstr(s, k);
|
||
if (str != NULL)
|
||
puts(str); /* test yes! */
|
||
else
|
||
printf("cannot find!\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yazı içersinde başka bir yazının aranması basit bir biçimde yapılabilir. Önce aranacak yazının uzunluğu bulunur.
|
||
Sonra ana yazı içerisinde aranacak yazı kaydırılır ve strncmp fonksiyonuyla arama yapılır. Aslında bu klasik
|
||
yöntemin çeşitli alternatifleri de vardır. Bu alternatiflerden biri ""Knuth-Moriss-Pratt" algoritması denilen algoritmadır.
|
||
Ancak standart kütüphaneler genellikle klasik kaydırma yöntemini uygulamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi biz yukarıdaki işlemin tersini yapacak olsak nasıl yaparız? Yani int bir sayıyı yazıya dönüştürmek istersek?
|
||
Bazı C derleyicilerinde bu işi yapan itoa isimli bir eklenti fonksiyon bulunmaktadır. Ancak bu işlemi yapan genel
|
||
sprintf isimli genel bir fonksiyon bulunmaktadır. Pekiyi aşağıdaki prototipe sahip itoa fonksiyonunu nasıl yazabiliriz:
|
||
|
||
char *itoa(int val, char *str);
|
||
|
||
Fonksiyonun birinci parametresi dönüştürülecek int değeri, ikinci parametresi dönüştürülmüş olan yazının adresini
|
||
almaktadır. Fonksiyon ikinci parametresiyle belirtilen adresin aynısına geri döner. Bu tür fonksiyonlar özyinelemeli
|
||
yazılabilmektedir. Ancak özyineleme olmadan bu fonksiyonu yazmak için önce sayının kaç basamaklı olduğunu belirlemek
|
||
sonra da basamakları sayıyı sürekli 10'a bölerek elde etmek gerekir. Aşağıda böyle bir gerçekleştirim verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <math.h>
|
||
|
||
char *itoa(int val, char *str)
|
||
{
|
||
int digit;
|
||
int ndigits;
|
||
int flag = 0;
|
||
|
||
if (val < 0) {
|
||
flag = 1;
|
||
val = -val;
|
||
}
|
||
|
||
ndigits = log10(val) + 1 + flag;
|
||
|
||
for (int i = ndigits - 1; val != 0; --i) {
|
||
digit = val % 10;
|
||
str[i] = digit + '0';
|
||
val /= 10;
|
||
}
|
||
if (flag)
|
||
str[0] = '-';
|
||
|
||
str[ndigits] = '\0';
|
||
|
||
return str;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char s[32];
|
||
int val = -12345;
|
||
|
||
itoa(val, s);
|
||
|
||
printf(":%s:\n", s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi bir göstericiye farklı türden bir adresin doğrudan atanması geçerli bir işlem değildir. Daha teknik ifade edilirse farklı türden adresler
|
||
arasında otomatik (implicit) dönüştürme yoktur. Ancak bazen biz bir gösteriye gerçekten farklı türden bir adresin atanmasını isteyebiliriz. İşte bu tür
|
||
durumlarda tür dönüştürme operatörü kullanılmalıdır. Derleyici tür dönüştürme operatörnü gördüğünde bu işlemin yanlışlıkla değil bilinçli bir biçimde
|
||
yapılmak istendiğini anlamaktadır. Örneğin:
|
||
|
||
char s[10];
|
||
int *pi;
|
||
|
||
pi = s; /* geçersiz! */
|
||
pi = (int *)s; /* geçerli, tür dönüştürmesi yapılmış */
|
||
|
||
Dönüştürmede * atomuna dikkat ediniz "(int *)" gibi bir dönüştürme "int türünden adrese dönüştürme" anlamına gelmektedir. Dönüştürme sırasında kaynak
|
||
adresin sayısal değeri değiştirilmeden hedef adresin türüne dönüştürülür. Yani yukarıdaki örnekte adresin sayısal bileşeninde bir değişiklik olmayacaktır.
|
||
Örneğin biz int bir nesnenin tüm byte'larını yazdırmak isteyebiliriz. Bu duurmda int nesnenin adresini unsigned char türünden bir adrese dönüştürürüz.
|
||
Sonra byte byte nesnenin parçalarını elde ederiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 0x12345678;
|
||
unsigned char *puc;
|
||
|
||
puc = (unsigned char *)&a;
|
||
printf("%02X %02X %02X %02X\n", puc[0], puc[1], puc[2], puc[3]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir tamsayı türünden bilginin de doğrudan bir göstericiye atanması geçerli değildir. Daha teknik bir anlatımla tamsayı türlerinden adres türlerine otoimatik (implicit)
|
||
tür dönüştürmesi yoktur. Ancak bu işlem de tür dönüştürme operatör ile yapılabilir. Aslında biz bunu "adres sabiti" olarak zaten görmüştük. Bu tür durumlarda
|
||
ilgili tamsayı değer değer adresin sayısal bileşeni olarak atanmaktadır. Örneğin:
|
||
|
||
int *pi;
|
||
int a = 12345;
|
||
|
||
pi = a; /* geçersiz */
|
||
pi = (int *)a; /* geçerli */
|
||
pi = (int *) 12345678; /* geçerli, adres sabiti olarak görmüştük */
|
||
pi = (int *) 0x1FC1234; /* geçerli */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir adres bilgisi de adres olmayan bir nesneye atanamaz. Yani teknik olarak ifade edersek adres türlerinden diğer türlere otomatik dönüştürme yoktur.
|
||
Ancak eğer istenirse bir adres türü tür dönüştürme operatörü ile bir tamsayı türüne dönüştürülebilir. Bu durumda dönüştürme sonucunda adres sayısal
|
||
bileşeni elde edilir. Örneğin:
|
||
|
||
int a;
|
||
long b;
|
||
|
||
b = &a; /* geçersiz! int türden adres bilgisi long bir nesneye atanmış */
|
||
b = (long)&a; /* geçerli, tür dönüştürmesi yapılmış */
|
||
|
||
Bu tür işlemler bazen gerekebilmektedir. Örneğin adresler üzerinde bit operatörleriyle işlem yapılamadığından (bit operatörleri ileride ele alınacaktır)
|
||
programcı önce onu bir tamsayı türüne dönüştürüp bir işlemlerini yapıp geri dönüştürmek isteyebilir. Ancak bu tür durumlarda seçilen tamsayı türünün
|
||
adresin sayısal bileşenini içerecek büyüklükte olmasına özen gösterilmelidir. Bu konuda ileride açıklamalar yapılacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi C'de void anahtar sözcüğü fonksiyonun geri dönüş değerinin türü yerine kullanıldığında bu durum fonksiyonun geri dönüş değerine
|
||
sahip olmadığı anlamına geliyordu. void anahtar sözcüğü fonksiyon tanımlamasında ve prototipinde fonksiyonun parametreye sahip olmadığı anlamına geliyordu.
|
||
Yine anımsanacağı gibi tanımlama sırasında parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında bir farklılık yoktu. Ancak
|
||
prototip bildiriminde parametre parantezinin içinin boş bırakılması parametre kontrolünün yapılmayacağı anlamına geliyordu. İşte void anahtar sözcüğü
|
||
gösterici tanımlamada da kullanılabilmektedir. Böyle göstericilere "void göstericiler" denilmektedir. Örneğin:
|
||
|
||
void *pv;
|
||
|
||
Burada pv vir void göstericidir. void türden nesne tanımlanamaz ancak gösterici tanımlanabilir. Örneğin:
|
||
|
||
void a; /* geçersiz! */
|
||
|
||
void gösterici türü olmayan göstericidir. void göstericinin türü olmadığı için void göstericiler * ve [] operatörleriyle kullanılamazlar. Örneğin:
|
||
|
||
void *pv;
|
||
...
|
||
a = *pv; /* geçersiz! */
|
||
a = pv[i] /* geçersiz! */
|
||
|
||
void göstericileri (genel olarak void adresleri) artırıp eksiltemeyiz. Çünkü onların türleri olmadığı için içerisindeki adreslerin sayısal bileşenlerinin
|
||
ne kadar artıp eksileceği belli değildir.
|
||
|
||
O zaman void bir gösterici ne işe yarar?
|
||
|
||
void bir göstericiye herhangi bir türden adres doğrudan atanabilir. Bunun nedeni zaten void göstericilerin zararlı bir duruma yol açamamasıdır. Tabii void
|
||
göstericilere adres olmayan bir bilgi atayamayız. Örneğin:
|
||
|
||
int a;
|
||
double b;
|
||
char c;
|
||
void *pv;
|
||
|
||
pv = &a; /* geçerli */
|
||
pv = &b; /* geçerli */
|
||
pv = &c; /* geçerli */
|
||
|
||
C'de void bir adres de herhangi bir türden göstericiye doğrudan atanabilmektedir. Yani yukarıdakinin tersi de C'de geçerlidir. Örneğin:
|
||
|
||
void *pv;
|
||
int *pi;
|
||
double *pd;
|
||
|
||
pi = pv; /* geçerli */
|
||
pd = pv; /* geçerli */
|
||
|
||
Yani C'de void bir göstericiye herhangi türden bir adres atanabileceği gibi void bir adres de herhangi bir türden göstericiye atanabilmektedir.
|
||
|
||
void bir adresin başka bir göstericiye atanması C'de serbest olsa da C++'ta yasaklanmıştır. Yani C++'ta yine void göstericiye herhangi bir türden
|
||
adres atanabilir. Ancak void bir adres herhangi bir türden gösteriye atanamaz. C++'ta bunun yasaklanmasının nedeni aşağıdaki gibi "arkadan dolaşma"
|
||
yönteminin bertaraf edilmesi içindir:
|
||
|
||
int *pi;
|
||
char *pc;
|
||
void *pv;
|
||
|
||
pv = pi; /* hem C'de hem C++'ta geçerli */
|
||
pc = pv; /* C'de geçerli ama C++'ta geçersiz! */
|
||
|
||
Brada aslında pc = pi işlemi yapılmıştır. Ancak bu işlem C'de tamamen yasal bir kılıfa uydurulmuştur. İşte C++ bu duruma itiraz etmiş, void bir adresin
|
||
başka türden bir göstericiye atanmasını geçersiz hale getirmiştir.
|
||
|
||
Madem ki C++'ta void bir adres türü belirli bir göstericiye C'deki gibi atanamıyor. O halde C++'ta void bir adres türü belirli göstericiye tür dönüştürme operatörü ile
|
||
atanmalıdır. Örneğin:
|
||
|
||
int *pi;
|
||
char *pc;
|
||
void *pv;
|
||
|
||
pv = pi; /* hem C'de hem C++'ta geçerli */
|
||
pc = pv; /* C'de geçerli, C++'ta geçersiz */
|
||
pc = (char *)pv; /* C'de de geçerli C++'ta da geçerli */
|
||
|
||
C programcılarının bazıları void bir adresi türü belirli bir göstericiye atarken de gerekmediği halde C++ uyumunu korumak için tür dönüştürmesi yapmaktadır.
|
||
Biz de kursumuzda aslnda C'de gerekmediği halde void bir adresi C++ uyumunu da korumak için türü belirli bir göstericiye tür dönüştürme operatörü ile atayacağız.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
41. Ders 01/11/2022 - Sali
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
void göstericilere neden gereksinim duyulduğuna ilişkin iyi bir örnek memcpy fonksiyonudur. memcpy fonksiyonu prototipi <string.h> içerisinde olan
|
||
standart bir C fonksiyonudur. Fonksiyon bir adresten diğer bir adrese koşulsuz n byte kopyalamaktadır. memcpy fonksiyonu strcpy fonksiyonunu çağrıştırıyorsa da
|
||
ondanm farklıdır. memcpy fonksiyonu null karakter görünce durmaz. Koşulsuz n byte kopyalar. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||
int b[10];
|
||
|
||
memcpy(b, a, 40);
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", b[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada a adresinden itibaren b adresine 40 byte kopyalanmıştır. Eğer sistemimizde bir int 4 byte uzunluktaysa bu durum a dizisinin hepsisinin b'ye
|
||
kopyalanacağı anlamına gelir. Biz memcpy fonksiyonuyla double bir diziyi de, long bir diziyi de aynı biçimde kopyalayabilirdik. Pekiyi o zaman
|
||
memcpy fonksiyonun parametresi hangi türden göstericidir? İşte memcpy fonksiyonunun parametreleri her türden adresi kabul ettiğine göre void gösterici
|
||
olmalıdır. memcpy fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
void *memcpy(void *dest, const void *source, size_t n);
|
||
|
||
Buradaki const anahtar sözcüğünü henüz görmedik. Fonksiyon ikinci parametresiyle belirtilen adreten başlayarak birinci parametresiyle belirtilen adrese
|
||
üçüncü parametresiyle belirtilen miktarda koşulsuz byte kopyalar. Fonksiyon birinci parametresiyle belirtilen adresin aynısına geri döner.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||
int b[10];
|
||
double c[10] = {1.1, 2.2, 3.3, 4.4, 5.5, 6.6, 7.7, 8.8, 9.9, 10.10};
|
||
double d[10];
|
||
|
||
memcpy(b, a, 40); /* int 4 byte olmalı */
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", b[i]);
|
||
printf("\n");
|
||
|
||
memcpy(d, c, 80); /* double 8 byte olmalı */
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%f ", d[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
s adresindeki yazıyı d adresine kopyalayacak olalım. Bunu strcpy fonksiyonu ile yaparız:
|
||
|
||
strcpy(d, s);
|
||
|
||
Bu işlemi memcpy fonksiyonu ile yapmak istesek fonksiyonu şöyle çağırmamız gerekir:
|
||
|
||
memcpy(d, s, strlen(s) + 1);
|
||
|
||
Buradaki "+ 1" null karakteri de kopyalamak için gerekmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[100];
|
||
|
||
memcpy(d, s, strlen(s) + 1);
|
||
puts(d);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi memcpy fonksiyonunu nasıl yazabiliriz? void göstericler * ve [] operatörleriyle kullanılamazlar, artırılıp eksiltilemezler. O zaman
|
||
bizim fonksiyon içerisinde bu void adresleri belli bir türden adrese dönüştürüp işleme sokmamız gerekir. Tabii bunun için en uygun tür char türüdür. Örneğin:
|
||
|
||
void *mymemcpy(void *dest, void *source, size_t n)
|
||
{
|
||
char *pcdest = (char *)dest; /* C'de dönüştürme gerekmez, C++'ta gerekir */
|
||
char *pcsource = (char *)source; /* C'de dönüştürme gerekmez, C++'ta gerekir */
|
||
|
||
for (size_t i = 0; i < n; ++i)
|
||
pcdest[i] = pcsource[i];
|
||
|
||
return dest;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void *mymemcpy(void *dest, void *source, size_t n)
|
||
{
|
||
char *pcdest = (char *)dest; /* C'de dönüştürme gerekmez, C++'ta gerekir */
|
||
char *pcsource = (char *)source; /* C'de dönüştürme gerekmez, C++'ta gerekir */
|
||
|
||
for (size_t i = 0; i < n; ++i)
|
||
pcdest[i] = pcsource[i];
|
||
|
||
return dest;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||
int b[10];
|
||
|
||
mymemcpy(b, a, 40);
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", b[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
memcpy fonksiyonu ile çakışık blok kopyalaması "tanımsız davranışa (undefined behavior)" yol açmaktadır. Çakışık blok kopyalaması dizi kaydırma (expand)
|
||
ve sıkıştırma (shrink) işlemlerinde sıkça kullanılmaktadır. Örneğin elimizde 10 elemanlı int türden bir a dizisi olsun. Bunun 9 elemanı dolu olsun:
|
||
|
||
1 2 3 4 5 6 7 8 9 ?
|
||
|
||
Bizim amacımız dizinin başına bir değer insert etmek olsun. Bunun için bizim 0'dan sonrasını bir int'lik kaydırmamız gerekir:
|
||
|
||
? 1 2 3 4 5 6 7 8 9
|
||
|
||
Programcı bu işlemi memcpy kullanarak aşağıdaki gibi yapmak isteyebilir:
|
||
|
||
memcpy(a + 1, a, 9 * 4);
|
||
|
||
Artık dizinin ilk elemanını kaydırdığımıza göre ilk elemana insert işlemini yapabiliriz:
|
||
|
||
a[0] = 100;
|
||
|
||
Ancak bu işlemde bir hata vardır. Eğer memcpy kopyalamayı baştan itibaren yaparsa bu durumda kopyalamayı yaparken aynı zamanda dizinin sonraki elemanlarını
|
||
bozar. İşte memcpy'nin çakışık bloklarda sonraki elemanları bozmadan kopyalama yapan memmove isimli bir versiyonu da vardır. memmove fonksiyonun memcpy'den tek
|
||
farkı çakışık bloklarda kopyalamayı duruma göre baştan duruma göre sondan başlatarak problemsiz yapabilmeisidir. Bu nedenle yukarıdaki gibi
|
||
insert, delete işlemlerinde memcpy yerine memmove fonksiyonu kullanılmalıdır. memmove fonksiyonunun orijinal prototipi memcpy gibidir:
|
||
|
||
void *memmove(void *dest, const void *source, size_t n);
|
||
|
||
Pekiyi madem ki memmove fonksiyonu aslında memcpy fonksiyonunu işlevsel olarak kapsamaktadır neden iki ayrı fonksiyon bulundurulmuştur? İşte memmove
|
||
fonksiyonunun çakışık bloklarda düzgün davranabilmesi için işin başında source ve dest adesleri kontrol etmesi gerekmektedir. Bu kontrol memcpy fonksiyonunda
|
||
yapılmamaktadır. Bu durumda memmove fonksiyonun çakışık olmayan bloklarda memcpy fonksiyonundan daha yavaş olduğu söylenebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Örneğin biz memmove fonksiyonunu kullanarak int bir dizide insert işlemi yapan genel bir fonksiyon yazmak isteyelim. int türünün de 4 byte olduğunu varsayalım.
|
||
Fonksiyonu şöyle yazabiliriz:
|
||
|
||
void insert(int *pi, size_t size, size_t index, int val)
|
||
{
|
||
memmove(pi + index + 1, pi + index, (size - index) * 4);
|
||
pi[index] = val;
|
||
}
|
||
|
||
Burada pi dizinin başlangıç adresini belirtir. size dizideki dolu eleman sayısıdır. index insert edilecek elemanın indeks numarasıdır. val ise insert edilecek
|
||
değeri belirtir. Bizim memmove fonksiyonu ile aslında her elemanı bir kaydırmamız gerekir. Sonra ilgili index pozisyonunu açıp oraya insert işlemi yapmamız gerekir.
|
||
Tabii işlem sonucunda artık dizinin dolu olan eleman sayısı 1 artacaktır. Bu tür durumlarda memcpy yerine memmove fonksiyonunu kullanmalısınız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
void insert(int *pi, size_t size, size_t index, int val)
|
||
{
|
||
memmove(pi + index + 1, pi + index, (size - index) * 4);
|
||
pi[index] = val;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[100] = {1, 2, 3, 4, 5};
|
||
|
||
insert(a, 5, 0, 100);
|
||
|
||
for (int i = 0; i < 6; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
42. Ders 03/11/2022 - Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
void göstericilerin kullanımına diğer bir örnek de memset fonksiyonudur. Bu fonksiyonun da prototipi <string.h> içerisindedir. memset fonksiyonu
|
||
belli bir adresten itibaren belli bir miktarda belli bir byte'ı doldurmak için kullanılır. Örneğin biz bellekte belli bir yeri 0'larla doldurmak
|
||
isteyebiliriz. Doldurulacak yerin türünün bir önemi yoktur. Orası memset fonksiyonu tarafından bir byte yığını olarak ele alınmaktadır. Fonksiyonun
|
||
prototipi şöyledir:
|
||
|
||
void *memset(void *ptr, int ch, size_t n);
|
||
|
||
Fonksiyonun ikinci parametresindeki değer 0 il2 255 arasında olmalıdır. Fonksiyon birinci parametresiyle belirtilen adresten başlayarak üçüncü parametresiyle belirtilen miktarda byte'ı
|
||
ikinci parametresiyle belirtilen değerle doldurur. Eğer ikinci parametredeki değer büyük ise onun düşük anlamlı byte değeri işleme sokulur.
|
||
|
||
Bazı derleyicilerde eklenti olarak strset isimli bir fonksiyon da bulundurulmaktadır. Bu fonksiyon bir yazıyı null
|
||
karakter görene kadar belli bir karakterle doldurmaktadır. Ancak strset fonksiyonu bir standart C fonksiyonu değildir.
|
||
s
|
||
Aşağıdaki örnekte int türünün 4 byte olduğu bir sistemde 10 elemanlı int bir dizinin tüm byte'ları sıfırlanmıştır. Dolayısıyla dizi elemanları da
|
||
sıfırlanmış olmaktadır. Tabii bir int dizinin tüm byte'larına örneğin 1 gibi bir değer atarsak biz dizi elemanlarını 1'lemiş olmayız. Çünkü tüm byte'ları 1 olan bir
|
||
int değer aslında farklı değerdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10];
|
||
|
||
memset(a, 0, 10 * 4);
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
memcmp fonksiyonu iki adres alır, o adreslerden itibaren karşılıklı n byte'ı bçimde karşılaştırır. strcmp fonksiyonuna
|
||
benzemekle birlikte null karakter görünce işlemini bitirmez. Bu fonksiyon yazıları değil bellekteki byte yığınlarını
|
||
karşılaştırmak için kullanılmaktadır. Bu fonksiyon için null karakterin de sıradan bir byte olduğuna dikkat ediniz.
|
||
Fonksiyonun prototipi yine <string.h> dosyası içerisidedir. Orijinal prototipi şöyledir:
|
||
|
||
int memcmp(const void *ptr1, const void *ptr2, size_t n);
|
||
|
||
Fonksiyon ilk iki paraetresiyle aldığı adresten itibaren karşılıklı byte'ları karşılaştırır. İlk eşit olmayan byte'ın
|
||
durumuna göre eğer birinci adresteki byte yığını ikinci adresteki byte yığınından büyükse pozitif bir değere, küçükse
|
||
negatif herhangi bir değere ve eşitse sıfır değerine geri döner. İki byte yıpınının eşit olması için buların n byte'ının
|
||
da eşit olması gerekir. Uygulamada memcmp fonksiyonu bellekte belli byte kalıplarını bulmak için sıkça kullanılmaktadır.
|
||
|
||
Aşağıdaki örnekte int türünün 4 byte olduğu bir sistemde iki int dizinin içeriklerinin aynı olup olmadığı memcmp
|
||
fonksiyonuyla tespit edilmeye çalışılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[5] = {1, 2, 3, 4, 5};
|
||
int b[5] = {1, 2, 3, 4, 5};
|
||
int result;
|
||
|
||
result = memcmp(a, b, 5 * 4);
|
||
|
||
printf(result ? "icerikler ayni degil\n" : "icerikler ayni\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yazılar üzerinde işlem yapan bazı fonksiyonlar bazı derleyicilerde aynı isimle bulunduğu için kişiler onları standart
|
||
C fonksiyonu sanabilmektedir. Aşağıdaki fonksiyonlar Microsoft ve Borland derleyicilerinde bulunmakla birlikte gcc
|
||
derleyicilerinde bulunmamaktadır. Bunlar standart C fonksiyonları değillerdir:
|
||
|
||
char *stricmp(const char *s1, const char *s2);
|
||
char *strrev(char *str);
|
||
char *strset(char *str, int ch);
|
||
char *strupr(char *str);
|
||
char *strlwr(char *str);
|
||
|
||
stricmp büyük harf küçük harf duyarlılığı olmadan yazı karşılaştırmasını yapmaktadır. strrev bir yazıyı ters yüz
|
||
etmektedir. strset bir yazının karakterlerini başka bir karakterle doldurmaktadır. strupr ve strlwr fonksiyonları
|
||
yazının tüm karakterlerini büyük harf ve küçük harfe dönüştürmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Göstericiler potansiyel olarak bellekte herhangi bir yeri gösterebilirler. Ancak göstericiler yoluyla bizim tarafımızdan tahsis edilmemiş olan alanlara erişmeye
|
||
çalışmak "tanımsız davranışa (undefined behavior)" yol açmaktadır. Örneğin:
|
||
|
||
char *pc = (char *)0x1FC020;
|
||
|
||
Burada pc göstericisine rastgele bir adres atanmıştır. Bu işlemde henüz bir sorun yoktur. Ancak bu gösterici yoluyla o adrese erişilmeye çalışılırsa
|
||
tanımsız davranış oluşur:
|
||
|
||
*pc = 0; /* rastgele bir adrese erişiliyor */
|
||
|
||
Burada bu rastgele adese veri aktarmak değil erişmek de tanımsız davranışa yol açar.
|
||
|
||
Tanımlama yoluyla tahsis edilmiş alanlar bizim için ayrılan alanlardır. Biz bu alanlara herhangi bir biçimde erişebiliriz. Ancak tanımlama yoluyla
|
||
tahsis etmediğimiz, bizim için ayrılmayan rastgele alanlara göstericiler yoluyla erişmemeiz gerekir. Örneğin:
|
||
|
||
char ch;
|
||
char *pc = &ch;
|
||
|
||
Burada *pc erişiminin hiçbir sorunu yoktur. Çünkü aslında eriştiğimiz yer zaten bizim için ayrılan bir yerdir. Örneğin:
|
||
|
||
int a[10];
|
||
int *pi = a;
|
||
|
||
Burada pi göstericisi ile biz int dizinin 10 elemanına da erişebiliriz. Ancak bu dizinin ötesine ya da gerisine erişirsek bu durum tanımsız davranışa yol açar.
|
||
|
||
Pekiyi biz bir göstericiye rastgele bir adres atayarak bellekte istediğimiz bir yere erişmeye çalışırsak ne olur? Bu durum çalıştığımız sistemde göre değişebilir.
|
||
Örneğin bir mikrodenetleyici sisteminde oradaki verileri bozmuyorsak bir şey olmayabilir. Ancak Windows, Linux, macOS gibi sistemlerde kullanılan mikroişlemcilerin
|
||
"koruma mekanizması (protection mechanism)" vardır. Bu mekanizma sayesinde bir program zaten kendi alanının dışına çıkıp başka programların alanlarına erişmeye
|
||
çalışırsa mikroişlemci durumu tespit eder bunu işletim sistemine bildirir. İşletim sistemi de ceza olarak programı sonlandırır. Ancak her sistemde
|
||
koruma mekanizması olmak zorunda değildir. Tabii bir sistemde koruma mekanizması olsa bile bir program yanlışlıkla kendini de bozabilir. Bu nedenle
|
||
genel olarak tahsis edilmemiş alanlara erişim tanımsız davranışa yol açmaktadır.
|
||
|
||
Buada bir analoji olarak şöyle düşünebiliriz: Bizim parmağımız bir gösterici olsun. Bizim başkasının evini göstermemizde bir sakınca yoktur. Ancak oraya
|
||
erişmemiz bir suç oluşturur. Ancak biz parmağımızla kendi evimizi gösteriyorsak oraya erişebiliriz. Çünkü evimiz zaten bizim için ayrılmış bir yerdir.
|
||
Biz kendi arazimizi parmağımızla gösteriyor olabiliriz. Oraya erişmemizde sakınca yoktur. Ancak arazinin bizim sınırlarının ötesindeki kısmına
|
||
erişmemeliyiz. Bu durumu bir dizinin bizim için ayrılan kısmının ötesine erişmeye benzetebiliriz.
|
||
|
||
Ancak bazı sistemlerde bazı alanlar herkesin erişimi için ayrılmış olabilir. Bu tür sistemlerde bu alanlara erişmekte bir sorun ortaya çıkmaz.
|
||
Tabii C standartları genel olarak tahsis edilmemiş alanlara erişimin tanımsız davranışa yol açacağını belirtmiş olsa da bu tür sistemlerde biz o sisteme
|
||
özgü olarak bu alanlara erişebiliriz. (Bu durumu parmağımızla bir parkı gösterme durumuna benzetebiliriz. Park herkesin kullanımı için ayrılmıştır. Dolayısıyla
|
||
dolayısıyla park bize tahsis edilmemiş olsa da oraya erişmekte bir sakınca olmaz.)
|
||
|
||
Gösterici hatası göstercilerle tahsis edilmemiş bellek alanlarına erişmekler oluşan hatalardır. Gösterici hatalarının tipik olarak ortaya çıkmasının
|
||
çeşitli biçimleri vardır. Burada bu biçimler üzerinde duracağız.
|
||
|
||
- İlkdeğer verilmemiş göstericilerin gösterdiği yerlere erişmek tipik gösterici hatalarındadır. Bir yerel göstericinin içerisinde çöp değer, global
|
||
bir gsötericinin içerisinde NULL adres bulunur. Bu gösterici * ya da [] operatörleriyle kullanırsak tanımsız davranış oluşur. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *pc; /* pc'nin içerisinde rastgele bir adresin sayısal bileşeni var */
|
||
|
||
*pc = 0; /* tanımsız davranış! rastegele bir yere erişiliyor! */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Örneğin:
|
||
|
||
char *s;
|
||
|
||
gets(s);
|
||
|
||
Burada gets fonksaiyonu klavyeden (stdin dosyasından) girilen karakterleri s göstericisinin içerisindeki adresten itibaren yerleştirir. s göstericisinin
|
||
içerisinde rastgele bir adres olduğuna göre klavyeden girilenler rastgele bir yere yerleştirilecektir. Ancak örneğin:
|
||
|
||
char s[1024];
|
||
|
||
gets(s);
|
||
|
||
Burada klavyeden (stdin dosyasından) girilen karakterler tahsis edilmiş olan diziye yerleştirilmektedir.
|
||
|
||
- Bir göstericinin içerisine rastgele bir adres yerleştirip o bölgeye erişmememiz gerekir. Örneğin bellekteki belli bir yerde ne var diye
|
||
o bölgeyi yazdırmaya çalışmamalıyız:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char *pc = (unsigned char *) 0x123456; /* Bu adresteki nesneleri biz tahsis etmemişiz! dikkat */
|
||
|
||
for (int i = 0; i < 16; ++i)
|
||
printf("%02X ", pc[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
- Bir dizi için bizim belirttiğimiz miktarda yer ayrılmaktadır. Göstericilerle diziler taşırılırsa tanımsız davranış oluşur. Örneğin:
|
||
|
||
int a[10];
|
||
|
||
for (int i = 0; i <= 10; ++i) /* dikkat dizi taşırılmış! gösterici hatası! */
|
||
a[i] = 0;
|
||
|
||
- Dizilerin taşırılması standart C fonksiyonlarıyla da yanlışlıkla yapılabilmektedir. Örneğin:
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[] = "izmir";
|
||
|
||
strcat(d, s); /* dikkat dizi taşırılmış! */
|
||
puts(d);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char d[6];
|
||
|
||
strcpy(d, s); /* dikkat dizi taşırılmış! */
|
||
puts(d);
|
||
|
||
return 0;
|
||
}
|
||
|
||
- NULL adrese dolaylı bir biçimde erişmeye çalışmak da çok karşılaşılan gösterici hatalarındadır. Örneğin strchr fonksiyonuyla bir yazı içerisinde bir karakteri
|
||
bulup onu başka bir karakterle yer değiştirmek isteyelim. Pekiyi ya karakteri bulamazsak?
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char *str;
|
||
|
||
str = strchr(s, 'x'); /* dikkat! kontrol yapılmamış! */
|
||
*str = 'y';
|
||
|
||
return 0;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
43. Ders 08/11/2022 - Sali
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Diziler ve göstericiler bazen birbirlerine karıştırılabilmektedir. Bu iki kavram arasındaki benzerlikler ve farklılıklar şunlardır:
|
||
|
||
- Hem dizi isimleri hem de göstericller birer adres belirtir.
|
||
- Göstericiler nesne belirtmektedir: Yani biz göstericinin içerisine bir şeyler yerleştirebiliriz. Ancak dizi isimleri nesne belirtmez.
|
||
Bir dizi ismine bir şey yerleştiremeyiz.
|
||
- Göstericilerin gösterdiği yer tahsis edilmiş olmak zorunda değildir. Yani biz bir göstericiye bir adres atadığımızda adres rastgele bir adres
|
||
olabilir. Dolayısıyla bu adresteki alan bizim tarafımızdan tahsis edilmemiş olabilir. Ancak dizi isimleri ike belirtilen adresten itibaren bizim
|
||
için ayrılmış olan bir alan vardır.
|
||
|
||
Örneğin:
|
||
|
||
int a[] = {10, 20, 30};
|
||
int *pi = a;
|
||
|
||
Burada biz ++a gibi bir ifade oluşturamayız. Ancak ++pi gibi bir ifade oluşturabiliriz. a bu dizinin tamamını temsil
|
||
etmektedir. Bu a ismi program içerisinde kullanıldığında derleyici dizinin başlangıç adresini adreta bir adres sabiti
|
||
gibi kullanır. Burada a bir nesne belirtmez ancak pi bir nesne belirtmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir gösterici ne kadar yer kaplar? Göstericinin kapladığı yer onun türüyle ilgili midir? Bir göstericinin kendi uzunluğu genel olarak göstericinin
|
||
türü ile ilgili değildir. Yani bir sistemde int türden gösterici ile long türden gösterici arasında uzunluk bakımından bir fark yoktur. Göstericinin
|
||
türü onun gösterdeği yer ile ilgilidir. Bir sistemde göstericilerin byte uzunluğu o sistemin teorik bellek kapasitesi ile ilgildir. Örneğin 32 bitlik
|
||
işlemcilerde belleğin maksimum uzunluğu 4 GB olabilir. 2 üzeri 32 4GB'dir. O halde 32 bit bir sistemde tipik olarak göstericiler 4 byte yer kaplarlar.
|
||
Ancak 64 bit sistemlerde teorik bellek uzunluğu 16EB'dir. Bu durumda 64 bit sistemlerdeki göstericiler de 8 byte uzunlukta olur.
|
||
|
||
Ancak işletim sistemlerinin önemli bir bölümünde "geriye doğru uyum (backward compatibility)" bulunmaktadır. 64 bit bir işletim sistemi 32 bit prosesörler için
|
||
yazılmış programları da sanki sistemde 32 bitlik prosesör varmış gibi çalıştırabilmektedir.
|
||
|
||
C standartlarında çeşitli gerekçelerle "türleri farklı olan göstericilerin aynı uzunlukta olduğuna yönelik" bir ibare yoktur. Yani burada açıklanan durum
|
||
sistemlerdeki tipik durumdur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
* ve [] operatörlerinin ++ ve -- operatörleriyle işleme sokulmasında programcılar tarafından bazı tereddütler oluşabilmektedir. Biz de bu tereddütleri
|
||
gidermek için bazı açıklamalar yapacağız.
|
||
|
||
1) [] operatörünün ++ ve -- operatörleriyle kullanımı
|
||
|
||
a) ++p[i] ifadesinde p[i] bir artırılmaktadır. Benzer biçimde a = p[i]++ ifadesinde de p[i]'nin değeri a'ya atanıp p[i] bir artırılacaktır.
|
||
|
||
b) (++p)[i] ifadesinde önce p bir artırılır. Artırılmış adresten i ilerinin içeriğine erişilir. Tabii burada p bir dizi ismi olamaz. Gösteri olmak zorundadır.
|
||
|
||
c) p[++i] gibi bir ifadede önce i bir artırılır sonra p adresindne artırılmış indeksin belrriği yere erişilir.
|
||
|
||
d) p[i++] Burada i bir artılır ancak [] operatörüne i'nin artırılmamış değeri sokulur. Yani p[i] değerine erişilip i bir artırılacaktır. Örneğin:
|
||
|
||
int a[3];
|
||
int i = 0;
|
||
|
||
a[i++] = 10;
|
||
a[i++] = 20;
|
||
a[i++] = 30;
|
||
|
||
e) p++[i] ifadesinde p bir artırılır (tabii p'nin bir gösterici olması gerekir, p bir dizi ismi olamaz) ancak artırılmamış p'den i ilerinin içeriğine erişilr.
|
||
|
||
2) * operatörü ile ++ ve -- operatörlerinin kullanımı
|
||
|
||
a) ++*p ifadesinde önce *p'ye erişilir sonra *p bir artırılır. Yani ifade *p = *p + 1 anlamına gelir.
|
||
|
||
b) a = *p++ gibi bir ifade en çok tereddüt edilen ifadelerin başında gelmektedir. Burada ++'ın operandı *p değildi, p'dir. Dolayısıyla burada a'ye *p
|
||
atanır ancak p de bir artırılmış olur. Bunu şöyle de ifade edebiliriz. Burada p bir artırılır ancak * işlemine p'nin artırılmamış hali sokulur.
|
||
Bu ifade programlarda çok sık karşımıza çıkar. Örneğin:
|
||
|
||
char *mystrcpy(char *dest, char *source)
|
||
{
|
||
char *temp = dest;
|
||
|
||
while ((*dest++ = *source++) != '\0')
|
||
;
|
||
|
||
return temp;
|
||
}
|
||
|
||
Burada *dest++ = *source++ ifadesinde ++ operatörleri sonek durumundadır. Dolayısıyla source ve dest bir artırılacaktır. Ancak * işlemine bunlarn artmamış değerleri
|
||
dokulacaktır. Yani burada aslında *dest = *source işlemi yapılıp source ve dest bir artırılmış gibidir. Karşılaştırma işlemine atanan değer sokulmaktadır.
|
||
Dolayısıyla null karakter de önce hedefe atanacak sonra döngüden çıkılacaktır.
|
||
|
||
c) a = *++p ifadesinde önce p bir artırılır sonra artmış adresin içeriği a'ya atanır.
|
||
|
||
d) a = (*p)++ ifadesinde ++ operatörünün operandı artık p değildir, *p'dir. Dolayısıyla burada *p bir artırılır ancak sonraki işlem olan atamaya *p'nin
|
||
artmamış değeri sokulur. Başka bir deyişle burada *p önce a'ya atanır sonra *p bir artırılır.
|
||
|
||
Aşağıdaki döngüden çıkıldığında str göstericisi nereyi göstermektedir?
|
||
|
||
while (*str++ != '\0)
|
||
;
|
||
|
||
Burada str döngüden çıkıldığında null karakterden bir sonrayı gösterecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de & operatör bir nesnenin adresini elde eder. Ancak elde edilen ürün bir nesne belirtmez. Yani ++&a gibi bir işlem geçersizdir. C'de ++ ve -- operatörleri de
|
||
nesne belirtmez. Bu nedenle aşağıdaki iki ifade de geçersizdir:
|
||
|
||
++a = 10; /* geçersiz! */
|
||
a++ = 10; /* geçersiz! */
|
||
|
||
++a ifadesinde a bir artırılmıştır ancak elde edilen ürün a değildir, a'nın artmış değeridir. Benzer biçimde a++ ifadsinde de durum böyledir.
|
||
Ancak C++'ta bu konuda bir farklılık vardır. C++'ta öncek ++ ve -- operatörleri nesnenin kendisini üretmektedir. Dolayısıyla ++a = 10 gibi bir ifade
|
||
her ne kadar mantıksal bakımdan anlamsız olsa da geçerlidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de char türü dışında hiçbir türün kaç byte uzunlukta olduğu önceden bilinmemektedir. Örneğin farklı sistemlerde int türü farklı uzunluklarda olabilmektedir.
|
||
Ancak bu durum taşınabilirlik bakımından problem doğurmaktadır. Örneğin:
|
||
|
||
int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
|
||
int b[10];
|
||
|
||
Biz a dizisinin içeriğini memcpy fonksiyonu ile b dizisine kopyalamak isteyelim. Pekiyi fonksiyonun üçüncü parametresi için ne girmeliyiz?
|
||
|
||
memcpy(b, a, 40);
|
||
|
||
Bu kod int türünün 4 byte uzunlukta olduğu varsayımı ile yazılmıştır. Eğer int türü ilgili sistemde 8 byte uzunluktaysa biz dizinin ancak yarısını kopyalayabilriz.
|
||
Eğer int türü 2 byte uzunluktaysa b dizisi taşacaktır.
|
||
|
||
sizeof operatörü bir türün o andaki sistemde kaç byte yer kapladığını anlamakta kullanılan bir operatördür. sizeof operatörünü gören derleyici
|
||
koda size_t türünden sabit bir sayı yerleştirmektedir. sizeof operatörünün üç kullanım biçimi vardır:
|
||
|
||
1) sizeof <ifade> ya da sizeof (<ifade>)
|
||
2) sizeof (<tür_ismi>)
|
||
3) sizeof <dizi ismi> ya da sizeof (<dizi ismi>)
|
||
|
||
sizeof tek opereand'lı önek bir operatördür. sizeof operatörünün operand'ı bir ifade ise bu ifade paranteze alınabilir ya da alınmayabilir. Ancak
|
||
sizeof opereatörünün operand'ı bir tür ismi ise bu tür isminin paranteze alınması zorunludur. sizeof operatörün ün operand'ı bir dizi ismi de olabilir.
|
||
sizoef operatörünün operand'ı bir ifade ise o ifadenin değeri hesaplanmaz. Yalnızca türüne bakılır. sizeof bu durumda o ifadenin türünün kaç byte yer
|
||
kapladığına ilişkin bir sabit değer üretir. Örneğin:
|
||
|
||
size_t result;
|
||
|
||
result = sizeof(10 + 2.);
|
||
|
||
Burada sizeof operatörü 10 + 2.0 işlemini yapmaz. Bu ifadenin double türünden olduğunu anlarve double türünün ilgili sistemde byte uzunluğuna ilişkin
|
||
sabit bir değer üretir. sizeof operatörü doğrudan bir tür ismi alabilir. Ancak bu durumda tür isminin parantez içerisinde belirtilmesi gerekir. Örneğin:
|
||
|
||
result = sizeof (int);
|
||
|
||
sizeof operatörünün operand'ı bir dizi ismi olursa sizeof o dizinin tamamının bellekte kaç byte yer kapladığına ilişkin bir değer üretir. Örneğin:
|
||
|
||
int a[10];
|
||
size_t result;
|
||
|
||
result = sizeof a;
|
||
|
||
Burada int 4 byte uzunlukta ise sizeof 40 değerini üretecektir.
|
||
|
||
sizeof operatörü öncelik tablosunun ikinci düzeyinde sağdan sola grupta bulunmaktadır:
|
||
|
||
() [] Soldan-Sağa
|
||
+ - ++ -- ! & * sizeof (tür) Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
Bu durumda sizeof operatörünün sağındaki ifadenin paranteze alınp alınmaması arasında fark oluşmaktadır. Örneğin:
|
||
|
||
result = sizeof 10 + 20.0;
|
||
|
||
İ1: sizeof 10 ===> 4 (int'in 4 byte olduğunu varsayalım)
|
||
İ2: İ1 + 20.0
|
||
İ3: result = İ2
|
||
|
||
Şimdi ifadeyi paranteze alalım:
|
||
|
||
result = sizeof (10 + 20.0)
|
||
|
||
Artık burada önce parantez içi ele alınacaktır. Parantez içinin türü double olduğu için sizeof muhtemel olarak 8 değerini üretecektir. Aşağıdaki kullanım
|
||
geçersizdir:
|
||
|
||
result = sizeof int; /* geçersiz! */
|
||
|
||
Burada int paranteze alınmalıydı:
|
||
|
||
result = sizeof (int); /* geçerli */
|
||
|
||
Bir göstericinin byte uzunluğu değişik biçimlerde elde edilebilir:
|
||
|
||
int *pi;
|
||
|
||
result = sizeof (pi);
|
||
result = sizeof (int *);
|
||
|
||
sizeof opereatörünün yanındaki ifadenin işletilmeyeceğine dikkat ediniz. Örneğin:
|
||
|
||
result = sizeof (a = 10);
|
||
|
||
Burada atama yapılmayacaktır. Bu ifade sizeof a ile eşdeğerdir.
|
||
|
||
Dizi ismi adres belirtmektedir. Ancak istisna olarak sizeof operatörüne dizi ismi uygulanırsa adresin kaç byte yer kapladığı değil dizinin
|
||
toplam kaç byte yer kapladığı bilgisi elde edilir.
|
||
|
||
Bişr fonksiyonun ismine sizeof operatörü uygulanamaz. Yani fonksiyonların kaç byte yer kapladığı sizeof operatörü
|
||
ile elde edilememektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10];
|
||
int *pi;
|
||
char *pc;
|
||
size_t result;
|
||
|
||
result = sizeof 100 + 2;
|
||
printf("%zd\n", result); /* 6 */
|
||
|
||
result = sizeof (100 + 2);
|
||
printf("%zd\n", result); /* 4 */
|
||
|
||
result = sizeof(pi);
|
||
printf("%zd\n", result); /* 8 */
|
||
|
||
result = sizeof(pc);
|
||
printf("%zd\n", result); /* 8 */
|
||
|
||
result = sizeof(a);
|
||
printf("%zd\n", result); /* 40 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte sizeof operatörü memcpy fonksiyonunda taşıanbilirliği sağlamak için kullanılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10] = {10, 20, 30, 40, 50, 60, 70, 80, 90, 100};
|
||
int b[10];
|
||
|
||
/* memcpy(b, a, sizeof a); */
|
||
memcpy(b, a, sizeof(int) * 10);
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", b[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
sizeof bir operatör olduğu için bir sabit ifadesi oluşturmaktadır. Örneğin:
|
||
|
||
int g_a[10];
|
||
char g_b[sizeof g_a]; /* geçerli */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizinin eleman uzunluğunu derleme zamanı sırasında elde edebiliriz. Dizinin ismi a olmak üzere sizeof(a) / sizeof(*a) dizinin eleman uzunluğunu verecektir.
|
||
Böylece biz dizinin eleman sayısı değişse bile onun uzunluğunu her zaman elde etmiş oluruz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[] = {10, 20, 30, 40, 50, 60, 70};
|
||
|
||
for (int i = 0; i < sizeof(a) / sizeof(*a); ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C99 ile birlikte C'ye "Değişken Uzunlukta Diziler (Variable Length Arrays)" denilen bir özellik de eklenmiştir. Bu
|
||
özellik bazıları tarafından eleştirilmektedir. Bu özellik C++'a hiçbir zaman sokulmamıştır. VLA yerel dizilerin
|
||
uzunluklarının sabit ifadesi biçiminde belirtilme zorunluğunun ortadan kaldırılmasına denilmektedir. Yani C99
|
||
ve sonrasında biz yerel diziler tanımlarken dizi uzunluğunu sabit ifadesi biçiminde belirtmek zorunda değiliz. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
int size;
|
||
|
||
scanf("%d", &size);
|
||
|
||
int a[size]; /* C90'da geçersiz, C++'ta geçerisiz ancak C99 ve ötesinde geçerli */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
Tabii global dizilerde bu durum mümkün değildir. Örneğin:
|
||
|
||
int g_size = 10;
|
||
int g_a[g_size]; /* C'nin tüm versiyonlarında geçersiz! */
|
||
|
||
Değişken uzunlukta dizilerin tahsis edilebilmesi için derleyici ek makine komutlarıyla stack üzerinde yer açmaktadır.
|
||
Ayrıca bu özelliğin C++'ta da geçerli olmadığına dikkat ediniz. Bu nedenle bu özelliği gereksiz biçimde kullanmamalısınız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İngilizce C standartlarında adres bilgisi için de adres tutan nesneler için de "pointer" sözcüğü kullanılmaktadır. Oysa biz adresi tür
|
||
olarak "adres bilgisi" biçiminde ifade ediyoruz. Biz Türkçe "gösterici (pointer)" terimini adres tutan nesneler için kullanıyoruz. Halbuki İngilizce'de
|
||
"pointer" sözcüğü adres kavramı için genel olarak kullanılmaktadır. C standartlarında address lafı yerine hep "pointer" sözcüğü kullanılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
44. Ders 10.11.2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Biz C'de ne zaman iki tırnak içerisinde bir yazı yazsak derleyici bu iki tırnak içerisindeki yazıyı char türünden statik ömürlü bir dizinin içerisine,
|
||
dizinin adresini de iki tırnak ifadesi yerine yerleştirir. Örneğin:
|
||
|
||
char *str;
|
||
|
||
str = "ankara";
|
||
|
||
Bu durumda derleyici önce "ankara" yazısını char türden bir diziye yerleştirir, sonuna null karakteri ekler sonra da bu dizinin başlangıç adresini
|
||
bu string ifadesinin bulunduğu yere yerleştirir. Böylece bu örnekte str göstericisi de içerisinde "ankara" yazısının bulunduğu char türden dizinin
|
||
adresini gösterecektir. C'de string'ler char türden bir adres belirtirler. Yani bir string'i ancak biz char türden bir göstericiye atayabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *str;
|
||
|
||
str = "ankara";
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de iki tırnak ifadesinin bu anlamda adres belirtmediği istisna tek bir durum vardır. char türden, unsigned char türden ya da signed char türden
|
||
bir diziye iki tırnak ile ilkdeğer verilirkenki iki tırnak ifadesi istisna bir durumdur ve bir adres belirtmez. Bu istisna durum iki tırnak içerisindeki
|
||
karakterlerin diziye tek tek yerleştirileceği anlamına gelmektedir. Örneğin:
|
||
|
||
char s[] = "ankara";
|
||
|
||
Buradaki "ankara" bir diziye yerleştirilip o adresi insert edilmez. Bu istisna bir durumdur. Bu durum "ankara" yazısının karakterlerinin tek tek
|
||
diziye yerleştirileceği anlamına gelir. Başka bir deyişle bu durum aşağıdaki ile eşdeğerdir:
|
||
|
||
char s[] = {'a', 'n', 'k', 'a', 'r', 'a', '\0'};
|
||
|
||
Ancak char türden bir göstericiye ilkdeğer verirkenki iki tırnak istisna durum değğildir, buradaki iki tırnak adres belirtmektedir. Örneğin:
|
||
|
||
char *str = "ankara";
|
||
|
||
Burada yine "ankara" yazısı char türden bir diziye yerleştirilir, dizinin adresi de string'in bulunduğu yere yerleştirilir.
|
||
|
||
Anımsanacağı gibi C'de yalnızca char denildiğinde derleyiciye bağlı olarak signed char ya da unsigned char anlaşılmaktadır. Ancak yine de C'de
|
||
bu üç tür birbirinden farklı türler kabul edilmektedir. Yani çalıştığımız derleyicide char denildiğinde default signed char anlaşılıyor olsa bile adres işlemlerinde
|
||
char ile signed char farklı türler kabul edilir. Bu durumda string'ler char türden adres belirttiğinde göre, ilgili sistemde char türü signed char anlamına gelse bile
|
||
biz bir string'i signed char türünden bir göstericiye yerleştiremeyiz. Örneğin:
|
||
|
||
signed char *str;
|
||
|
||
str = "ankara"; /* geçersiz! */
|
||
|
||
Burada char * türü signed char * türüne atanmıştır. Bu durum geçersizdir. Çünkü char ve signed char türü farklı türler kabul edilmektedir. Halbuki C standartlarında
|
||
biz iki tırnak ifadesiyle signed char, signed char ya da unsigned char türünden dizilere ilkdeğer verebiliriz:
|
||
|
||
char c[] = "ankara"; /* geçerli */
|
||
signed char s[] = "ankara"; /* geçerli */
|
||
unsigned char k[] = "ankara"; /* geçerli */
|
||
|
||
Fakat örneğin:
|
||
|
||
signed char *str;
|
||
|
||
str = "ankara"; /* geçersiz! */
|
||
|
||
Bir dizi ismine biz bir string'i atayamayız. Dizi isimleri nesne belirtmez. Örneğin:
|
||
|
||
char s[] = "ankara"; /* geçerli, dizi elemanlarına ilkdeğer veriliyor, istisna durum */
|
||
|
||
s = "izmir"; /* geçersiz! bir adres dizi ismine atanmaya çalışılıyor */
|
||
|
||
İlkdeğer verme tanımlamanın bir parçası olarak yapılan bir işlemdir. Bir değişkeni tanımladıktan sonra ona değer atamak
|
||
ilkdeğer verme anlamına gelmez. Biz char türden, signed char türden, unsigned char türden bir diziye iki tırnak ifadesiyle
|
||
ilkdeğer verebiliriz. Ancak daha sonra dizi isimlerine değer atayamayız. Dizi isimleri nesne belirtmemektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonun parametre değişkeni char türden bir gösterici ise biz fonksiyonu bir string ifadesiyle çağırabiliriz. Bu durumda fonksiyonun gösterici
|
||
parametresine string'in belrttiği yazının başlangıç adresi atanmış olur. Örneğin:
|
||
|
||
void foo(char *str) /* char *str = "ankara" */
|
||
{
|
||
/* ... */
|
||
}
|
||
...
|
||
foo("ankara"); /* geçerli */
|
||
|
||
Örneğin standart puts fonksiyonu char türden bir gösterici parametresine sahiptir. Biz bu fonksiyonu bir string ifadesiyle çağırabiliriz:
|
||
|
||
puts("ankara");
|
||
|
||
Anımsanacağı gibi dizi isimleri nesne belirtmemektedir. Adeta bunlar birer sembolik sabit gibi düşünülmelidir. Pekiyi bu durumda char türden bir diziye
|
||
ilkdeğer vermenin dışında bir yazı yerleştirmenin en pratik yolu nedir? Akla ilk gelen yöntem karakterleri tek tekl dizi elemanlarına atamaktır.
|
||
Ancak bu yöntem çok zahmetlidir. Örneğin:
|
||
|
||
char s[100];
|
||
|
||
s[0] = 'a';
|
||
s[1] = 'n';
|
||
s[2] = 'k';
|
||
s[3] = 'a';
|
||
s[4] = 'r';
|
||
s[5] = 'a';
|
||
s[6] = '\0';
|
||
|
||
Bunun en pratik yolu strcpy fonksiyonunu kullanmaktır. Örneğin:
|
||
|
||
char s[100];
|
||
|
||
strcpy(s, "ankara");
|
||
|
||
Burada strcpy fonksiyoınu "ankara" yazısının başlangıç adresinden başlayarak null karakter görene kadar (null karakter dahil) tüm karakterleri
|
||
bir döngü içerisinde s adresinden itibaren yerleştirecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[100];
|
||
|
||
strcpy(s, "ankara");
|
||
puts(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir string'in karakterlerinin değiştirilmesi "tanımsız davranışa (undefined behavior)" yol açmaktadır. Örneğin:
|
||
|
||
char *str = "ankar";
|
||
|
||
s[2] = 'x'; /* tanımsız davranış"
|
||
|
||
Gerçekten de macOS, Linux ve Windows'taki C derleyicileri string'leri const section'lara yerleştirdikleri için bu
|
||
durum programın çökmesine yol açmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *str = "ankara";
|
||
|
||
str[2] = 'x'; /* dikkat undefined behavior */
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii char türden, signed char türden ve unsigned char türden dizilere iki tırnakla ilkdeğer verirkenki iki tırnak
|
||
ifadesi bir string belirtmediği için (istisna durum) yukarıdaki tanımsız davranış söz konusu değildir. Örneğin:
|
||
|
||
char str[] = "ankara";
|
||
|
||
str[2] = 'x'; /* tamamen normal */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char str[] = "ankara";
|
||
|
||
str[2] = 'x'; /* tamamen normal */
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki koda bakınız:
|
||
|
||
strcpy("veli", "ali");
|
||
|
||
Bu kodun derlenmesinde hiçbir problem olmaz. Ancak fonksiyon çağrıldığında "veli" belirtilen string'in karakteri değiştirildiği için "tanımsız davranış"
|
||
oluşacaktır. Aşağıdaki kodu inceleyiniz:
|
||
|
||
char s[] = "ankara";
|
||
|
||
strcat(s, "izmir");
|
||
|
||
Burada derleme aşamsında hiçbir sorun çıkmayacaktır. Ancak program çalışırken strcat s dizisini taşıracaktır. Dolayısıyla bir gösterici hatası
|
||
söz konusudr ve tanımsız davranış oluşur. Tabii biz s dizisini yeteri kadar büyük açsaydık bir sorun ortaya çıkmayacaktı:
|
||
|
||
char s[100] = "ankara";
|
||
|
||
strcat(s, "izmir"); /* sorun yok */
|
||
|
||
Aşağıdaki gibi bir kodda kişi ne yapmaya çalışıyor olabilir?
|
||
|
||
while (*text != '\0' && strchr(";!.,?-*()/ ", *text) != NULL)
|
||
++text;
|
||
|
||
Burada text göstericisinin gösterdiği yerdeki karakterler ";!.,?-*()/ " karakterlerinden biri olduğu sürece yazıda ilerlenmiştir. Bu tema ile biz
|
||
bir yazıdaki sözcüklerin sayısını aşağıdaki gibi bulabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int get_word_count(char *text)
|
||
{
|
||
int count = 0;
|
||
|
||
for (;;) {
|
||
while (*text != '\0' && strchr(";!.,?-*()/ ", *text) != NULL)
|
||
++text;
|
||
if (*text == '\0')
|
||
break;
|
||
++count;
|
||
while (strchr(";!.,?-*()/ ", *text) == NULL)
|
||
++text;
|
||
|
||
if (*text == '\0')
|
||
break;
|
||
}
|
||
|
||
return count;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = ";;;;;; bugun hava guzel! cok guzel...Evet ,,, evet 234 guzel. Sence ali ";
|
||
int result;
|
||
|
||
result = get_word_count(s);
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Boş bir string söz konusu olabilir. Örneğin:
|
||
|
||
char *str;
|
||
|
||
str = "";
|
||
|
||
Burada derleyici yine char türden bir dizinin içerisine yalnızca null karakteri yerleştirir ve yine onun adresi str göstericisine atanacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de iki adres bilgisi toplanamaz. Bu nedenle bazı programlama dilelrinde geçerli olan string toplama işlemi C'de geçerli değildir. Örneğin:
|
||
|
||
char *str;
|
||
|
||
str = "ali" + "veli"; /* geçersiz! */
|
||
|
||
Bu tarzda bir kod Java gibi C# gibi, Python gibi dillerde geçerlidir. Ancak C'de "ali" + "veli" iki adresi toplamak
|
||
anlamına gelir ve bu durum geçerli değildir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
String'ler char türden adres belirttiğine göre * ve [] operatörleriyle kullanılabilirler. Örneği *"ali" ve "ali"[2]
|
||
gibi ifadeler geçerlidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char ch;
|
||
|
||
ch = *"ali";
|
||
printf("%c\n", ch); /* a */
|
||
|
||
ch = "ali"[2];
|
||
printf("%c\n", ch); /* i */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki programda "ankara" yazısının karakter uzunluğu yazdırılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int i;
|
||
|
||
for (i = 0; "ankara"[i]; ++i)
|
||
;
|
||
printf("%d\n", i); /* 6 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
String'ler aynı satır üzerinde bulunmak zorundadır. String'ler tek bir atom (token) kabul edilmektedir. Örneğin:
|
||
|
||
char *str;
|
||
|
||
str = "bugun hava
|
||
cok guzel"; /* geçersiz! */
|
||
|
||
Pekiyi gerçekten bir string'i farklı satırlara yazamak istersek? Örneğin editörümüzün sütun uzunluğu yeterli olmayabilir. İşte C'de aralarında hiçbir operatör olmayan
|
||
yalnızca boşluk karakterleri olan yan yana iki string derleyici tarafından otomatik olarak birleştirilmektedir. Örneğin:
|
||
|
||
char *str;
|
||
|
||
str = "ali" "veli";
|
||
|
||
Bu işlemin aşağıdakinden bir farkı yoktur:
|
||
|
||
str = "aliveli";
|
||
|
||
Bu sayede biz string'leri iki farklı satıra bölebiliriz:
|
||
|
||
str = "ali"
|
||
"veli";
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *str;
|
||
|
||
str = "bugun hava"
|
||
" cok guzel";
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de programcıların çok yaptığı hatalarından biri de fonksiyonu yerel bir nesnenin ya da dizinin adresiyle geri döndürmektir. Hiçbir fonksiyon
|
||
yerel bir nesnenin ya da dizinin adresieyle geri dönmemelidir. Bu durum tanımsız davranışa yol açar. Çünkü fonksiyon sonlandığında bu yerel
|
||
nesne ya da dizi bellekten yok edileceği için geri döndürülen adres artık tahsis edilmiş olan bir alanın adresi durumunda olmayacaktır. Rastgele
|
||
bir adres durumunda olacaktır. Örneğin:
|
||
|
||
int *foo(void)
|
||
{
|
||
int a = 10;
|
||
|
||
return &a;
|
||
}
|
||
...
|
||
int *pi;
|
||
|
||
pi = foo(); /* dikkat! foo fonksiyonun geri döndürdüğü adres güvenli bir adres değil! */
|
||
|
||
printf("%d\n", *pi);
|
||
|
||
Aşağıdaki tarzda gösterici hataları sık yapılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
char *getname(void)
|
||
{
|
||
char s[1024];
|
||
|
||
printf("Adi soyadi:");
|
||
gets(s);
|
||
|
||
return s;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char *str;
|
||
|
||
str = getname(); /* gösterici hatası */
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de string'ler statik ömürlü nesnelerdir. Yani string'ler programın belleğe yüklenmesiyle yaratılırlar, program sonlanana kadar bellekte kalırlar.
|
||
Biz bir fonksiyon içerisinde bir string kullandığımızda fonksiyondan çıksak bile string yaşamaya devam eder. Bu nedenle biz örneğin bir string'in
|
||
başlangıç adresiyle bir fonksiyonu geri döndürebiliriz. Çünkü fonksiyon sonlansa bile string programın sonuna kadar bellekte kalmaya
|
||
devam edecektir. Örneğin:
|
||
|
||
char *name(void)
|
||
{
|
||
return "ali"; /* tammaen normal *()
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
45. Ders - 15/11/2022 Sali
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
String'ler için yer derleme aşamasında ayrılır. Program çalışmak üzere belleğe yüklendiğinde string'in adresi bellidir. Yani programın akışı string'i
|
||
her gördüğünde string için yer ayrılmaz. String için tek bir yer ayrılır. String'in adresi de derleme sırasında elde edilip string yerine yerleştirilir.
|
||
(Program yüklenmeden derleyicinin string'in adresini nasıl belirlediği konusunda tereddütleriniz olabilir. Ancak bu işlem biraz karmaşık
|
||
süreçlerle yürütülmektedir.) Dolayısıyla aşağıdaki kodda ekrana hep aynı adres basılır. Farklı adresler basılmaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *str;
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
str = "ankara";
|
||
printf("%p\n", str); /* hep aynı adres yazdırılır */
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir program içerisinde tamamen özdeş string'ler kullanıldığında bunlar için tek bir yer mi ayrılacağı ya da her özdeş string için ayrı yerler mi
|
||
ayrılacağı derleyicileri yazanların isteğine bırakılmıştır (implementation dependent). Pek çok derleyicide bu durum derleyici seçeneklerinden
|
||
ayarlanabilmektedir. Örneğin Microsoft C derleyicilerinde bu ayar Visual Studio'da proje seçeneklerinden ""C-C++/Code Generation/Enable String Pooling" seçeneği ile
|
||
ayarlanabilmektedir. Komut satırından /GF seçeneği kullanılmaktadır. Microsoft C derleyicilerinde default durumu böyledir. gcc ve clang derleyicilerinde de
|
||
default durumda özdeş string'ler için tek bir yer ayrılmaktadır. Modern amaç dosya formatlarında bağlayıcının farklı amaç dosyalardaki özdeş string'lerin
|
||
tek bir kopyasının çalıştırılabilir dosyaya yerleştirilmesi de sağlanabilmektedir. Yani pek çok derleyici projenin farklı C dosyalarındaki özdeş string'leri de
|
||
tek bir string olarak ele alabilmektedir.
|
||
|
||
Bu durumu aşağıdaki programla tets edebilirsiniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *s;
|
||
char *k;
|
||
|
||
s = "ankara";
|
||
k = "ankara";
|
||
|
||
printf("%p, %p\n", s, k);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C standartlarına göre İngilizcedeki karakterler ve operatör semboller ve noktalama işaretlerine ilişkin semboller 1 byte yer kaplamalıdır.
|
||
Yani biz bu karakterlerin 1 byte yer kaplayacağı bir encoding'i kullanmak zorundayız. Ancak diğer karakterler (örneğin Türkçe karakter) için
|
||
standartlar bu bir byte zorunluluğu belirtmemiştir. Dolayısıyla temel karakterler 1 byte yer kaplarken örneğin Türkçe karakterler birden fazla byte yer kaplayabilir.
|
||
Bu durumda bir C programı tamamen ASCII karakterleriyle, ASCII tablosunun code page'leriyle ya da Unicode UTF-8 encoding'i ile kodlanmış olabilir.
|
||
Tabii derleyicilerin bu kodlama biçimlerini destekliyor olması gerekir. Günümüzde Unicode karakter tablosu yaygın kullanılmaktadır. Unicode karakter
|
||
tablosunun çeşitli encoding'leri bulunmaktadır. UNOCODE'un temel encoding'i UTF-16 denilen her bir karakterin iki byte kodlandığı encoding'tir.
|
||
Ancak ASCII karakterlerin bir byte ile diğer karakterlerin birden fazla byte ile kodlandığı UTF-8 encoding'i en yaygın kullanılan encoding'tir.
|
||
|
||
C'de karakterler 1 byte yer kaplamaktadır. Ancak 1 byte'tan uzun olan karakter tablolarındaki karakterleri belirtmek için C'de "geniş karakter (wide character)"
|
||
denilen bir tür de oluşturulmuştur. Geniş karakterlerin hangi karakter tablosunu temel aldığı satndartlarda belirtilmemiştir. Ancak Microsoft Unicode UTF-16
|
||
encoding'i olarak, gcc ve clang derleyicileri Unicode UTF-32 encoding'i ele almaktadır.
|
||
|
||
C'de geniş karakterler wchar_t türü ile temsil edilmektedir. wchar_t bir anahtar sözcük değildir. Bir typedef ismidir. Dolayısıyla aslında başka bir türü
|
||
temsil etmektedir. C standartlarına göre wchar_t işaretsiz bir tamsayı türü olarak typedef edilmek zorundadır. Örneğin bu tür Microsoft derleyicilerinde 2 byte'lık
|
||
unsigned short olarak, gcc ve clang derleyicilerinde 4 byte'lık unsigned int olarak typedef edilmiştir. wchar_t türü <stddef.h> dosyası içerisinde typede edilmiştir.
|
||
Dolayısıyla bu tür ismini kullanmak için bu dosyanın include edilmesi gerekmektedir.
|
||
|
||
C'de tek tırnak içerisindeki karakterin önüne onunla yapışık bir L harfi getirilirse böyle karakter geniş karakter sabiti olarak ele alınır. Dolayısıyla
|
||
geniş karakter sabitlerini biz wchar_t türünden nesnelerin içerisine yerleştirmeliyiz. Örneğin:
|
||
|
||
wchar_t ch;
|
||
|
||
ch = L'ş';
|
||
|
||
Burada derleyicilerin geniş karakterleri kodlamak için hangi karakter tablosunu temel alacağı standartlarda belirtilmemiştir. Ancak yukarıda da belirttiğimiz gibi
|
||
Microsoft derleyicileri Unicode UTF-16 encoding'ini temel almaktadır. gcc ve clang derleyicileri geniş karakterler için Unicode UTF-32 encoding'ini
|
||
kullanmaktadırç
|
||
|
||
Özetle geniş karakterler derleyiciler tarafından 1 byte'tan daha uzun byte'larla kodlanmaktadır. Ancak standartlar geniş karakter kodlamasının hangi karakter tablosu
|
||
ve encoding'i dikkate alınarak yapılacağı konusunda bir belirlemede bulunmamıştır. Derleyicilerin çoğu geniş karakterleri Unicode UTF-16 ya da Unicode UTF-32 encoding'ine
|
||
göre kadlamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stddef.h>
|
||
|
||
int main(void)
|
||
{
|
||
wchar_t ch;
|
||
|
||
ch = L'ş';
|
||
printf("%llu\n", (unsigned long long)ch); /* Muhtemelen 351 yani Unicode UTF-16 'ş' */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Geniş karakter dizileri ve string'leri de söz konusudur. Örneğin:
|
||
|
||
wchar_t s[] = L"ağrı dağı";
|
||
wchar_t *str;
|
||
|
||
str = L"ağrı dağı";
|
||
|
||
Başında L olan string'ler wchar_t türünden bir diziye yerleştirilir. Bunlar wchar_t türünden bir adres belirtirler. Aslında C'de geniş karakterlerle ilgili
|
||
yazma yapan ek fonksiyonlar da bulunmaktadır. Örneğin printf fonksiyonunun wprintf isminde geniş karakterlerle çalışan bir biçimi de vardır.
|
||
Ancak bu durum wprintf fonksiyonu ile bizim geniş karakterleri bastıracğımız anlamına gelmemektedir. wprintf fonksiyonu format karakter yazısını da
|
||
geniş karakter string'i olarak alır. Başında w olan IO fonksiyonları geniş karakterlerle çalışmaktadır. Bunların prototipleri <wchar.h> dosyası
|
||
içerisinde bulunur. Ancak yukarıda da belirttiğimiz gibi örneğin wprintf fonksiyonu ile %s seçeneği kullanılarak bir geniş karakterli yazı yazdırılmak
|
||
istendiğinde bu yazıyı biz ekranda göremeyebiliriz. Çünkü bir yazının ekranda istediğimiz görüntülenmesi o anda ekran için kullanılan aygıt sürücünün kabul ettiği encoding'e
|
||
de bağlı olmaktadır. wprintf geniş karakterleri terminal aygıt sürücüsüne gönderir. Ancak aygıt sürücü eğer geniş karakterlere uygun bir encoding'e ayarlı değilse
|
||
bunları gösteremez.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C11 ile birlikte C'ye Unicode UTF-16 ve Unicode UTF-32 encoding'lerine ilişkin karakter sabitleri ve string'ler de eklenmiştir. Unicode UTF-16 karakterleri
|
||
tek tırnağa yapışık u harfi ile, Unicode UTF-32 karakterleri ise tek tırnağa yapışık U karakteri ile temsil edilmektedir. Bu karakterlerin saklanması için
|
||
char16_t ve char32_t isimli typedef türleri bildirilmiştir. Bu türler <uchar.h> başlık dosyasında typedef edilmiştir. Örneğin:
|
||
|
||
char16_t x = u'ş'; /* Unicode UTF-16 */
|
||
char32_t y = U'ş'; /* Unicode UTF-32 */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <uchar.h>
|
||
|
||
int main(void)
|
||
{
|
||
char16_t x = u'ş'; /* Unicode UTF-16 */
|
||
char32_t y = U'ş'; /* Unicode UTF-16 */
|
||
|
||
printf("%llu\n", (unsigned long long)x); /* 351 */
|
||
printf("%llu\n", (unsigned long long)y); /* 351 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Benzer biçimde biz C11 ile birlikte bir string'e de yapışık olarak u ve U karakterlerini ekleyebiliriz. Bu durumda bu string'ler Unicode UTF-16 ve
|
||
Unicode UTF-32 encoding'lerine göre kodlanırlar. Örneğin:
|
||
|
||
char16_t s[] = u"ağı dağı";
|
||
char16_t *k = u"ağrı dağı";
|
||
char32_t m[] = U"ağı dağı";
|
||
char32_t *r = U"ağrı dağı";
|
||
|
||
Tabii u"xxx" biçimindeki bir string char16_t türünden, U"xxx" biçimindeki string char32_t türünden adres belirtmektedir. char16_t ve char32_t
|
||
16 bitlik ve 32 bitlik işaretli tamsayı türü olarak typedef edilmek zorundadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C11 ile birlikte Unicode UTF-8 string'leri de C'ye eklenmiştir. Böyle stringlerin önüne onlarla yapışık olarak u8 öneki getirilir.
|
||
Böyle string'ler için özel bir tür düşünülmemiştir. Bu string'ler de yine char türdendir. Örneğin:
|
||
|
||
char s[] = u8"ağrı dağı"; /* Unicode UTF-8 */
|
||
|
||
C11'de ve C17'de Unicode UTF-8 karakter sabiti bulunmamaktadır. Ancak C23'te bu karakter sabitlerinin de (yani u8'x' gibi) eklenmesi düşünülmektedir.
|
||
C23'e kadar u8 önekli string'ler char türden adres belirtiyordu. C23 ile birlikte char8_t biçiminde yeni bir tür daha eklendi. C23 ve sonrasında
|
||
u8 önekli karakterler ve string'ler char8_t türrüne ilişkindir. C++17 ile birlikte bu karakter sabitleri de C++'a eklenmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi diziler elemanları aynı türden olan ve bellekte ardışıl bir biçimde bulunan veri yapılarıdır. Örneğin
|
||
int bir dizinin tüm elemanları int türdendir. double bir dizinin double türdendir. İşte her elemanı bir adres tutan dizilere
|
||
gösterici dizileri denilmektedir. Yani gösterici dizilerinin her elemanı bir göstericidir. Gösterici dizileri dekleratörde
|
||
hem köşeli parantez hem de * ile bildirilir. Örneğin:
|
||
|
||
int a[10]; /* int türden bir dizi */
|
||
int *b[10]; /* int türden bir gösterici dizisi, b'nin her elemanı int türden bir adres tutar */
|
||
char *c[5]; /* c'nin her elemanı char türden bir adres tutar */
|
||
|
||
Gösterici dizilerindeki * atomu türe ilişkin değil dekleratöre ilişkindir. Örneğin:
|
||
|
||
int a, b[10], *c[10];
|
||
|
||
Burada a int türden bir nesne olarak tanımlanmıştır. b ise int türden 10 elemanlı bir dizidir. c de her elemanı int türden
|
||
gösterici olan 10 elemanlı bir dizidir.
|
||
|
||
Bir gösterici dizisinin her elemanı bir göstericidir. Dolayısıyla bir adres atanmalıdır. Örneğin:
|
||
|
||
int x = 10, y = 20, z = 30;
|
||
int *a[3];
|
||
|
||
a[0] = &x;
|
||
a[1] = &y;
|
||
a[2] = &z;
|
||
|
||
a bir gösterici dizisini belirtiyor olsun. O zaman a[i] bu gösterici dizisinin i'inci indisli elemanıdır. Yani bir adres belirtir. O halde *a[i]
|
||
ifadesinde [] operatörü öncelikli olduğu için önce dizi elemanına erişir, sonra o adresteki nesne elde edilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int x = 10, y = 20, z = 30;
|
||
int *a[3];
|
||
|
||
a[0] = &x;
|
||
a[1] = &y;
|
||
a[2] = &z;
|
||
|
||
for (int i = 0; i < 3; ++i)
|
||
printf("%d\n", *a[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Gösterici dizilerine de küme parantezleri içerisinde ilkdeğer verilebilir. Tabii verilen ilkdeğerlerin adres belirtmesi gerekir. Örneğin:
|
||
|
||
int x = 10, y = 20, c = 30;
|
||
int *a[] = {&x, &y, &z};
|
||
|
||
Ya da örneğin:
|
||
|
||
int x[] = {1, 2, 3, 4, 5}, y[] = {6, 7}, z[] = {8, 9, 10};
|
||
int *a[] = {x, y, z};
|
||
|
||
Burada a gösterici dizisi x, y ve z dizilerinin başlangıç adreslerini göstermektedir. Bu dizilerin aynı uzunlukta olması gerekmez.
|
||
Kme parantezleriyle gösterici dizilerinin az sayıda elemanına ilkdeğer verilirse ilkdeğer verilmeyen elemanlara derleyici
|
||
tarafından NULL adres yerleştirilmektedir. (Yani 0 adresi yerleştirilmemektedir. Tabii yaygın derleyicilerin hepsinde NULL adres zaten 0 adresidir.)
|
||
|
||
a bir gösterici dizisi olsun. Bu dizinin i'inci indisli elemanı da bir diziyi gösteriyor olsun. Bu durumda a[i][k] gibi bir ifade geçerlidir.
|
||
Bu ifade a'nın i'indisli elemanı olan göstericinin gösterdiği yerdeki dizinin k'ıncı elemanı anlamına gelir. [] operatörünün solda-sağa
|
||
öncelikli olduğunu anımsayınız. a[i]'den bir adres elde edilecek o adrese [k] operatörü uygulanacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int x[5] = {1, 2, 3, 4, 5}, y[5] = {6, 7, 8, 9, 10}, z[5] = {11, 12, 13, 14, 15};
|
||
int *a[3] = {x, y, z};
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
for (int k = 0; k < 5; ++k)
|
||
printf("%d ", a[i][k]);
|
||
printf("\n");
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında C'de en fazla karşılaşılan gösterici dizileri char türden gösterici dizileridir. Çünkü bir grup yazının başlangıç adresleri char türden bir gösterici
|
||
dizisinde saklanabilir. Böylece char türden gösterici dizileri adeta string dizileri gibi kullanılır. Örneğin:
|
||
|
||
char *names[3];
|
||
|
||
Burada names 3 elemanlı, her elemanı char türden bir adres tutan göstericidir. O halde biz bu dizinin her elemanına char türden bir adres yerleştirebiliriz:
|
||
|
||
names[0] = "ali";
|
||
names[1] = "veli";
|
||
names[2] = "selami";
|
||
|
||
Burada "ali", "veli" ve "selami" string'leri derleyici tarafından güvenli yerlere (static ömürlü char türden dizilere),
|
||
bunların sonlarına null karakter eklenecek ve derleyici de bu string'ler yerine char türden adresler, kullanacaktır.
|
||
O halde buradaki names dizisi aslında bu yazıların başlangıç adreslerini tutan bir dizi haline gelmiştir. Şimdi bu
|
||
yazıları yazdıralım:
|
||
|
||
for (int i = 0; i < 3; ++i)
|
||
puts(names[i]);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *names[3];
|
||
|
||
names[0] = "ali";
|
||
names[1] = "veli";
|
||
names[2] = "selami";
|
||
|
||
for (int i = 0; i < 3; ++i)
|
||
puts(names[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii char türden gösterici dizilerine doğrudan string'lerle de ilkdeğer verebilirdik. Örneğin:
|
||
|
||
char *names[3] = {"ali", "veli", "selami"};
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *names[3] = {"ali", "veli", "selami"};
|
||
|
||
for (int i = 0; i < 3; ++i)
|
||
puts(names[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
46. Ders - 17/11/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bazen programcı gösterici dizisinin sonuna NULL adres yerleştirir(null karakter değil NULL adres). Böylece NULL adres görene kadar dizinin bütün
|
||
elemanlarına erişebilir. NULL adresin geçerli bir adres belirtmediğine dikkat ediniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *names[] = {"ali", "veli", "selami", "suleyman", "fatih", "ayse", NULL};
|
||
|
||
for (int i = 0; names[i] != NULL; ++i)
|
||
puts(names[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte bir grup isim arasında en uzun karakterden oluşan isim bulunmaktadır. İsimlerin uzunluklarını strlen fonksiyonuyla elde edebiliriz.
|
||
Ancak bu tür durumlarda strlen gibi fonksiyonların gereksiz biçimde aynı yazı için yeniden çağrılmasını elimine etmelisiniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *names[] = {"ali", "veli", "selami", "suleyman", "fatih", "ayse", NULL};
|
||
size_t max_index, max_length, length;
|
||
|
||
max_index = 0;
|
||
max_length = strlen(names[0]);
|
||
|
||
for (int i = 1; names[i] != NULL; ++i) {
|
||
length = strlen(names[i]);
|
||
if (length > max_length) {
|
||
max_index = i;
|
||
max_length = length;
|
||
}
|
||
}
|
||
|
||
puts(names[max_index]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Şimdi de bir grup ismi sözlükteki sırasına göre sıraya dizelim. Bunun için strcmp fonksiyonunu kullanabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
#define SIZE 12
|
||
|
||
int main(void)
|
||
{
|
||
char *names[SIZE] = {"ali", "veli", "selami", "suleyman", "fatih", "ayse", "salih", "burhan", "can", "sibel", "jale", "temel"};
|
||
char *temp;
|
||
int flag;
|
||
size_t i;
|
||
|
||
i = 0;
|
||
do {
|
||
flag = 0;
|
||
for (size_t k = 0; k < SIZE - 1 - i; ++k)
|
||
if (strcmp(names[k], names[k + 1]) > 0) {
|
||
temp = names[k];
|
||
names[k] = names[k + 1];
|
||
names[k + 1] = temp;
|
||
flag = 1;
|
||
}
|
||
++i;
|
||
} while (flag);
|
||
|
||
for (size_t i = 0; i < SIZE; ++i)
|
||
printf("%s ", names[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
En fazla üç basamak olan bir sayıyı yazı ile yazdıran örnek bir program.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
void disp_number(int number)
|
||
{
|
||
char *ones[] = {"", "bir", "iki", "uc", "dort", "bes", "alti", "yedi", "sekiz", "dokuz"};
|
||
char *tens[] = {"", "on", "yirmi", "otuz", "kirk", "elli", "altmis", "yetmis", "seksen", "doksan"};
|
||
int one, ten, hundred;
|
||
|
||
if (number == 0) {
|
||
printf("sifir\n");
|
||
return;
|
||
}
|
||
|
||
one = number % 10;
|
||
ten = number / 10 % 10;
|
||
hundred = number / 100;
|
||
|
||
if (hundred > 0) {
|
||
if (hundred != 1)
|
||
printf("%s ", ones[hundred]);
|
||
printf("yuz");
|
||
}
|
||
if (ten > 0) {
|
||
if (hundred > 0)
|
||
putchar(' ');
|
||
printf("%s", tens[ten]);
|
||
}
|
||
if (one > 0){
|
||
if (number > 10)
|
||
putchar(' ');
|
||
printf("%s", ones[one]);
|
||
}
|
||
putchar('\n');
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int number;
|
||
|
||
for (;;) {
|
||
printf("En fazla 3 basamakli bir sayi giriniz:");
|
||
scanf("%d", &number);
|
||
if (number == -1)
|
||
break;
|
||
if ((int)log10(number) + 1 > 3) {
|
||
printf("sayi 3 basamaktan buyuk!\n");
|
||
continue;
|
||
}
|
||
|
||
disp_number(number);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında orijinal printf fonksiyonunun prototipi şöyledir:
|
||
|
||
int printf(const char *format, ...);
|
||
|
||
Bu prototipteki const anahtar sözcüğünü henüz görmedik. Prototipteki "..." fonksiyonun değişken sayıda argüman alacağını belirtmektedir.
|
||
Biz şimdiye kadar hep printf fonksiyonunun format parametresini bir string olarak girdik. Tabii aslında bu parametre char türden bir adres almaktadır.
|
||
Örneğin biz format yazısını char türden bir dizinin içerisine yerleştirip onun adresini de printf fonksiyonuna verebiliriz:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10, b = 20;
|
||
char format[] = "a = %d, b = %d\n";
|
||
|
||
printf(format, a, b);
|
||
|
||
return 0;
|
||
}
|
||
|
||
printf fonksiyonunun geri dönüş değeri int türdendir. printf stdout dosyasına (ekrana) yazılan karakterlerin sayısına geri dönmektedir. Tabii printf de
|
||
başarısız olabilir (böylesi bir şey normalde mümkün değildir) bu durumda printf negatif herhangi bir değerle geri dönmektedir. Tabii printf fonksiyonun geri
|
||
dönüş değerini genellikle kontrol etmeyiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 10, b = 20;
|
||
char format[] = "a = %d, b = %d\n";
|
||
int result;
|
||
|
||
result = printf(format, a, b);
|
||
printf("%d\n", result); /* 15 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
sprintf fonksiyonu printf fonksiyonunun kardeşi olan bir fonksiyondur. sprintf bir parametre fazlalığa sahiptir. Orijinal prototipi şöyledir:
|
||
|
||
int sprintf(char *buf, const char *format, ...);
|
||
|
||
Fonksiyonun printf'e göre ilk parametre fazladır. sprintf tamamen printf gibi çalışır ancak çıktıyı stdout dosyasına yazmak yerine birinci parametresiyle verilen
|
||
dizinin içerisine yazar. Tabii yazının sonuna null karakteri de yerleştirir. Fonksiyon yine diziye yerleştirdiği karakter sayısına geri dönmektedir (null karakter dahil değildir)
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char buf[1024];
|
||
int a = 10, b = 20;
|
||
int result;
|
||
|
||
result = sprintf(buf, "a = %d, b = %d", a, b);
|
||
|
||
printf("buf = %s, result = %d\n", buf, result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Şimdi printf varken sprintf fonksiyonuna neden gereksinim duyulduğunu düşünebilirsiniz. İşte bazen bir yazıyı bekletip belli koşullarda yazdırabiliriz.
|
||
Bazen yazıyı yazdırmayız bir sokettten karşı tarafa yollamak isteyebiliriz. Bazen de bir GUI ortam söz konusu olabilir. Bu ortamda yalnızca bir yazı yazdırılıyor olabilir.
|
||
Biz de mecburen önce yazdırmak istediklerimizi bir yazı biçimine dönüştürürüz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char buf[1024];
|
||
int a = 10, b = 20;
|
||
int result;
|
||
|
||
result = sprintf(buf, "a = %d, b = %d", a, b);
|
||
|
||
printf("buf = %s, result = %d\n", buf, result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Üç basamaklı sayıyı ekrana (stdout dosyasına) yazdıran yukarıdaki örneği char türden bir dizi içerisine yazı olarak yerleştiren örnek biçiminde değiştirebiliriz.
|
||
Bu tür kodlarda bir yazının sona sürekli olarak başka yazıların eklenmesi gerekebilmektedir. Programcılar genellikle bu tür eklemeleri düz mantık ile
|
||
strcat fonksiyonunu kullnarak yapma eğilimindedir. Halbuki sprintf fonksiyonun geri dönüş değerinden hareketle bu işlemler daha pratik yapılabilmektedir.
|
||
Örneğin:
|
||
|
||
index = 0;
|
||
char buf[1024];
|
||
...
|
||
|
||
index += sprintf(buf, "a = %d, b = %d", a, b);
|
||
index += sprintf(buf, "-this is a test-");
|
||
|
||
Burada sonraki yazı ilk yazının sonuna eklenecektir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <math.h>
|
||
|
||
void num2text(int number, char *buf)
|
||
{
|
||
char *ones[] = {"", "bir", "iki", "uc", "dort", "bes", "alti", "yedi", "sekiz", "dokuz"};
|
||
char *tens[] = {"", "on", "yirmi", "otuz", "kirk", "elli", "altmis", "yetmis", "seksen", "doksan"};
|
||
int one, ten, hundred;
|
||
int index = 0;
|
||
|
||
if (number == 0) {
|
||
strcpy(buf, "sifir");
|
||
return;
|
||
}
|
||
|
||
one = number % 10;
|
||
ten = number / 10 % 10;
|
||
hundred = number / 100;
|
||
|
||
if (hundred > 0) {
|
||
if (hundred != 1)
|
||
index += sprintf(buf + index, "%s ", ones[hundred]);
|
||
index += sprintf(buf + index, "yuz");
|
||
}
|
||
if (ten > 0) {
|
||
if (hundred > 0)
|
||
buf[index++] = ' ';
|
||
index += sprintf(buf + index, "%s", tens[ten]);
|
||
}
|
||
if (one > 0) {
|
||
if (number > 10)
|
||
buf[index++] = ' ';
|
||
index += sprintf(buf + index, "%s", ones[one]);
|
||
}
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int number;
|
||
char str[1024];
|
||
|
||
for (;;) {
|
||
printf("En fazla 3 basamakli bir sayi giriniz:");
|
||
scanf("%d", &number);
|
||
if (number == -1)
|
||
break;
|
||
if ((int)log10(number) + 1 > 3) {
|
||
printf("sayi 3 basamaktan buyuk!\n");
|
||
continue;
|
||
}
|
||
|
||
num2text(number, str);
|
||
printf(":%s:\n", str);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
47 Ders - 22/11/2022 - Sali
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizinin ismi dizinin başlangıç adresini belirtiyordu. Ve bu adres dizinin ilişkin olduğu tür türündendi. Başka bir deyişle bir dizinin ismi
|
||
dizinin ilk elemanın adresiydi. Pekiyi bir göstericisinin ismi ne belirtmektedir? Bir gösterici dizisinin ismini bir göstericiye atayacaksak
|
||
göstericinin hangi türden olması gerekir? Örneğin:
|
||
|
||
int *a[10];
|
||
|
||
Burada a dizisi int * türünden elemanları tutmaktadır. Bir dizinin ismi dizinin ilk elemanın adresi anlamına geleceğine göre ve bu dizinin de ilk elemanı int türden
|
||
bir gösterici olduğuna göre int türden bir göstericinin adresi nasıl bir göstericiye atanmalıdır? İşte C'de bir göstericinin adresi ** ile temsil edilen
|
||
göstericiyi gösteren bir göstericiye atanabilmektedir. Çrneğin:
|
||
|
||
int *a[10];
|
||
int **ppi;
|
||
|
||
ppi = a;
|
||
|
||
Bu konu ileride ayrı bir başlık altında ele alınacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Rastgele sayı üretimi programlamada pek çok alanda kullanılmaktadır. Örneğin Tetris gibi bir oyunda düşen şekillerin rastgele olması için rastgele sayı
|
||
üretilmesi gerekmektedir. Bilgisayarda rastgele sayı üretme işlemi artimetik yolla yapılmaktadır. Bu biçimde rastgele sayı üretilmesine "pseudo random number generation"
|
||
denilmektedir. Rastgele sayı üretiminde belli bir tohum (seed) değer alınır. O değer üzerinde bir işlem uygulanır, rastgele bir değer elde edilir.
|
||
Elde edilen rastgele değer yeniden bir işleme sokulur ondan da rastgele değer elde edilir. Böylece bir dizi rastgele değer elde edilmiş olur.
|
||
İlk tohum değer aynı olursa hep aynı rastgele sayılar elde edilecektir. Bir sayının bir sayıya bölümünden elde edilen kalan rastegelik içermektedir.
|
||
Benzer biçimde sayının ortadaki n basamağının karesi de rastgele bir değer oluşturmaktadır. Yani rastegele sayı elde etmek için buna benzer çeşitli yöntemler kullanılmaktadır.
|
||
|
||
C'de rastgele sayı üretimi ile ilgili iki standart C fonksiyonu vardır: rand ve srand. Bu fonksiyonların prototipleri <stdlib.h> içerisinde bildirilmiştir.
|
||
rand fonksiyonu her çağrıldığında 0 ile RAND_MAX arasında rastgele int ütrdne bir tamsayı değeri üretir. RAND_MAX <stdlib.h> içerisinde bildirilmiş olan bir sembolik sabittir.
|
||
Microsoft C derleyicilerinde 32767, gcc derleyicilerinde 2147438647 olarak define edilmiş durumdadır. Standartlara göre kaç olarak define edileceği derleyicileri
|
||
yazanların isteğine bırakılmıştır. Standartlar RAND_MAX için minimum değerin 32767 olableceğini de belirtmektedir. Yani derleyicilerimiz RAND_MAZ için bu değerden daha küçük bir değer
|
||
define edemez. rand fonksiyonun prototipi şöyledir:
|
||
|
||
int rand(void);
|
||
|
||
Tabii eğer biz daha dar bir aralıkta rassal sayı elde etmek istiyorsak bu durumda rand fonksiyonundan elde edilen değere mod işlemi uygularız.
|
||
rand fonksiyonunun kullandığı tohum değer program her çalıştırıldığında hep aynı değerden başlatıldığı için programın her çalışmasında aynı rassal sayılar
|
||
elde edilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
for (int i = 0; i < 20; ++i) {
|
||
val = rand() % 10;
|
||
printf("%d ", val);
|
||
}
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Rassal sayı üretiminde kullanılabilecek tohum değer srand fonksiyonuyla değiştirilebilmektedir. srand fonksiyonun prototipi şöyledir:
|
||
|
||
void srand(unsigned int seed);
|
||
|
||
Pekiyi programın her çalışmasında farklı bir rassal diziliminin elde edilmesi için ne yapılabilir? srand fonksiyonu ile işin başında bir tohum değer belirlemekle
|
||
bu işlem yapılamaz. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
srand(123456);
|
||
|
||
for (int i = 0; i < 20; ++i) {
|
||
val = rand() % 10;
|
||
printf("%d ", val);
|
||
}
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada üretilen 20 değer bir önceki programdaki 20 değerden farklı olacaktır. Ancak yine program her çalıştığında tohum değer aynı olacağı için aynı değerler
|
||
elde edilecektir. Tabii srand çağırmasını dönügünün içerisine yerleştirirsek bu defa hep aynı sayıyı elde ederiz:
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
for (int i = 0; i < 20; ++i) {
|
||
srand(123456);
|
||
val = rand() % 10;
|
||
printf("%d ", val);
|
||
}
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
Programın her çalışmasında farklı bir rastgele sayı kümesinin elde edilmesi için programın her çalışmasında tohum değerin farklı bir değerle başlatılması gerekir.
|
||
İşte bunun için time isimli standart C fonksiyonunda faydalanılmaktadır. Bu fonksiyon ileride açıklanacaktır. Ancak fonksiyon her çağrıldığında belli bir noktadan
|
||
itibaren çağrılma zamanına kadarki saniye sayısını vermektedir. Genellikle bu belli nokta 01/01/1970 olarak alınmaktadır. time fonksiyonunun prototipi <time.h>
|
||
dosyası içerisindedir. O halde problem şöyle çözülebilir:
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
|
||
srand(time(NULL));
|
||
|
||
for (int i = 0; i < 20; ++i) {
|
||
val = rand() % 10;
|
||
printf("%d ", val);
|
||
}
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
Bu konuda en sık yapılan hatalardan biri de srand çağırmasını döngü içerisine yerleştirmektir. Örneğin:
|
||
|
||
for (int i = 0; i < 20; ++i) {
|
||
srand(time(NULL));
|
||
val = rand() % 10;
|
||
printf("%d ", val);
|
||
}
|
||
|
||
Burada time fonksiyonu 01/01/1970'ten çağrım zamanına kadar geçen saniye sayısını vereceğine göre aslında tohum değer hep aynı değerle başlatılacaktır. srand(time(NULL))
|
||
çağırması programın başında yalnızca bir kez yapılmalıdır.
|
||
|
||
time fonksiyonunun 01/01/1970'ten geçen saniye sayısını vermesi zorunlu değildir. Ancak geleneksel olarak bu fonksiyon bu biçimde davranmaktadır. UNIX türevi sistemlerde
|
||
ise time fonksiyonunun 01/01/1970'ten geçen saniye sayısını vereceği POSIX standartlarında garanti edilmektedir.
|
||
|
||
Dennis Ritchie ve Brian Kernigan tarafından yazılmış olan "The C Programming Language" kitabının ikinci baskısında (edition) rand ve srand fonksiyonlarının olası
|
||
gerçekleştirimi aşağıdaki gibi verilmiştir:
|
||
|
||
unsigned long int next = 1;
|
||
|
||
int rand(void)
|
||
{
|
||
next = next * 1103515245 + 12345;
|
||
|
||
return (unsigned int)(next/65536) % 32768;
|
||
}
|
||
|
||
void srand(unsigned int seed)
|
||
{
|
||
next = seed;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte 5 kişi arasında rastgele kişiler seçilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <time.h>
|
||
|
||
#define NPERSONS 5
|
||
|
||
int main(void)
|
||
{
|
||
int index;
|
||
char *names[] = {"ali", "veli", "selami", "ayse", "fatma"};
|
||
|
||
srand(time(NULL));
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
index = rand() % NPERSONS;
|
||
printf("%s\n", names[index]);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
1 ile 49 arasında birbirinden farklı 6 sayı üreten bir fonksiyon aşağıdaki gibi yazılabilir. Fonksiyonun prototipi şöyledir:
|
||
|
||
int *get_lotto_column(int *col);
|
||
|
||
Fonksiyon 6 elemanlı bir int dizinin adresini parametre olarak alır ve aldığı adrese geri döner. Fonksiyon rastegele ürettiği sayıları bu diziye yerleştirmektedir.
|
||
|
||
Asında aşağıdaki yöntem her zaman uygun bir yöntem değildir. Örneğin 100 tane sayıdan birbirinin aynısı olmayan 95 sayı elde edeceğimiz zaman bu yöntem
|
||
çok fazla zamana mal olabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <time.h>
|
||
|
||
int *get_lotto_column(int *col);
|
||
|
||
int main(void)
|
||
{
|
||
int a[6];
|
||
|
||
srand(time(NULL));
|
||
|
||
get_lotto_column(a);
|
||
|
||
for (int i = 0; i < 6; ++i)
|
||
printf("%d ", a[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
void get_lotto_column(int *col)
|
||
{
|
||
int val;
|
||
int flag;
|
||
|
||
for (int i = 0; i < 6; ++i) {
|
||
do {
|
||
flag = 0;
|
||
val = rand() % 49 + 1;
|
||
for (int k = 0; k < i; ++k) {
|
||
if (col[k] == val) {
|
||
flag = 1;
|
||
break;
|
||
}
|
||
}
|
||
} while (flag);
|
||
|
||
col[i] = val;
|
||
}
|
||
return col;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
48 Ders - 24/11/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Olasılığın en yaygın tanımı "göreli sıklık (relative frequencey)" tanımıdır. Olasılık bu tanıma göre bir limit durumudur. Örneğin bir paranın atılmasında
|
||
yazı gelme olsalığının 0.5 olması demek atım artırıldığında sayıda tekrarlanırsa yazı gelme sayısının atım sayısına oranının 0.5'e yakınsaması demektir.
|
||
Buna istatistikte "büyük sayılar yasası (law of large numbers)" da denilmektedir. Aşağıda yazı tura atma işleminde deneme sayısının artırılmasıyla oranın gitgide
|
||
0.5'e yakınmaması gösterilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <time.h>
|
||
|
||
#define TRYNUM 100000000
|
||
|
||
int main(void)
|
||
{
|
||
unsigned long long head, tail;
|
||
int val;
|
||
double head_ratio, tail_ratio;
|
||
|
||
srand(time(NULL));
|
||
|
||
head = tail = 0;
|
||
for (unsigned long long i = 0; i < TRYNUM; ++i) {
|
||
val = rand() % 2;
|
||
if (val == 0) /* Head */
|
||
++head;
|
||
else
|
||
++tail; /* tail */
|
||
}
|
||
|
||
head_ratio = (double)head / TRYNUM;
|
||
tail_ratio = (double)tail / TRYNUM;
|
||
|
||
printf("head: %.8f, tail: %.8f\n", head_ratio, tail_ratio);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pek çok programlama dilinin standart kütüphanesinde 0 ile 1 arasında rasgele gerçek sayı veren fonksiyonlar bulunmaktadır. Ancak C'de böyle bir fonksiyon
|
||
yoktur. C'de bunu sağlamanın en prik yolu aşağıdaki gibidir:
|
||
|
||
result = (double)rand() / RAND_MAX;
|
||
|
||
Diğer kütüphanelerde de aslında işlemler buradaki yöntemle yapılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
double result;
|
||
|
||
srand(time(NULL));
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
result = (double)rand() / RAND_MAX;
|
||
printf("%f\n", result);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Şimdi de rastgele sayı üreterek PI sayısını bulan ilginç bir örnek yapalım. Birim çemberin dörtte birini düşünelim. Bu dörtte birlik bölgedeki kare
|
||
nin alanı 1'dir. Bu dörtte birlik daire diliminin alanı ise pi / 4'tür. Biz 0 ile 1 arasında N tane rastgele nokta üretirsek bu nokta karenin içerisinde olacaktır.
|
||
Ancak çemberin içerisinde olmayabilir. KArenin içerisinde olanların çemberin içerisinde olanlara oranı bunların alanlarının oranı kadar olmalıdır. Bu duurmda
|
||
N toplam elde edilen nokta sayısını, k ise bunoktaların dörtte birlik çember içerisinde kalanlarının sayısını belirtiyor olsun. Bu durumda
|
||
|
||
1 / (pi /4) = N / k olmalıdır. İçler dışlar çarpımıyla pi'yi çekersek şu eşitliği elde ederiz:
|
||
|
||
pi = 4 * k / n
|
||
|
||
Aşağıda örnek verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <math.h>
|
||
#include <time.h>
|
||
|
||
#define N 10000000
|
||
|
||
int main(void)
|
||
{
|
||
double x, y;
|
||
unsigned long long k;
|
||
double pi;
|
||
|
||
srand(time(NULL));
|
||
|
||
k = 0;
|
||
for (unsigned long long i = 0; i < N; ++i) {
|
||
x = (double)rand() / RAND_MAX;
|
||
y = (double)rand() / RAND_MAX;
|
||
if (sqrt(pow(x, 2) + pow(y, 2)) < 1)
|
||
++k;
|
||
}
|
||
|
||
pi = 4. * k / N;
|
||
printf("%f\n", pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında rassal sayı üreticilerinin kaliteleri diye bir kavram vardır. Ritchie Kernigan kitabında belirtildiği gibi üretilen rassal sayılar çabuk üretilirler
|
||
ancak düşük kaliteye sahiplerdir. Kalite döngüye girme olasılığı ve yansızlık ile ölçülmektedir. Eğer tohum değer güncellenirken eski tohum değerlerden biri elde edilirse
|
||
rassal sayı üreticisi döngüye girer. İşte bu döngüye girme ne kadar geç olursa kalite o kadar yükselmektedir. Kaliteli rassal sayı üeticileri rassal sayıları daha
|
||
uzun zamanda ürettiği için programlama dillerinin kütüphanelerinde genellikle kullanılmazlar.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Eskiden beri kullanılan klasik siyak ekrana "console" ekranı denilmektedir. Console ekranı text bir ekrandır Text ekran demek yalnızca karakter basılan
|
||
ancak pixel basılamayan ekran demektir. Yani text ekranın en küçük birimi bir karakterdir. Halbuki grafik ekranın en küçük birimi bir pixel'dir.
|
||
Console ekranında imleçtan bağımsız herhangi bir yere bir karakter basmak için hazır bir standart C fonksiyonu yoktur. Benzer biçimde console
|
||
ekranında renkli yazı yazmak için de standart C fonksiyonları yoktur. Bu tür işlemler iki biçimde yapılabilmektedir:
|
||
|
||
1) Terminal aygıt sürücüleri genellikle ANSI escape komutlarını desteklemektedir. Dolayısıyla ekrana sanki yazı yazılıyormuş gibi bazı escape
|
||
karakterleri gönderilirse bu işlemler yapılabilmektedir.
|
||
|
||
2) Bu işlemler için üçüncü parti kütüphaneler bulunmaktadır. Örneğin Windows sistemlerinde "Console API'leri" denilen özel fonksiyonlar bu işlemleri
|
||
yapmaktadır. Benzer biçimde UNIX/Linux ve macOS sistemlerinde bu tür işlemler "curses" gibi kütüphanelerle daha kolay yapılabilmektedir.
|
||
|
||
Aşağıdaki örnekte Windows'un console API'leri kullanılarak writec, hide_cursor ve get_console_size fonksiyonları yazılmıştır. Sonra da write fonksiyonu kullanılarak
|
||
bir '*' hareket ettirilmiştir. Buradaki _getch fonksiyonu yuşa basar basmaz alan Microsoft derleyicilerinde bulunan ek bir fonksiyondur. Aslında
|
||
bu fonksiyon da Console API'leri kullanılarak yazılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <Windows.h>
|
||
#include <conio.h>
|
||
|
||
void get_console_size(int *height, int *width)
|
||
{
|
||
CONSOLE_SCREEN_BUFFER_INFO csbi;
|
||
|
||
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
|
||
*width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
|
||
*height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
|
||
}
|
||
|
||
void hide_cursor(void)
|
||
{
|
||
CONSOLE_CURSOR_INFO cinfo;
|
||
|
||
cinfo.dwSize = 100;
|
||
cinfo.bVisible = FALSE;
|
||
|
||
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cinfo);
|
||
}
|
||
|
||
void writec(int row, int col, char ch)
|
||
{
|
||
COORD coord = {col, row};
|
||
DWORD dw;
|
||
|
||
WriteConsoleOutputCharacterA(GetStdHandle(STD_OUTPUT_HANDLE), &ch, 1, coord, &dw);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int rowsize, colsize;
|
||
int row, col;
|
||
int ch;
|
||
|
||
get_console_size(&rowsize, &colsize);
|
||
hide_cursor();
|
||
|
||
row = 10, col = 10;
|
||
|
||
for (;;) {
|
||
|
||
writec(row, col, '*');
|
||
|
||
ch = _getch();
|
||
|
||
writec(row, col, ' ');
|
||
|
||
switch (ch) {
|
||
case 'w':
|
||
if (row == 0)
|
||
row = rowsize - 1;
|
||
else
|
||
--row;
|
||
break;
|
||
case 's':
|
||
if (col == colsize - 1)
|
||
col= 0;
|
||
else
|
||
++col;
|
||
break;
|
||
case 'z':
|
||
if (row == rowsize - 1)
|
||
row = 0;
|
||
else
|
||
++row;
|
||
break;
|
||
case 'a':
|
||
if (col == 0)
|
||
col = colsize - 1;
|
||
else
|
||
--col;
|
||
break;
|
||
}
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Hem bir tuşa basılana kadar programımın beklememesi hem de tuş okuması nasıl yapabiliriz? Bu işlemi yapabilen standart C fonksiyonları yoktur.
|
||
Microsoft'un Windows C kütüphanesinde _kbhit isimli bir fonksiyon bulunmaktadır. Bu fonksiyonun benzeri Curses kütüphanesinde de vardır.
|
||
Bu fonksiyon o anda klavyede tuşa basılı olup olmadığı bilgisini verir. O halde tuşa basılmışsa tuşu okursak istediğimizi yapabiliriz. Örneğin:
|
||
|
||
if (_kbhit()) {
|
||
ch = _getch();
|
||
...
|
||
}
|
||
|
||
Aşağıdaki örnekte bir '*' belli yönde hareket etmekte 'w', 's', 'z', 'a' tuşları ile onn yönü değiştirilmektedir. Kod içerisinde Sleep isimli bir
|
||
Windows API fonksiyonu daha kullanımıştır. Bu fonksiyon programın akışını parametresiyle belirtilen milisaniye kadar bekletmektedir. Bu fonksiyonun UNIX/Linux ve
|
||
macOS sistemlerinde de benzerleri vardır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <Windows.h>
|
||
#include <conio.h>
|
||
|
||
#define UP 0
|
||
#define RIGHT 1
|
||
#define DOWN 2
|
||
#define LEFT 3
|
||
|
||
void get_console_size(int *height, int *width)
|
||
{
|
||
CONSOLE_SCREEN_BUFFER_INFO csbi;
|
||
|
||
GetConsoleScreenBufferInfo(GetStdHandle(STD_OUTPUT_HANDLE), &csbi);
|
||
*width = csbi.srWindow.Right - csbi.srWindow.Left + 1;
|
||
*height = csbi.srWindow.Bottom - csbi.srWindow.Top + 1;
|
||
}
|
||
|
||
void hide_cursor(void)
|
||
{
|
||
CONSOLE_CURSOR_INFO cinfo;
|
||
|
||
cinfo.dwSize = 100;
|
||
cinfo.bVisible = FALSE;
|
||
|
||
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cinfo);
|
||
}
|
||
|
||
void writec(int row, int col, char ch)
|
||
{
|
||
COORD coord = {col, row};
|
||
DWORD dw;
|
||
|
||
WriteConsoleOutputCharacterA(GetStdHandle(STD_OUTPUT_HANDLE), &ch, 1, coord, &dw);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int rowsize, colsize;
|
||
int row, col;
|
||
int direction;
|
||
int ch;
|
||
|
||
get_console_size(&rowsize, &colsize);
|
||
hide_cursor();
|
||
|
||
row = 10, col = 10;
|
||
direction = RIGHT;
|
||
|
||
for (;;) {
|
||
writec(row, col, '*');
|
||
|
||
Sleep(100);
|
||
|
||
|
||
if (_kbhit()) {
|
||
ch = _getch();
|
||
switch (ch) {
|
||
case 'w':
|
||
direction = UP;
|
||
break;
|
||
case 's':
|
||
direction = RIGHT;
|
||
break;
|
||
case 'z':
|
||
direction = DOWN;
|
||
break;
|
||
case 'a':
|
||
direction = LEFT;
|
||
break;
|
||
case 'p':
|
||
_getch();
|
||
break;
|
||
case 'q':
|
||
goto EXIT;
|
||
}
|
||
}
|
||
|
||
writec(row, col, ' ');
|
||
|
||
switch (direction) {
|
||
case UP:
|
||
if (row == 0)
|
||
row = rowsize - 1;
|
||
else
|
||
--row;
|
||
break;
|
||
case RIGHT:
|
||
if (col == colsize - 1)
|
||
col = 0;
|
||
else
|
||
++col;
|
||
break;
|
||
case DOWN:
|
||
if (row == rowsize - 1)
|
||
row = 0;
|
||
else
|
||
++row;
|
||
break;
|
||
case LEFT:
|
||
if (col == 0)
|
||
col = colsize - 1;
|
||
else
|
||
--col;
|
||
break;
|
||
}
|
||
}
|
||
EXIT:
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
49. Ders - 29/11/2022 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Normal olarak bir program akış main fonksiyonunu bitirdiğinde ya da main fonksiyonunda return deyimini gördüğünde sonlanmaktadır. Ancak bir program
|
||
her hangi bir fonksiyonun içerisinde de istenilen noktada sonlandırılabilmektedir. Bunun exit isimli standar C fonksiyonu kullanılmaktadır. exit
|
||
fonksiyonunun prototipi <stdlib.h> dosyası içerisindedir ve aşağıdaki gibidir:
|
||
|
||
void exit(int status);
|
||
|
||
Programın akışı exit fonksiynunu gördüğünde proses sonlandırılmaktadır.
|
||
|
||
exit fonksiyonu parametre olarak "exit kod (exit code)" denilen int türden bir değer alır. İşletim sistemlerinde sonlanan programlar (çalışmakta olan programlara "proses"
|
||
de denilmektedir) işletim sistemine sonlanma bilgşisi olarak "exit kod (exit code)" denilen bir bilgi de gönderirler. İşletim sistemi bu exit kodla ilgili bir şey yapmaz.
|
||
Dolayısıyla işletim sistemi için bu exit kodun kaç olduğunun bir önemi yoktur. Ancak işletim sistemi bu exit kodu bir proses (özellikle üst proses)
|
||
talep ederse ona verebilmektedir. Böylece bir programın başka bir programı çalıştırdığı durumlarda çalıştırılan program başarısızlık durumunda belli exit kodlarla
|
||
sonlandırılırsa onu çalıştıran programlar o programın başarı ya da başarısızlığını exit koduna bakarak anlayabilmektedir. Pekiyi biz programımızı sonlandırırken
|
||
exit kod olarak exit fonksiyonunun içerisine gangi değeri yazmalıyız? İşte genellikle ve geleneksel olarak C'de başarılı sonlanmalar için 0 değeri, başarısız
|
||
sonlanmalar için sıfır dışı değerler tercih edilmektedir. Okunabilirliği artırmak için <stdlib.h> dosyası içerisinde
|
||
aşağıdaki iki sembolik sabit bildirilmiştir:
|
||
|
||
#define EXIT_SUCCESS 0
|
||
#define EXIT_FAILURE 1
|
||
|
||
Standartlarda EXIT_SUCCESS ve EXIT_FAILURE değerlerinin 0 ve 1 olduğu belirtilmemiştir. Bu durum bu sembolik sabitlerin değerlerinin derleyicileri yazanların
|
||
isteğine bağlı olarak değişebileceği anlamına gelmektedir. Ancak hemen her zaman derleyiciler bu sembolik sabitleri 0 ve 1 olarak define etmektedir. main fonksiyonunda
|
||
return işlemi uygulanmazsa sanki 0 değeriyle geri dönülmüş gibi işlem uygulandığını anımsayınız.
|
||
|
||
Pekiyi programımızı neden erken sonlandırmak isteyebiliriz? Bunun iki nedeni olabilir: Birincisi program zaten hedeflediği şeyi yapmış durumdadır. Dolayısıyla
|
||
programcı o noktada artık mutlı bir biçimde exit(EXIT_SUCCESS) çağrısıyla programını sonlandırabilir. Programın exit fonksiyonuyla sonlandırılmasının ikinci
|
||
nedeni de bazı başarısızlıklar olabilmektedir. Örneğin program bir dosyayı açamamış olabilir. Bu durumda programa devam edilmesinin bir anlamı kalmaya bilir.
|
||
Böylesi bir durumda programcı exit(EXIT_FAILURE) çağrısı ile programını sonlandırabilir. exit fonksiyonuna negatif bir değer de argüman olarak geçilebilir.
|
||
Ancak işletim sistemlerinin çoğu negatif exit kodlarını kabul etmemektedir. Yani işletim sistemlerinin çoğu exit kodu olarak verilen değeri işaretsiz tamsayı
|
||
türlerine dönüştürmektedir.
|
||
|
||
Programın başarısz bir biçimde sonlandırılması sırasında programcının exit fonksiyonunu çağırmadan önce oluşan problemli durum hakkında ekrana bir yazı bastırması
|
||
iyi bir tekniktir. Böylece kullanıcı başarısızlığın endenini anlayabilir ve onu düzeltme yoluna gidebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
void bar(void)
|
||
{
|
||
printf("bar begins...\n");
|
||
|
||
exit(EXIT_SUCCESS);
|
||
|
||
printf("bar ends...\n");
|
||
}
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo begins...\n");
|
||
bar();
|
||
printf("foo ends...\n");
|
||
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
printf("main begins...\n");
|
||
|
||
foo();
|
||
|
||
printf("main ends...\n");
|
||
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
main fonksiyonu bittiğinde program bitiyordu. Peki bu durumda programın exit kodu ne olacaktır? İşte bir C programı eğer exit fonksiyonuyla sonlandırılmamışsa
|
||
programın main fonksiyonunu bitirmesiyle sonlandırılır. main fonksiyonun geri dönüş değeri (yani main fonksiyonunda return ettiğimiz değer) bu durumda
|
||
programın exit kodu olacaktır. C standartları main fonksiyonu bittiğinde onun geri dönüş değeri ile exit fonksiyonun derleyici tarafından çağrılmasını gerektiğini
|
||
belirtmektedir. Başka bir deyişle standartlara göre main fonksiyonun çağrılması exit(main()) biçiminde olmaktadır. Ayrıca C standaratları main fonksiyonun aözgü olarak
|
||
main fonksiyonunda hiç return kullanılmazsa sanki main fonksiyonunun sonuna return 0 yazılmış gibi bir etki oluşacağını da belirtmektedir.
|
||
Yani biz main fonksiyonunda hiç return deyimini kullanmasak bile sanki 0 ile return etmişiz gibi bir durum oluşmaktadır. Tabii bu durum main fonksiyonuna
|
||
özgüdür. Başka bir fonksiyonda açıkça return kullanmazsak geri dönüş değeri olarak çöp değer elde edilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
UNIX/Linux ve macOS sistemlerinde komut satırında son çalıştırılmış olan programın exit kodunu aşağıdaki komtla görüntüleyebiliriz:
|
||
|
||
echo $?
|
||
|
||
Aynı işlem Windows'un komut satırında şu komutla yapılmaktadır:
|
||
|
||
echo %errorlevel%
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Program hatalı bir biçimde exit ile sonlandırılmadan önce hata mesajının stdout dosyasına değil stderr dosyasına yazdırılması iyi bir tekniktir.
|
||
Biz henüz stderr dosyasının ne anlama gedliğini görmedik. Ancak stderr dosyasına yazdırılan mesajlar default durumda yine ekrana basılmaktadır.
|
||
stderr dosyasına yazdırma yapmak için printf yerine fprintf fonksiyonu kullanılmaktadır. fprintf fonksiyonunun birinci parametresine stderr
|
||
yazmalısınız. fprintf fonksiyonun diğer parametreleri tamamen printf fonksiyonu ile aynıdır. Örneğin:
|
||
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi C90'a kadar C'de dizilerin uzunlukları tanomlama sırasında sabit ifadeleriyle belirtilmek zorundaydı. C99 ile birlikte yerel
|
||
dizilerin uzunluklarınıjn sabit ifdeleri ile belirtilme zorunluluğu ortadan kaldırılmıştı. Ancak bu değişekn uzunluktaki diziler (variable length array) konusu
|
||
C++'a dahil edilmemişti. Ayrıca bir diziyi tanımladıktan sonra onun boyutlarını değiştiremiyorduk.
|
||
|
||
Ancak bazen bir dizinin eleman uzunluğu programın çalışma zamanı sırasında ve anacak birtakım olaylar sonucunda belirlenebilmektedir. Bazen de dizilerin
|
||
büyütülüp küçültülmesi gerekebilmektedir. İşte bunların sağlanması için programın çalışma zamanı sırasında güvenli bir biçimde ardışıl byte'ların tahsis edilmesi gerekmektedir.
|
||
C'de programın çalışma zamanı sırasın ardışıl byte tansis edebilmeye yönelik mekanizmalara "dinamik bellek yöetimi (dynamic memory management)"
|
||
denilmektedir. C'de dinamik bellek yönetimi ismine "dinamik bellek fonksiyonları" denilen standart C fonksiyonlarıyla yapılmaktadır.
|
||
|
||
Dinamik bellek fonksiyonlarının rototipleri <stdlib.h> içerisindedir. C'de 4 dinamik bellek fonksiyonu vardır: malloc, calloc, realloc ve free.
|
||
|
||
Dinamik bellek fonksiyonlarıyla tahsis edime potansiyelinde olan alanlara "heap" denilmektedir. Heap alanının ne büyüklükte olduğu ve prosesin bellek alanında
|
||
nerede oluşturulduğu sistem sisteme değişebilmektedir. Windows, UNIX/Linux ve macOS işletim sistemlerinde heap alanı prosese özgüdür.
|
||
Yani bu sistemlerde her prosesin heap alanı diğerlerinden farklıdır ve o proses için oluşturulmuştur. Bu sistemlerde bir program bellek yüklendiğinde
|
||
onun için bir heap alanı oluşturulur. Program bittiğinde programla birlikte onun heap alanı da sisteme iade edilir. Böylece bu sistemlerde bir
|
||
program içerisinde yapılan dinamik tahsisatların başka programların heap alanlarını daraltıcı bir etkisi olmaz. Ancak C standartlarında heap alanının
|
||
proses özgü olup olmadığı konusunda bir şey söylenmemiştir. Bazı sistemlerde heap alanı tüm prosesler için ortak bir alan biçiminde oluşturulabilmektedir.
|
||
|
||
Genel olarak çalışmakta olan bir program için en büyük alan data/bss ve heap alanlarıdır. En küçük alan yerel değişkenlerin ve parametre değişkenlerinin
|
||
yaratıldığı stack alanıdır. Heap alanı genel olarak büyük bir alan olma eğilimindedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
en sık kullanılan dinamik bellek fonksiyonu malloc fonksiyonudur. Fonksiyonun prototipi şöyledir:
|
||
|
||
void *malloc(size_t size);
|
||
|
||
Fonksiyn parametresiyle belirtilen miktarda ardışıl alanı heap'te tahsis eder. Tahsis ettiği alanın başlangıç daresiyle geri döner. Artık o alan
|
||
bizim kullanımımız için güvenli bir biçimde tahsis edilmiş olur. malloc fonksiyonu başarısızlık durumunda NULL adresle geri dönmektedir. eğer heap alanı doluysa
|
||
ve istenilen miktarda ardışıl alan bulunamadıysa malloc başarısız olabilmektedir. Bugün kullanıdğımız 64 bit Windows, UNIX/Linux ve macOS sistemlerinde
|
||
proseslerin heap alanları oludkç geniştir. Ancak ne olursa olsun programcı malloc ile yaptığı tahsisatın başarısını kontrol etmelidir. Genellikle böylesi
|
||
bir başarısızlıkta programcılar programlarını exit fonksiyonuyla sonlandırırlar. malloc fonksiyonu ile tahsis edilen alanda çöp değerler vardır.
|
||
|
||
Programcı tipik olarak malloc ile tahsis edilen alanın adresiniş bir göstericiye atar ve o alanı artık bir dizi gibi kullanır. Zaten diziyi dizi yapan
|
||
unsur elemanların aynı türden olması ve bellekte ardışıl bir biçimde bulunmasıdır.
|
||
|
||
malloc fonksiyuonun void bir adresle geri dönmesi oldukça anlamlıdır. Çünkü malloc fonksiyonu tahsis etmiş olduğu alanın programcı tarafından
|
||
ne niyetle (örneğin hangü türden dizi olarak) kullanılacğını bilmez. void adresler genel adres olarak da kullanılmaktadır.
|
||
|
||
Aşağıdaki örnekte malloc fonksiyonu ile klavyeden girilen miktar kadar int bir dizi tahsis edilmiştir. Sonra o diziye scanf fonksiyonu ile
|
||
değerler okunmuş ve okunan değerler yazdırılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
int *pi;
|
||
int size;
|
||
|
||
printf("Dizi ununlugu:");
|
||
scanf("%d", &size);
|
||
|
||
pi = (int *)malloc(size * sizeof(int));
|
||
if (pi == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
for (int i = 0; i < size; ++i) {
|
||
printf("%d. elemani giriniz:", i + 1);
|
||
scanf("%d", &pi[i]);
|
||
}
|
||
|
||
for (int i = 0; i < size; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte malloc fonksiyonu ile heap'te 100 byte tahsis edilmiştir. Tahsis edilen alanın adresi char türden bir göstericiye atanmıştır ve o alan
|
||
char türden bir dizi gibi kullanılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *str;
|
||
|
||
if ((str = (char *)malloc(100)) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
printf("Bir yazi giriniz:");
|
||
gets(str);
|
||
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte 5 elemanlı char türden bir gösterici dizisinin her elemanı için malloc ile 64 byte alan tahsis edilmiş ve o alana gets
|
||
ile okuma yapılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *names[5];
|
||
|
||
for (int i = 0; i < 5; ++i) {
|
||
if ((names[i] = (char *)malloc(64)) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
printf("Bir isim giriniz:");
|
||
gets(names[i]);
|
||
}
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
puts(names[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dinamik bellek fonksiyonlarıyla tahsis ettiğimiz alanlar kullanıldıktan sonra ne olacaktır? Yukarıda a belirtitğimiz gibi Windows, UNIX/Linux ve macOS
|
||
sistemlerinde heap alanı prosese özgüdür. Yani bu sistemlerde program bitince zaten programınb tüm bellek alanı heap alanı da dahil olmak üzere sisteme
|
||
iade edilmektedir. O halde bu sistemlerde en kötü olasılıkla program sonlandığında tahsis edilmiş olan dinamik alanlar free hale getirilecektir.
|
||
Ancak C standartlarında heap alanın prosese özgü olup olmadığı konusunda bir belirlemede bulunulmamıştır. Gerçekten de bazı nadir sistemlerde proses bittiği
|
||
halde prosesin yaptığı tahsisatlar otomatik boşaltılmayabilmektedir. Ayrıca programcının tahsis ettiği dinamik alanı kullandıktan sonra artık
|
||
o alanla bir işlemi kalmamışsa o alanı serbest bırakması iyi bir tekniktir. İşte daha önce tahsis edilmiş olan dinamik alanı serbest bırakmak için
|
||
frtee isimli standart C fonksiyonu kullanılmaktadır. free fonksiyonun prototipi şöyledir:
|
||
|
||
void free(void *ptr);
|
||
|
||
free fonksiyonu parametre olarak daha önce dinamik bellek fonksiyonlarıyla tahsis edilmiş olan bloğun başlangıç adresini alır. Tüm bloğu serbest bırakır.
|
||
Bloğun bir kısmının serbest bırakılması biçiminde bir olanak yoktur. Dinamik bellek fonksiyonları heap alanının tahsisatı için ortak bir tahsisat tablosu
|
||
oluşturmaktadır. Bu tahsşisat tablosunu tapu kadastro dairesine benzetebiliriz. Bu tahsisat tablosunda tahsis edilmiş olan alanlar bşalngıç adresleri ve
|
||
uzunluklarıyla kaydedilmektedir. Dolayısıyla bizim free fonksiyonuna daha önce tahsis edilmiş olan bloğun başlangıç adresini vermemiz gerekir.
|
||
Aksi takdirde fonksiyon tanımsız davranışa yol açmaktadır ve bu hatalı kullanımn programın çökmesine neden olabilmektedir.
|
||
|
||
free fonksiyonun geri dönüş değeri yoktur. Yani biz bu fonksiyonun başarılı olup olmadığını anlayamayız. Bize düşen görev fonksiyona daha önce tahsis edilmiş
|
||
olan bloğun başlangıç adresinidüzgün bir biçimde geçirmektedir. VBu durumda fonksiyon başarısz olmayacaktır.
|
||
|
||
Aşağıdaki örnekte önce malloc fonksiyonu ile dinamik bir alan tahsis edilmiş bu alan kullanıldıktan sonra da free fonksiyonu ile serbest bırakılmıştır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *str;
|
||
|
||
if ((str = (char *)malloc(100)) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
printf("Bir yazi giriniz:");
|
||
gets(str);
|
||
|
||
puts(str);
|
||
|
||
free(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte 5 elemanlı char türden göstericisinin her elemanı için 64 byte'lık alanlar dinamik olarak tehsis edilmiştir. Kullanım bittikten sonra
|
||
bu beş ayrı alan tek tek free hale getirilmiştir. Yukarıda da belirttiğimiz gibi aslında Windows, Linux ve macOS gibi
|
||
sistemlerde her prosesin ayrı heap alanı vardır. Bir proses sonlandığında zaten onun heap alanı da sisteme iade edilmektedir.
|
||
Yani aslında bu sistemlerde program sonlanırken daha önce tahsis etmiş olduğumuz alanların free hale getirilmesi gerekmez.
|
||
Ancak C genelinde böyle bir zorunluluğun olmadığını tekrar belirtmek istiyoruz. Bu nedenle aşağıdaki örnekte bir tahsisat
|
||
başarısızsa daha önce yapılan tahsisatlar da free hale getirilerek programdan çıkılmıştır:
|
||
|
||
if ((names[i] = (char *)malloc(64)) == NULL) {
|
||
for (int k = 0; k < i; ++k)
|
||
free(names[k]);
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
char *names[5];
|
||
|
||
for (int i = 0; i < 5; ++i) {
|
||
if ((names[i] = (char *)malloc(64)) == NULL) {
|
||
for (int k = 0; k < i; ++k)
|
||
free(names[k]);
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
printf("Bir isim giriniz:");
|
||
gets(names[i]);
|
||
}
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
puts(names[i]);
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
free(names[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
50. Ders - 01/12/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dinamik bellek fonksiyonlarıyla tahsis edilen alanlar free hale getirilene kadar yaşamaya devam eder. Tabii yukarıda
|
||
da belirttiğimiz gibi Windows, UNIX/Linux ve macOS gibi işletim sistemlerinde proses bittiğinde prosesin heap alanı
|
||
da boşaltılacağı için dinamik alanlar en kötü olasılıkla program bittiğinde serbest bırakılırlar. Bir fonksiyon içerisinde
|
||
dinamik tahsisat yaptığımızda fonksiyon sonlansa bile tahsis edilen tahsis alan edilmiş olarak kalmaktadır. Örneğin:
|
||
|
||
char *foo()
|
||
{
|
||
char *str;
|
||
|
||
if ((str = (char *)malloc(64)) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
return str;
|
||
}
|
||
|
||
Burada str göstericisi malloc ile tahsis edilmiş bloğu göstermektedir. Fonksiyon da str içerisindeki adresle geri dönmüştür. Fonksiyon bittiğinde
|
||
str yerel değişkeni yok edilecektir. Ancak onun içerisindeki adreste bulunan alan tahsis edilmiş olarak kalacaktır.
|
||
Tabii durada bu alanın free hale geitirilmesi artık fonksiyonu çağıranın sorumluluğundadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi hiçbir fonksiyon yerel değişkenin ya da dizinin adresiyle geri dönmemeliydi. Örneğin:
|
||
|
||
char *getname(void)
|
||
{
|
||
char name[1024];
|
||
|
||
printf("Adi soyadi:");
|
||
gets(name);
|
||
|
||
return name;
|
||
}
|
||
|
||
Burada getname fonksiyonu yerel name dizisinin adresiyle geri dönmüştür. Ancak fonksiyon bittiğinde fonksiyonun yerel değişkenleri yok edileceğinden
|
||
namme dizisi de yok edilecektir. O halde geri döndürülen adres aslında artık tahsis edilmiş olan bir adres olmayacaktır.
|
||
|
||
Ancak bir fonksiyon dinamik bir biçimde tahsis ettiği bir alanın adresiyle geri dönebilir. Örneğin:
|
||
|
||
char *getname(void)
|
||
{
|
||
char name[1024];
|
||
char *str;
|
||
|
||
printf("Adi soyadi:");
|
||
gets(name);
|
||
|
||
if ((str = (char *)malloc(strlen(name) + 1)) == NULL)
|
||
return NULL;
|
||
|
||
strcpy(str, name);
|
||
|
||
return str;
|
||
}
|
||
|
||
Burada bir sorun yoktur. Çünkü getname fonksiyonu dinamik tahsis edilmiş alanın başlanıç adresiyle geri dönmektedir. Fonksiyon bittiğinde
|
||
bu dinamik alan yaşamaya devam edecektir. Burada yerel dizi kullanılmasının nedeni malloc fonksiyonuyla tam gereken miktarda byte tahsis etmek içindir.
|
||
name dizisi nasıl olsa fonksiyon bittiğinde yok edilecektir. Ancak dinamik alan tahsis edilmiş olarak kalacaktır. Tabii burada getname fonksiyonun geri döndürdüğü
|
||
dinamik alanın free hale getirilmesi getname fonksiyonunu çağıranın sorumluluğundadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
char *getname(void)
|
||
{
|
||
char name[1024];
|
||
char *str;
|
||
|
||
printf("Adi soyadi:");
|
||
gets(name);
|
||
|
||
if ((str = (char *)malloc(strlen(name) + 1)) == NULL)
|
||
return NULL;
|
||
|
||
strcpy(str, name);
|
||
|
||
return str;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char *name;
|
||
|
||
if ((name = getname()) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
eit(EXIT_FAILURE);
|
||
}
|
||
puts(name);
|
||
|
||
free(name);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki örnekte olduğu gibi char türden bir yerel dizinin içeriğini dinamik olarak tahsis edilmiş bir alana kopyalamak
|
||
için strdup isimli standart bir C fonksiyonu da bulundurulmuştur. Fonksiyonun prototipi şöyledir:
|
||
|
||
#include <string.h>
|
||
|
||
char *strdup(const char *s);
|
||
|
||
Fonksiyon parametresiyle belirtilen adresteki yazı kadar (null karakter de dahil olmak üzere) alanı malloc ile tahsis
|
||
eder. Bu yazıyı tahsis ettiği alana kopyalar ve tahsis edilen alanın başlangıç adresine geri döner. Fonksiyon
|
||
başarısızlık durumunda NULL adrese geri dönmektedir. Örneğin:
|
||
|
||
char *getname(void)
|
||
{
|
||
char name[1024];
|
||
|
||
printf("Adi soyadi:");
|
||
gets(name);
|
||
|
||
return strdup(name);
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
char *getname(void)
|
||
{
|
||
char name[1024];
|
||
|
||
printf("Adi soyadi:");
|
||
gets(name);
|
||
|
||
return strdup(name);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char *name;
|
||
|
||
if ((name = getname()) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
eit(EXIT_FAILURE);
|
||
}
|
||
puts(name);
|
||
|
||
free(name);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dinamik olarak tahsis edilmiş bir alanın free fonksiyonuyla serbest bırakılması unutulursa ne olur? Böylesi durumlara programlamada "bellek sızıntısı (memory leak)"
|
||
denilmektedir. Bellek sızıntısı eğer önemli bir boyutta değilse bir sorun olarak kendini göstermeyebilir. Nasıl olsa Windows, UNIX/Linux ve macOS sistemlerinde
|
||
en kötü olasılıkla program bittiğinde bu alanlar serbest bırakılacaktır. Ancak C standartlarında daha önceden de belirtildiği gibi program bittiğinde
|
||
dinamik alanların serbest bırakılması garanti edilmemiştir. Bunun yanı sıra Windows, UNIX/Linux ve macOS sistemlerinde program bittiğinde tüm dinamik alanlar
|
||
sisteme iade ediliyor olsa bile bellek sızıntısı önemli bir sorun haline gelebilmektedir. Çok uzun süre (yıllarca) çalışan programlar vardır. Bu programlarda
|
||
küçük sızıntılar bile zamanla çok büyük miktarda alanın tahsis edilmiş olmasına yol açabilmektedir. Bu tür sızıntılarda heap alanı dolabilmekte ya da
|
||
sistemin sanal bellek alanı yetersiz kalabilmektedir. Bunlardan biri olmasa bile kronik bir sızıntı sistem performansını olumsuz etkileyebilir.
|
||
Bu nedenle C'de bellek sızıntısı önemli bir problem olarak değerlendirilmektedir. Programcının yaptığı dinamik tahsisatları işi bittiğinde serbest bırakması gerekir.
|
||
|
||
Aşağıdaki örnekte getname fonksiyonu her çağrıldığında dinamik alanın adresiyle geri dönmektedir. Ancakl programcı bu alanları free hale getirmemiştir.
|
||
Bu biçimdeki bellek sızıntılarına dikkat ediniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
char *getname(void)
|
||
{
|
||
char name[1024];
|
||
char *str;
|
||
|
||
printf("Adi soyadi:");
|
||
gets(name);
|
||
|
||
if ((str = (char *)malloc(strlen(name) + 1)) == NULL)
|
||
return NULL;
|
||
|
||
strcpy(str, name);
|
||
|
||
return str;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char *name;
|
||
|
||
for (;;) {
|
||
if ((name = getname()) == NULL) { /* dikkat! bellek sızıntısı (memory leak */
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
if (!strcmp(name, "quit"))
|
||
break;
|
||
puts(name);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Java, C# gibi dillerde tahsis edilen dinamik alanlar o dillerin framework'leri tarafından otomatik olarak yok edilmektedir. Bu mekanizmaya
|
||
"çöp toplayıcı (garbage collector)" denilmektedir. C'de böyle bir çöp toplayıcı mekanizma yoktur. C++'ta da yoktur. Dolayısıyla C'de kullanılmayan
|
||
dinamik alanların serbest bırakılması programcının sorumluluğundadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
calloc (count of allocation) fonksiyonu aslında taban (base) bir fonksiyon değildir. Bir sarma (wrapper) fonksiyon biçimindedir. calloc fonksiyonunun prototipi
|
||
şöyledir:
|
||
|
||
void *calloc(size_t nelem, size_t size);
|
||
|
||
calloc fonksiyonu iki parametresini çarpımı kadar ardışıl byte tahsis etmektedir. Geleneksel olarak ilk parametre tahsis edilecek dizinin eleman sayısını,
|
||
ikinci parametre dizinin bir elemanının byte uzunluğunu belirtir. Örneğin 10 elemanlı int bir diziyi dinamik olarak tahsis etmek için calloc
|
||
fonksiyonu şöyle kullanılır:
|
||
|
||
pi = (int *)calloc(10, sizeof(int));
|
||
|
||
calloc fonksiyonu yine başarı durumunda tahsis edilen alanıon başlangıç adresiyle, başarısızlık durumunda NULL adresle geri dönmektedir. Ancak calloc fonksiyonun malloc
|
||
fonksiyonundan asıl farkı calloc fonksiyonun tahsis ettiği alanı sıfırlamasıdır. Halbuki malloc fonksiyonu ile tahsis edilen alan içerisinde çöp değerler bulunmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
int *pi;
|
||
|
||
if ((pi = (int *)calloc(10, sizeof(int))) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
|
||
free(pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*--------------------------------------------------------------------------------------------------------------------------------------------------
|
||
calloc fonksiyonu malloc fonksiyonunu kullanarak aşağıdaki gibi yazılabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
void *mycalloc(size_t nelems, size_t size)
|
||
{
|
||
void *ptr;
|
||
|
||
if ((ptr = malloc(nelems * size)) == NULL)
|
||
return NULL;
|
||
|
||
return memset(ptr, 0, nelems * size);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int *pi;
|
||
|
||
if ((pi = (int *)mycalloc(10, sizeof(int))) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
|
||
free(pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
realloc fonksiyonu daha önce tahsis edilmiş olan dinamik alanı büyütmek ya da küçültmek çin kullanılmaktadır. Prototipi şöyledir:
|
||
|
||
void *realloc(void *ptr, size_t newsize);
|
||
|
||
Fonksiyonun birinci parametresi daha önce tahsis edilmiş olan dinamik alanın başlangıç adresini, ikinci parametresi ise bloğun arzu edilen
|
||
toplam yeni uzunluğunu belirtmektedir. ralloc fonksiyonu tipik olarak şöyle çalışmaktadır (ancak fonksiyonun bu biçimde çalışması standartda garanti
|
||
edilmemiştir): Eğer blok büyütülmek istenmişse fonksiyon daha önce tahasi edilmiş olan bloğun hemen altında toplam yeni uzunluk için yeteri kadar boş alanın
|
||
olup olmadığına bakar. Eğer daha önce tahsis edilmiş olan bloğun hemen altında toplam yeni uzunluk için yeterli alan varsa orayı da tahsis eder. Eğer eski
|
||
bloğun hemen altında toplam yeni uzunluğu karşılayacak kadar yeterli boş alan yoksa realloc bu sefer heap'in başka bir yerinde toplam yeni
|
||
uzunluk kadar alan tahsis etmeye çalışır. Eğer böyle bir alanı tahsis edebilirse eski alandaki bilgileri yeni alana kopyalar ve eski alanı free hale getirir.
|
||
realloc toplam yeni uzunluk kadar olan alanın başlangıç adresiyle geri dönmektedir. Programcı eski bloğun yer değiştirmiş olabileceğini göz önüne
|
||
almak zorundadır. Bu nedenle programcı her zaman realloc fonksiyonun geri döndürdüğü değeri dikkate almalıdır. Örneğin:
|
||
|
||
p = malloc(10)
|
||
//..
|
||
realloc(p, 20);
|
||
|
||
Bu kullanım hatalıdır. Çünkü p göstericisinin gösterdiği yerdeki blok yer değiştirmiş olabilir ve eğer blok yer değiştirdiyse programcı bunu dikkate almalıdır:
|
||
|
||
p = malloc(10);
|
||
//...
|
||
pnew = realloc(p, 20);
|
||
|
||
Eğer realloc fonksiyonu eski bloğun altında toplam yeni uzunluğu karşılayacak kadar yer bulamazsa ve heap'in başka bir yerinde de toplam yeni uzunluk kadar
|
||
yer bulamazsa başarısız olur ve NULL adresle geri döner. Tabii bu durumda eski blok free hale getirilmemektedir.
|
||
|
||
realloc fonksiyonu ile daha önce tahsis etmiş olduğumuz bloğu küçültmek de isteyebiliriz. Örneğin:
|
||
|
||
p = malloc(100);
|
||
//...
|
||
pnew = realloc(p, 50);
|
||
|
||
Burada 100 byte'lık blok 50 byte'a küçültülmüştür. realloc fonksiyonu bloğu küçültürken de bloğun yerini yerini değiştirebilmektedir. Dolayısıyla programcının
|
||
yine realloc fonklsiyonunun geri dönüş değerini dikkate alması gerekir.
|
||
|
||
Biz yukarıda realloc fonksiyonu ile daha önce tahsis edilmiş olan bloğu büyütürken realloc fonksiyonun tipik olarak önce eski bloğun altında
|
||
toplam yeni uzunluğu karşılayacak kadar yer var mı diye baktığını, eğer yer varsa hemen orayı da tahsis ettiğini söylemiştik. C Standartları
|
||
realloc fonksiyonun önce eski bloğun altına bakacağı yönünde bir ifade kullanmamıştır. Dolayısıyla biz burada tipik gerçekleştirimin bu biçimde
|
||
olduğunu belirttik. Bir derleyicideki reealloc fonksiyonu eski bloğun aşağısına bakmadan heap'in başka bir yerinde toplam yeni uzunluk kadar
|
||
yer araştırabilir.
|
||
|
||
realloc fonksiyonun birinci parametresi NULL adres geçilirse realloc tamamen malloc gibi davranmaktadır. Yani realloc(NULL, n) çağrısı ile malloc(n)
|
||
çağrısı tamamen eşdeğerdir.
|
||
|
||
realloc fonksiyonu ile bloğu büyüttüğümüzde büyütülen kısımda çöp değerler bulunmaktadır. Yani büyütülen kısım sıfırlanmamaktadır.
|
||
|
||
realloc ile normal diziler büyütülüp küçültülemezler. Ancak dinamik biçimde tahsis edilmiş (yani malloc ya da calloc ile ya da realloc ile tahsis edilmiş) alanlar
|
||
büyütülüp küçültülebilirler.
|
||
|
||
realloc fonksiyonunu kullanırken fonksiyonun başarısız olduğu durumda eski bloğu kaybetmemek için geri dönüş değerini önce başka bir göstericye atayıp
|
||
sonra asıl gösteriye atayabilirsiniz. Örneğin:
|
||
|
||
for (n = 0;; ++n) {
|
||
//...
|
||
if ((pnew = realloc(pi, (n + 1) * sizeof(int))) == NULL) {
|
||
fprintf(stderr,i "cannot realloc memory!..\n);
|
||
break;
|
||
}
|
||
pi = pnew;
|
||
//...
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
realloc fonksiyonu dibnamik biçimde büyütülen dizilerde sıkça kullanılmaktadır. Dinamik olarak büyütülen dizi demekle gerektiği zaman büyütülen dizi
|
||
anlaşılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
int *pi, *pnew;
|
||
int n;
|
||
|
||
pi = NULL;
|
||
for (n = 0;; ++n) {
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &val);
|
||
if (val == 0)
|
||
break;
|
||
if ((pnew = (int *)realloc(pi, (n + 1) * sizeof(int))) == NULL) {
|
||
fprintf(stderr, "cannot realloc memory!..\n");
|
||
break;
|
||
}
|
||
pi = pnew;
|
||
pi[n] = val;
|
||
}
|
||
|
||
for (int i = 0; i < n; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
|
||
free(pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki gibi dinamik büyütülen dizilerde aslında büyütme bi,rer birer yapılmamaktadır. Çünkü bir eleman için yeniden realloc fonksiyonun çağrılması
|
||
oldukça maliyetlidir. Şimdi yukarıdaki örneği büyütmeyi birer birer değil CHUNK_SIZE kadar yapacak biçimde değiştirelim.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
#define CHUNK_SIZE 10
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
int *pi, *pnew;
|
||
int n;
|
||
|
||
pi = NULL;
|
||
for (n = 0;; ++n) {
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &val);
|
||
if (val == 0)
|
||
break;
|
||
if (n % CHUNK_SIZE == 0) {
|
||
if ((pnew = (int *)realloc(pi, (n + CHUNK_SIZE) * sizeof(int))) == NULL) {
|
||
fprintf(stderr, "cannot realloc memory!..\n");
|
||
break;
|
||
}
|
||
pi = pnew;
|
||
}
|
||
pi[n] = val;
|
||
}
|
||
|
||
for (int i = 0; i < n; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
|
||
free(pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında dinamik büyütülen dizilerde büyütme genellikle eskisinin iki katı olacak biçimde (yani geometrik biçimde) yapılmaktadır. Bunun nedenini
|
||
burada açıklamayacağız. Ancak eğer dizi 1'den başlatılacaksa tipik olarak 1, 2, 4, 8, 16, 32, 64, 128, 256 biçiminde büyütme uygulanmaktadır.
|
||
Bu biçimde dinamik büyütme gerçekleştiriminde tipik olarak programcı iki uzunlşuğu tutar: Toplam tahsis edilmiş eleman uzunluğu ve dolu eleman uzunluğu.
|
||
Genellikle toplam tahsis edilmişl eleman uzunluğuna "capacity", dolu eleman uzunlupuna da "size" denilmektedir. İşin başında size = 0 biçimindedir.
|
||
Her adımda size değeri bir artırılır. size değeri capacity değerine eriştiğinde eskisinin iki katı olacak biçimde yeniden tahsisat yapılır.
|
||
|
||
Aşağıdaki örnekte dinamik dizi için işin başında DEF_CAPACITY kadar yer ayrılmıştır. Sonra size değeri capacity değerine eriştiğinde capacity iki
|
||
kat artırılarak blok eskisinin iki katı büyüklükte olacak biçimde büyütülmüştür.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
#define DEF_CAPACITY 4
|
||
|
||
int main(void)
|
||
{
|
||
int val;
|
||
int *pi, *pnew;
|
||
size_t size, capacity;
|
||
|
||
size = 0;
|
||
capacity = DEF_CAPACITY;
|
||
|
||
if ((pi = (int *)malloc(DEF_CAPACITY * sizeof(int))) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (;;) {
|
||
printf("Bir deger giriniz:");
|
||
scanf("%d", &val);
|
||
if (val == 0)
|
||
break;
|
||
if (size == capacity) {
|
||
if ((pnew = (int *)realloc(pi, (capacity * 2) * sizeof(int))) == NULL) {
|
||
fprintf(stderr, "cannot reallocate memory!..\n");
|
||
break;
|
||
}
|
||
pi = pnew;
|
||
capacity *= 2;
|
||
}
|
||
pi[size] = val;
|
||
++size;
|
||
}
|
||
|
||
for (int i = 0; i < size; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
|
||
free(pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
51. Ders - 08/12/2022 Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'nin dil tarafından desteklenen veri yapılarından biri de yapıladır. Elemanları bellekte ardışıl bir biçimde bulunan fakat farklı türlerden olabilen
|
||
veri yapılarına "yapı (structure)" denilmektedir. Yapılarla diziler birbirlerine oldukça benzemektedir. Hem dizilerin hem de yapıların elemanları bellekte
|
||
ardışıl bir biçimde bulunmaktadır. Ancak dizi elemanlarının hepsi aynı türden iken yapı elemanları farklı türlerden olabilmektedir. Derleyiciler
|
||
yapı elemanlarının arasında "padding" denilen kontrolü boşluklar bırakabilmektedir. Bu olguya "hizalama (alignment)" denilmektedir. Hizalama
|
||
yapı elemanlarının ardışıllığını bozmaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapıyla çalışmadan önce yapı elemanlarının türleri ve isimleri derleyiciye bildirilir. Buna "yapı bildirimi (structure declaration)" denilmektedir.
|
||
Yapı bildiriminin genel biçimi şöyledir:
|
||
|
||
struct <yapı_ismi> {
|
||
<yapı eleman bildirimleri>
|
||
};
|
||
|
||
Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
Örneğin:
|
||
|
||
struct point {
|
||
int x, y;
|
||
};
|
||
|
||
struct Person {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
Yapı isimlerinin Kernighan & Ritchie stilinde genellikle tüm karakterleri büyük harflerden oluşturulmaktadır. Ancak bazı programcılar yapı isimleribib
|
||
tüm karakterlerini küçük harflerle oluşturabildiği gibi bazı programcılar da yapı isimlerinin yalnızca ilk karakterlerini (ve sonraki sözcüklerin ilk karakterlerini)
|
||
büyük harflerden diğerlerini küçük harflerden oluşturabilmektedir. Biz kursumuzda daha çok yapı isimlerini büyük harflerle oluşturacağız. Ancak bazı örneklerde
|
||
de diğer harflendirme biçimlerini kullanacağız.
|
||
|
||
C'de yapı bildiriminin içi boş olamaz. En az bir yapı eelemanının bildirilmesi gerekmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapı bildirildiği zaman aynı zamanda bir tür de oluşurulmuş olur. Yapı bildirimi ile oluşturulan türün ismi struct anahtar sçzüğü ile yapı isminden
|
||
oluşmaktadır. Örneğin:
|
||
|
||
struct POINT {
|
||
int x, y;
|
||
};
|
||
|
||
Burada oluşturulan bu türün ismi "POINT" değil "struct POINT" biçimindedir. Örmeğin:
|
||
|
||
struct date {
|
||
int day;
|
||
int month;
|
||
int year;
|
||
};
|
||
|
||
Burada oluşturulmuş olan türün ismi ise "struct date" biçimindedir.
|
||
|
||
Bir yapı bildirildikten sonra oluşturulan tür ismi kullanılarak o yapı türünden nesneler yaratılabilir. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
struct SAMPLE s, k;
|
||
|
||
Burada s ve k nesneleri struct SAMPLE türündendir.
|
||
|
||
Bir yapı bildirimi tanımlama değildir. Dolayısıyla biz bir yapı bildirdiğimizde yalnızca derleyiciye bilgi vermiş oluruz. Yapı bildirimini gören derleyici
|
||
bellekte bizim için bir yer ayırmaz. Bellekte yer ayırma işlemi o yapı türünden nesler tanımlandığında yapılmaktadır. Örneğin:
|
||
|
||
struct DATE { /* bildirim, bellekte yer ayrılmaz */
|
||
int day, month, year;
|
||
};
|
||
|
||
struct DATE d; /* tanımlama, bellekte yer ayrılır */
|
||
|
||
Yapı nesneleri "bileşik (compound)" nesnelerdir. Yani parçalardan oluşmaktadır. Yapı bildiriminde belirtilen elemanlar o yapı türünden nesneler
|
||
tanımlandığında onların parçalarını belirtmektedir. Yapı bildiriminde ilk yazılan eleman düşük adreste olacak biçimde ardışıl bir yerleşim uygulanmaktadır.
|
||
(Ancak yukarıda da belirtildiği gibi kontrollü padding denilen boşluklar bırakılabilmektedir.) Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
struct SAMPLE s;
|
||
|
||
Burada s üç parçadan oluşmaktadır. Parçaların isimleri a , b ve c'dir. s'in a parçası en düşük adreste bulunur. Onu b parçası onu da c parçası izler.
|
||
Bir yapı nesnesi yoluyla yapının belli bir elemanına "nokta operatörü" ile erişilir. Nokta operatörü iki operand'lı araek bir operatördür. Nokta operatörünün
|
||
sol tarafındaki operand bir yapı türünden nesneyi, sağ tarafındaki operand ise o yapının bir elemanını belirtmek zorundadır. Örneğin s.a ifadesinde
|
||
s bir yapı nesnesidir, a ise o yapının bir elemanıdır. Nokta opetratörü öncelik tablosunun en üst grubunda bulunmaktadır:
|
||
|
||
() [] . Soldan-Sağa
|
||
+ - ++ -- ! & * sizeof (tür) Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
struct SAMPLE s;
|
||
|
||
Burada s "strct SAMPLE" türündendir. Ancak s.a int tüdendir. Benzer biçimde s.b long türden, s.c ise double türdendir. Biz yapının elemanlarını bağımsız nesneler
|
||
gibi kullanabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
|
||
s.a = 10;
|
||
s.b = 20;
|
||
s.c = 30.2;
|
||
|
||
printf("%d, %ld, %f\n", s.a, s.b, s.c);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapı elemanları bildirimde ilk yazılan eleman düşük adreste olacak biçimde ardışıldır. Aşağıdaki örnekte yapı nesnesi içerisindeki elemanların
|
||
adresleri yazdırılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct DATE d;
|
||
|
||
printf("%p\n", &d.day);
|
||
printf("%p\n", &d.month);
|
||
printf("%p\n", &d.year);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapı bildirmi global düzeyde de yapılabilir, yerel düzeyde de yapılabilir. Eğer yapı bildirimi global düzeyde yapılırsa o yapının ismi her yerde
|
||
kullanılabilir. Dolyısıyla biz her fonksiyon da istersek o yapı türünden nesneler yaratabiliriz. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct DATE d; /* geçerli */
|
||
|
||
/* ...*/
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo(void)
|
||
{
|
||
struct DATE x; /* geçerli */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
struct DATE m; /* geçerli */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
Ancak yapı bildirimleri yerel düzeyde yapılırsa bildirimle oluşturulan tür ismi ancak o blokta kullanılabilir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct DATE d; /* geçerli */
|
||
|
||
/* ...*/
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo(void)
|
||
{
|
||
struct DATE x; /* geçersiz! */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
struct DATE m; /* geçersiz! */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
C90'da tüm yerel bildirimler blokların başında yapılmak zorunda olduğu için yerel yapı bildirimleri de blokların başlarında yapılmak zorundadır.
|
||
Anımsayacağınız gibi C99 ve sonrasında yerel değişkenlerin bildirimleri blokların herhangi bir yerinde yapılabilir hale getirilmiştir.
|
||
|
||
Hemen her zaman programcılar yapı bildirimlerini global düzeyde yaparlar. Bazen yapı bildirimleri başlık dosyalarının içerisine yerleştirilir.
|
||
Onlar include edildiği zaman derleyici o bildirimleri görmüş olur.
|
||
|
||
Aynı faaliyet alanında yapı ismiyle aynı isimli başka türden bir değişken bildirimi de yapılabilir. Bu durumda bu değişken ismi kullanıldığında başka türden olan ismin
|
||
kullanıldığı kabul edilir. Ancak struct anahtar sözcüğü ile yapı ismi kullanıdığında artık yapıya ilişkin tür ismi kullanılmış olur. Örneğin:
|
||
|
||
struct point {
|
||
int x, y;
|
||
};
|
||
|
||
int point; /* geçerli */
|
||
|
||
/* ... */
|
||
|
||
point = 10; /* int olan point kullnımış */
|
||
struct point pt; /* yapı ismi olan point kullanılmış */
|
||
|
||
Tabii bir yapı ile aynı isimde o yapı türünden bir nesne de tanımlayabiliriz. Örneğin:
|
||
|
||
struct point {
|
||
int x, y;
|
||
};
|
||
|
||
struct point point; /* geçerli */
|
||
|
||
point.x = 10; /* geçerli */
|
||
point.y = 20; /* geçerli */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapı bildiriminde yapı elemanlarına ilkdeğer verme anlamsız geçersiz bir işlemdir. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a = 10; /* geçersiz! */
|
||
long b = 20; /* geçersiz! */
|
||
};
|
||
|
||
Yukarıdaki işlem bir bildirim işlemidir. Bu bildirim işlemiyle a ve b için bir yer ayrılmaz. Dolayısıyla oraya değer atamak da anlamsız ve geçersizdir.
|
||
Ancak bir yapı nesnesinin parçalarına bildirim sırasında tıpkı dizilerde olduğu gibi küme parantezleri içerisinde değer atayabiliriz. Örneğin:
|
||
|
||
struct POINT {
|
||
int x, y;
|
||
};
|
||
|
||
struct POINT pt = {10, 20};
|
||
|
||
Bu durumda küme parantezlerinin içerisindeki değerler sırasıyla yapı nesnesinin parçalarına atanmaktadır. Yani yukarıdaki örnekte 10 değeri pt.x elemanına,
|
||
20 değeri de pt.y elemanına atanacaktır.
|
||
|
||
Yapı nesnelerine küme parantezleri ile ilkdeğer verilirken eksik sayıda elemana ilkdeğer verebiliriz. Yine dizilerde olduğu gibi geri kalan elemanlara
|
||
yapı nesnesi global da olsa yerel de olsa 0 yerleştirilmektedir. Yapı nesnesine küme parantezleri içerisinde ilkdeğer verilirken küme parantezlerinin
|
||
içi boş bırakılamaz. Örneğin:
|
||
|
||
struct POINT pt = {}; // geçersiz!
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {10};
|
||
|
||
printf("%d, %ld, %f\n", s.a, s.b, s.c); /* 10, 0, 0.000000 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C99 ile birlikte "designated initializer" denilen sentaks yapılar için de oluşturulmuştur. Bu sayede C99 ve ötesinde yapının yalnızca belirli elemanlarına
|
||
ilkdeğerler verilebilmektedir. Yapılar için designated initializer sentaksı ".<eleman_ismi> = <değer>" biçimindedir. Yine bu sentakstan sonraki değerler
|
||
bu sentaksta belirtilen elemandan sonraki elemanlara sırasıyla atanmaktadır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a, b, c, d, e, f, g, h;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {1, 2, .e = 10, 4, .g = 20, 30};
|
||
|
||
printf("%d %d %d %d %d %d %d %d\n", s.a, s.b, s.c, s.d, s.e, s.f, s.g, s.h); /* 1 2 0 0 10 4 20 30 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Yine aynı elemana birden fazla kez değer atama geçerli kabul edilmektedir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a, b, c, d, e, f, g, h;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {1, 2, 3, 4, .b = 10, 20};
|
||
|
||
printf("%d %d %d %d %d %d %d %d\n", s.a, s.b, s.c, s.d, s.e, s.f, s.g, s.h); /* 1 10 20 4 0 0 0 0 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada yapının b ve c elemanlarına iki kez değer atanmıştır. Tabii son atanan değerler bu elemanlarda kalmaktadır. Yine değer atanmayan elemanlarda
|
||
0 değerinin bulunduğuna dikkat ediniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İlkdeğer verilmemiş yapı nesnelerinin elemanlarında nesne yerel ise çöp değerler, nesne global ise 0 değerleri bulunmaktadır. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct DATE x; /* x'in elemanlarında 0 değerleri var */
|
||
|
||
void foo(void)
|
||
{
|
||
struct DATE d; /* d'nin elemanlarında çöp değerler var */
|
||
|
||
/* ... */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapı nesneleri bütünsel olarak artimetik işlemlere sokulamaz, karşılaştırma işlemlerine de sokulamaz. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct DATE x = {10, 12, 2006};
|
||
struct DATE y = {11, 10, 2013};
|
||
|
||
if (x > y) { /* geçersiz! */
|
||
/* ... */
|
||
}
|
||
|
||
Yapı nesnelerinin parçalarına erişerek parçaları üzerinde işlemler yapabiliriz.
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
Ancak aynı türden olmak koşuluyla iki yapı nesnesi bütünsel olarak birbirlerine atanabilmektedir. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct DATE x = {10, 12, 2006};
|
||
struct DATE y;
|
||
|
||
y = x; /* geçerli */
|
||
|
||
Bu durumda yapının karşılıklı elemanları birbirine atanmaktadır. (Ancak derleyici elemanlar arasında hizalama amacıyla "padding" boşlukları bırakmışsa
|
||
bu boşlukların atanması konusunda bir garanti verilmemiştir. Hizalama (alignment) ve padding konusu izleyen paragraflarda ele alınmaktadır.)
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day;
|
||
int month;
|
||
int year;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct DATE d = {10, 12, 1993};
|
||
struct DATE k;
|
||
|
||
k = d;
|
||
|
||
printf("%d/%d/%d\n", k.day, k.month, k.year); /* 10/12/1993 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İki yapının içeriği aynı olsa bile bu iki yapı aynı türden değildir. Dolayısıyla bu iki yapı türünden nesne biribirine atanamaz. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct MATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct DATE d = {10, 12, 2001};
|
||
struct MATE m;
|
||
|
||
m = d; /* geçersiz! yapılar farklı türlerden */
|
||
|
||
Burada iki struct DATE nesnesi biribirine atanabilir. İki struct MATE nesnesi de biribirine atanabilir. Ancak struct DATE ile struct MATE nesneleri birbirine
|
||
atanamazlar.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapının elemanı bir dizi olabilir. Örneğin:
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
Burada yapının name elemanı 32 elemanlık char türden bir dizidir. Bu yapı türünden bir nesne yaratalım:
|
||
|
||
struct PERSON per;
|
||
|
||
Bir yapı nesnesi yoluyla nokta operatörü kullanılarak yapının dizi elemanına erişilirse yine bu dizi ismi bir nesne belirtmez. Yapı içerisindeki dizinin
|
||
tüm bellekteki başlangıç adresini belirtir. Örneğin per.name ifadesi per yapı nesnesi içerisindeki name dizisinin tüm bellekteki başlangıç adresini
|
||
belirtmektedir. per.name ifadesi char * türündendir. Yani char türden bir adres belirtmektedir. per.no ifadesi ise int türdendir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per;
|
||
|
||
strcpy(per.name, "Ali Serce");
|
||
per.no = 123;
|
||
|
||
printf("%s, %d\n", per.name, per.no);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
52. Ders - 13/12/2022 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapının elemanı bir dizi ise yapı nesnesi yoluyla dizinin elemanlarına erişirken nokta ve [] operatörleri aynı ifade içerisinde kullanılmaktadır.
|
||
Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a[5];
|
||
double b;
|
||
};
|
||
|
||
struct SAMPLE s;
|
||
|
||
s.a[3] = 10;
|
||
|
||
Burada önce nokta operatör üsonra [] operatörü, en sonunda da atama operatörü yapılır:
|
||
|
||
İ1: s.a (buradan int türdne bir adres elde edilecek)
|
||
İ2: İ1[3] (yapı nesnesi içerisindeki dizinin 3'üncü indisli elemanına erişildi)
|
||
İ3: İ2 = 10
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapı nesnesine ilkdeğer verilirken yapının elemanı dizi ise diziye de ilkdeğer verilebilir. Bu tür durumlarda aslında iç içe küme parantezlerini
|
||
kullanmak zorunlu değildir. Ancak iyi bir tekniktir. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a[5];
|
||
double b;
|
||
};
|
||
|
||
struct SAMPLE s = { 1, 2, 3, 4, 5, 3.14 };
|
||
|
||
Burada iç küme parantezi kullanılmamıştır. Bu durumda derleyici küme parantezlerinin içerisindeki değerleri yapı nesnesinin içerisindeki de diziye yerleştirir.
|
||
Dizi bittikten sonra geri kalan elemanları dizi elemanından sonraki elemanlara yerleştirecektir. Bu örnekte b elemanına 3.14 değeri yerleştirilecektir:
|
||
|
||
s.a[0] => 1
|
||
s.a[1] => 2
|
||
s.a[2] => 3
|
||
s.a[3] => 4
|
||
s.a[4] => 5
|
||
s.c => 3.14
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a[5];
|
||
double b;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {1, 2, 3, 4, 5, 3.14};
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
printf("%d ", s.a[i]);
|
||
printf("\n%f\n", s.b);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapının elemanı bir dizi ise ilkdeğer verme sırasında bu dizi için de küme parantezleri bulundurulabilir. Bu tür durumlarda iç içe küme parantezleri
|
||
okunabilirliği artırmktadır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a[5];
|
||
double b;
|
||
};
|
||
|
||
struct SAMPLE s = {{1, 2, 3, 4, 5}, 3.14};
|
||
|
||
İç içe küme parantezi kullanmaının şöyle bir faydası vardır: Dizi için yanlışlıkla az eleman girilse bile sonraki elemanların yerleri değişmez. Örneğin:
|
||
|
||
struct SAMPLE s = {1, 2, 3, 4, 3.14}; /* geçerli ancak muhtemelen programcının istediği bu değil */
|
||
|
||
Burada programcı muhtemelen yanlışlıkla yapı içerisindeki dizinin son elemanını girmeyi unutmuştur. 3.14 değeri bu dizinin son elemanı gibi ele alınacaktır.
|
||
Dolayısıyla yapının b elemanı 0 değerinde kalacaktır:
|
||
|
||
s.a[0] => 1
|
||
s.a[1] => 2
|
||
s.a[2] => 3
|
||
s.a[3] => 4
|
||
s.a[4] => 3
|
||
s.c => 0
|
||
|
||
Halbuki biz iç küme parantezini de kullansaydık bu durumda a dizinin son elemanı 0 olacak ancak b elemanı 3.14 değerini
|
||
alacaktı. Bu durum diğerinden daha anlamlıdır:
|
||
|
||
struct SAMPLE s = {{1, 2, 3, 4}, 3.14};
|
||
|
||
s.a[0] => 1
|
||
s.a[1] => 2
|
||
s.a[2] => 3
|
||
s.a[3] => 4
|
||
s.a[4] => 0
|
||
s.c => 3.14
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a[5];
|
||
double b;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {{1, 2, 3, 4}, 3.14};
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
printf("%d ", s.a[i]); /* 1 2 3 4 0 */
|
||
printf("\n%f\n", s.b); /* 3.140000 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
char türden, unsigned char türden ve signed char türden dizilere iki tırnak ile ilkdeğer verebiliyorduk. Ancak bu istisna durumda iki tırnak bir adres
|
||
belirtmiyordu. Bu sentaks "iki tırnağın içerisindeki karakterleri tek tek diziye yerleştir" anlamına geliyordu. O halde aynı anlam yapının içrisinde
|
||
de geçerlidir. Örneğin:
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
struct PERSON per = {"Ali Serce", 123}; /* geçerli */
|
||
|
||
Burada "ALi Serce" yazısı bir string belirtmez. Yazının karakterleri per nesnesinin içerisindeki name dizisinin elemanlarına ,
|
||
yerleştirilecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per = {"Ali Serce", 123};
|
||
|
||
printf("%s %d\n", per.name, per.no);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapının içeisindeki dizi isminin de nesne belirtmediğine dikkat ediniz. Örneğin:
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
struct PERSON per;
|
||
|
||
per.name = "Ali Serce"; /* geçersiz! */
|
||
|
||
Burada dizi ismine bir adres atanmak istenmiştir. Bu durum geçersizdir. Böylesi bir şeyi ilkdeğer verme dışında yine strcpy fonksiyonu ile yapmalıyız.
|
||
Örneğin:
|
||
|
||
struct PERSON per;
|
||
|
||
strcpy(per.name, "Ali Serce");
|
||
per.no = 123;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapının elemanı gösterici de olabilir. Ancak tabii yapı nesnesi tanımlandığında bu gösterici içerisinde rastgele bir adres vardır. Örneğin:
|
||
|
||
struct PERSON {
|
||
char *name;
|
||
int no;
|
||
};
|
||
|
||
struct PERSON per;
|
||
|
||
Burada per nesnesi yerel ise name göstericisinin içerisinde rastgele bir adres, no içerisinde de rastgele bir değer bulunur. Ancak per global ise
|
||
name göstericisinin içerisinde NULL adres, no içerisinde de 0 bulunacaktır. (Anımsanacağı gibi global bir göstericiye değer atanmadıysa içeisinde NULL adres bulunacağı
|
||
garanti edilmiştir.) Örneğin:
|
||
|
||
struct PERSON per;
|
||
|
||
strcpy(per.name, "Ali Serce"); /* dikkat yazı rastgele bir adrese atanıyor */
|
||
|
||
Burada "Ali Serce" yazısı per.name adresinden itibaren bellekte rastgele bir yere yerleştirilecektir. Dolayısıyla tanımsız davranış söz konusudur.
|
||
Ancak örneğin:
|
||
|
||
per.name = "Ali Serce"; /* tamamen normal */
|
||
|
||
Burada "Ali Serce" yazısı derleyici tarafından char türden statik ömürlü bir diziye yerleştirilip o alanın adresi per.name göstericisine atanmıştır.
|
||
Dolayısıyla per.name göstericisi güvenli (yani tahsis edilmiş) bir yeri göstermektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct PERSON {
|
||
char *name;
|
||
int no;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per;
|
||
|
||
per.name = "Ali Serce"; /* tamamen normal, name göstericisine güvenli bir yazının adresi atanıyor */
|
||
per.no = 123;
|
||
printf("%s %d\n", per.name, per.no);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii yapının elemanı char türden bir gösterici ise biz yine iki tırnak içerisinde bu elemana ilkdeğer verebiliriz. Şüphesiz bu durumda verilen ilkdeğer
|
||
aslında bir adrestir ve gösteriye bu adres atanmış olmaktadır. Örneğin:
|
||
|
||
struct PERSON {
|
||
char *name;
|
||
int no;
|
||
};
|
||
|
||
struct PERSON per = {"Ali Serce", 123};
|
||
|
||
Burada artık per.name göstericisinin içerisinde "Ali Serce" yazısının başlangıç adresi bulunacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct PERSON {
|
||
char *name;
|
||
int no;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per = {"Ali Serce", 123};
|
||
|
||
printf("%s %d\n", per.name, per.no);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapı türünden nesneler bileşik nesnelerdir. Ancak biz bir yapı nesnesinin adresini & operatörü ile alabiliriz. Bu durumda elde edilen adresin tür bileşeni
|
||
ilgi yapı türünden, sayısal bileşeni de bellekteki yapı nesnenin başlagıcına ilişkin doğrusal adresten oluşur. Bir yapı nesnesinin adresi ancak aynı türden bir
|
||
yapı göstericisine artanabilir. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
struct SAMPLE s;
|
||
struct SAMPLE *ps;
|
||
|
||
ps = &s;
|
||
|
||
Burada artık ps göstericisi s nesnesini göstermektedir. Bir yapı tründen adresi biz * (indirection) operatörü ile kullanırsak o yapı göstericisinin gösterdiği yerdeki
|
||
yapı nesnesinin tamamına erişmiş oluruz. Yukarıdaki örnekte *ps ifadesi struct SAMPLE türündendir ve nesnenin bütününü temsil etmektedir. Başka bir deyişle *ps
|
||
ile s tamamen aynı nesneyi belirtmektedir.
|
||
|
||
ps bir yapı türünden adres a da ilgili yapının bir elemanı olmak üzere ps adresinin gösterdiği yerdeki yapı nesnesinin a elemanına erişmek için (*ps).a
|
||
ifadesi kullanılır. Burada *ps'nin parantez içerine alındığına dikkat ediniz. Nokta operatörünün sol tarafındaki operand bir yapı nesnesinin bütünü olmak zorundadır.
|
||
Eğer burada parantezler kullanılmazsa *ps.a ifadesi geçersiz olur. Çünkü nokta operatörü * operatöründen daha önceliklidir. Dolayısıyla burada önce
|
||
nokta operatörü sonra buna * operatörü uygulanacaktır. Nokta operatörünün sol tarafındaki operand bir yapı adresi olamaz. Yapı nesnesi olmak zorundadır.
|
||
Dolayısıyla eleman erişimde * operatörünün paranteze alınması gerekmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {10, 12, 14.25};
|
||
struct SAMPLE *ps;
|
||
|
||
ps = &s;
|
||
|
||
printf("%d\n", (*ps).a);
|
||
printf("%ld\n", (*ps).b);
|
||
printf("%f\n", (*ps).c);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapı türündne göstericilerle yapı elemanlarına erişirken * operatörünü paranteze almayı unutmayınız. Nokta operatörünnü solunda bir yapı nesnesi bulunmalıdır.
|
||
Nokta operatör bir yapı nesnesi ile yapının elemanına erişmekte kullanılır. Eğer elimizde bir yapı türünden adres varsa önce * ile o nesneyi elde edip
|
||
sonra nokta operatörünü uygulamalıyız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {10, 12, 14.25};
|
||
struct SAMPLE *ps;
|
||
|
||
ps = &s;
|
||
|
||
printf("%d\n", (*ps).a);
|
||
printf("%ld\n", (*ps).b);
|
||
printf("%f\n", (*ps).c);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Standartlara göre yapı elemanları bildirimde ilk belirtilen eleman düşük adreste olacak biçimde ardışıl yerleşim uygulanmaktadır.
|
||
Yapının ilk elemanı en düşük adreste bulunacağına göre bir yapı nesnesinin adresinin sayısal bileşeni ile onun ilk elemanının adresinin sayısal bileşeni aynıdır.
|
||
Ancak bunların tür bileşenleri farklıdır. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct DATE d;
|
||
|
||
Burada &d ifadesi struct DATE * türündendir. Halbuki &d.a ifadesi int * türündendir. Ancak bunların sayısal bileşenleri aynıdır. Çünkü yapı nesnesinin
|
||
hemen başında yapının ilk elemanı bulunmak zorundadır. Bir yapı elemanın adresi alınırken parantez gerekmediğine dikkat ediniz. Örneğin &d.day ifadesinde
|
||
önce d.day işlemi yapılacak ve yapının day elemanına erişilecek sonra da bu elemanın adresi alınacaktır.Nokta operatörünün & operatöründen daha yüksek
|
||
öncelikli olduğunu anımsayınız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapı göstericisi ile yapı elemanına çok sık erişilmektedir. Yani ps bir yapı türünden adres a da bu yapının bir elemanı olmak üzere (*ps).a gibi ifadeler
|
||
çok sık kullanılmaktadır. İşte bu tür ifadeleri daha kolay yazabilmek için "Ok operatörü (arrow operator)" düşünülmüştür. Ok operatörü -> karakterleriyle
|
||
temsil edilmektedir. -> operatörünün solundaki operand bir yapı türünden adres sağındaki operand ise o yapının bir elemanı olmak zorundadır. (*ps).a ifadesi
|
||
ile ps->a ifadesi tamamen eşdeğerdir. Böylece biz (*ps).a gibi bir ifade yerine ps->a ifadesini yazım bakımındanm tercih ederiz.
|
||
|
||
Ok operatörü iki operand'lı araek bir adres operatörüdür. Ok operatörünün solunda bir yapı türünden adres sağında ise yapının bir elemanı bulunmak zorundadır.
|
||
Operatör o adresin gösterdiği yerdeki yapı nesnesinin ilgili elemanına erişmek için kullanılmaktadır. Örneğin:
|
||
|
||
struct SAMPLE s;
|
||
struct SAMPLE *ps;
|
||
|
||
ps = &s;
|
||
|
||
Burada biz s nesnesinin a parçasına s.a ifadesiyle ya da (*ps).a ifadesi ile erişebiliriz. İşte ps göstericisinin gösterdiği yerdeki nesnenin
|
||
a parçasına ps->a ifadesi ile de erişebiliriz. (*ps).a ifadesi ile ps->a ifadesi tamamen aynı anlama gelmektedir.
|
||
|
||
Nokta ve ok operatörlerini birbirine karıştırmayınız. Her iki operatör de yapının elemanına erişmekte kullanılır. Yani bunların sağ tarafındaki operand'lar
|
||
yapının elemanını belirtir. Ancak nokta operatörünün solundaki operand yapı nesnesinin bütünüyken, ok operatörünün solundaki operand yapı nesnesinin
|
||
adresi olmak zorundadır.
|
||
|
||
Ok operatörü öncelik tablosunun en yüksek düzeyinde soldan-sağa grupta bulunmaktadır:
|
||
|
||
() [] . -> Soldan-Sağa
|
||
+ - ++ -- ! & * sizeof (tür) Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct DATE d = {10, 12, 2009};
|
||
struct DATE *pd;
|
||
|
||
pd = &d;
|
||
|
||
printf("%d/%d/%d\n", pd->day, pd->month, pd->year);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki gibi bir yapı olsun:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
};
|
||
|
||
Bu yapı türünden s isminde bir nesne tanımlayalım:
|
||
|
||
struct SAMPLE s;
|
||
|
||
&s->a ifadesi geçersizdir. Çünkü burada -> operatörünün solundaki operand &s değil s'tir. -> operatörü & operatöründen daha yüksek önceliklidir.
|
||
-> operatörünün solunda yapı nesnesi bulunamaz. Bir yapı nesnesinin adresi bulunmak zorundadır. Ancak (&s)->a ifadesi geçerlidir. Artık parantezden dolayı
|
||
önce &s işlemi yapılacak ve buradan bir yapı adresi elde edilecek sonra -> operatöryle o adreste bulunan nesnenin a parçasına erişilecektir. Tabii (&s)->a
|
||
ifadesine gerek yoktur zaten s.a ifadesi aynı işi yapmaktadır.
|
||
|
||
Aşağıdaki gib bir yapı olsun:
|
||
|
||
struct CITY {
|
||
char name[32];
|
||
int plate;
|
||
};
|
||
|
||
Bu yapı türünden bir nesne bir de gösterici tanımlayalım:
|
||
|
||
struct CITY c = {"Ankara", 6};
|
||
struct CITY *pc = &c;
|
||
|
||
Burada pc->name ifadesi char * türündendir. Ancak pc->name bir nesne belirtmez. pc adresinde bulunan yapı nesnesinin içerisindeki dizinin başlangıç adresini
|
||
belirtir. pc->name[i] gibi bir ifadede önce ok operatör sonra [] operatörü yapılacaktır. Bu durumda bu ifade char türdendir. Bu ifade pc göstericisinin
|
||
gösterdiği yerdeki yapı nesnesinin içerisindeki name dizisinin başlangıç adresinden itibaren i ilerideki char nesneyi belirtmektedir. &pc->plate ifadesi
|
||
int * türündedir. Yani pc göstericisinin gösterdiği yerdeki yapı nesnesinin plate elemanın adresini belirtir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct CITY {
|
||
char name[32];
|
||
int plate;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct CITY c = {"Ankara", 6};
|
||
struct CITY *pc = &c;
|
||
|
||
putchar(pc->name[2]); /* k */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapılar türünden diziler de söz konusu olabilir. Bunlara yapı dizileri (array of structure) denilmektedir. Yapı dizilerinin her elemanı bir yapı
|
||
nesnesidir. Örneğin:
|
||
|
||
struct CITY {
|
||
char name[32];
|
||
int plate;
|
||
};
|
||
|
||
struct CITY cities[5];
|
||
|
||
Burada cities her elemanı struct CITY türünden olan 5 elemanlı bir dizidir. Yapı elemanları ardışıldır, dizi elemanları da ardışıldır. O zaman
|
||
buradaki dizinin tüm elemanları bellekte ardşıl bir biçimde tutulacaktır.
|
||
|
||
Bir yapı dizisine ilkdeğer verilirken yine küme parantezleri kullanılmalıdır. Dizinin her elemanı bir yapı olduğuna göre elemanlar için de ayrıca küme parantezleri
|
||
kullanılabilir. Ancak C'de elemanlar için küme parantezlerinin kullanılması zorunlu tutulmamıştır. Fakat yukarıda daha önce açıkladığımız gerekçelerden dolayı
|
||
elemanlar için de ayrıca küme parantezi kullanmak iyi bir tekniktir. Örneğin:
|
||
|
||
struct CITY cities[5] = {"Ankara", 6, "Izmir", 35, "Eskisehir", 26, "Kutahya", 43, "Istanbul", 34}; /* geçerli ama kötü teknik */
|
||
|
||
Burada bir sorun oluşmayacaktır. Elemanlar sırasıyla dizi içerisindeki yapı elemanlarına atanacaktır. Tabii bu iyi bir teknik değildir. Dizinin elemanı olan
|
||
yapıların da ayrıca küme parantezlerine alınması iyi bir tekniktir:
|
||
|
||
struct CITY cities[5] = {{"Ankara", 6}, {"Izmir", 35}, {"Eskisehir", 26}, {"Kutahya", 43}, {"Istanbul", 34}}; /* iyi teknik */
|
||
|
||
Bir yapı dizisinin belli bir elemanına [] operatörü ile erişebiliriz. O elemanın da elemanına nokta operatörü ile erişebilir. Bu durumda örneğin
|
||
cities[2].name[3] ifadesinde üç operatör vardır. Bunların hepsi soldan sağa operatörlerdir:
|
||
|
||
İ1: cities[2]
|
||
İ2: İ1.name
|
||
İ3: İ2[3]
|
||
|
||
Burada cities[2] ifadesi struct CITY türünden, cities[2].name ifadesi char * türünden ve cities[2].name[3] ifadesi de char türdendir.
|
||
O halde cities[2].name[3] ifadesi "Eskisehir" yazısının 'i' karakterini belirtir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct CITY {
|
||
char name[32];
|
||
int plate;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct CITY cities[5] = {
|
||
{"Ankara", 6},
|
||
{"Izmir", 35},
|
||
{"Eskisehir", 26},
|
||
{"Kutahya", 43},
|
||
{"Istanbul", 34}
|
||
};
|
||
|
||
for (int i = 0; i < 5; ++i)
|
||
printf("%s, %d\n", cities[i].name, cities[i].plate);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizinin ismi ifade içerisinde kullanıldığında dizinin ilk elemanın adresini belirtiyordu. O zaman biz bir yapı dizsinin ismini aynı türden bir
|
||
yapı göstericisine atayabiliriz. Örneğin:
|
||
|
||
struct CITY cities[5] = {
|
||
{"Ankara", 6},
|
||
{"Izmir", 35},
|
||
{"Eskisehir", 26},
|
||
{"Kutahya", 43},
|
||
{"Istanbul", 34}
|
||
};
|
||
struct CITY *pc;
|
||
|
||
pc = cities;
|
||
|
||
Burada artık pc göstericisi cities dizinin ilk elemanını göstermektedir. Biz bu ilk elemanı şöyle yazdırabiliriz:
|
||
|
||
printf("%s, %d\n", pc->name, pc->plate);
|
||
|
||
Bir adresi 1 artırdığımızda adresin sayısal bileşeni adresin türünün uzunluğu kadar artıyordu. Bu durumda biz bir yapı göstericisini 1 artırırsak
|
||
gösterici içerisindeki adres göstericinin türünün uznluğu kadar artacaktır. O halde örneğin:
|
||
|
||
++pc;
|
||
|
||
Artık burada pc göstericisi "İzmir" ile ilgili yapı nesnesini göstermektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct CITY {
|
||
char name[32];
|
||
int plate;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct CITY cities[5] = {
|
||
{"Ankara", 6},
|
||
{"Izmir", 35},
|
||
{"Eskisehir", 26},
|
||
{"Kutahya", 43},
|
||
{"Istanbul", 34}
|
||
};
|
||
struct CITY *pc;
|
||
|
||
pc = cities;
|
||
|
||
printf("%s, %d\n", pc->name, pc->plate);
|
||
|
||
++pc;
|
||
|
||
printf("%s, %d\n", pc->name, pc->plate);
|
||
|
||
++pc;
|
||
|
||
printf("%s, %d\n", pc->name, pc->plate);
|
||
|
||
++pc;
|
||
|
||
printf("%s, %d\n", pc->name, pc->plate);
|
||
|
||
++pc;
|
||
|
||
printf("%s, %d\n", pc->name, pc->plate);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dizi isimlerinin adres belirttiğini ancak nesne belirtmediğini anımsayınız. Bir yapı dizisinin ismi o yapının başlangıç adresini yani ilk elemanın adresini
|
||
belirtir. Ancak biz bu ismi ++ operatörüyle artıramayız. Örneğin:
|
||
|
||
struct CITY cities[5] = {
|
||
{"Ankara", 6},
|
||
{"Izmir", 35},
|
||
{"Eskisehir", 26},
|
||
{"Kutahya", 43},
|
||
{"Istanbul", 34}
|
||
};
|
||
|
||
printf("%s, %d\n", cities->name, cities->plate); /* geçerli */
|
||
|
||
++cities; /* geçersiz! */
|
||
|
||
return 0;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct CITY {
|
||
char name[32];
|
||
int plate;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct CITY cities[5] = {
|
||
{"Ankara", 6},
|
||
{"Izmir", 35},
|
||
{"Eskisehir", 26},
|
||
{"Kutahya", 43},
|
||
{"Istanbul", 34}
|
||
};
|
||
|
||
printf("%s, %d\n", cities->name, cities->plate);
|
||
printf("%s, %d\n", (cities + 1)->name, (cities + 1)->plate);
|
||
printf("%s, %d\n", (cities + 2)->name, (cities + 2)->plate);
|
||
printf("%s, %d\n", (cities + 3)->name, (cities + 3)->plate);
|
||
printf("%s, %d\n", (cities + 4)->name, (cities + 4)->plate);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Ritchie & Kernighan yazım tarzında genel olarak iki operand'lı operatörlerle operand'lar arasında birer boşluk karakteri kullanılmaktadır. Örneğin:
|
||
|
||
a = b + c;
|
||
|
||
Ancak nokta ve ok operatörleri iki operand'lı olmasına karşın bu operatörler ile operand'ları arasında boşluklar kullanılmamaktadır.
|
||
Örneğin s.a ve ps->a gibi.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
53. Ders - 15/12/2022 Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki gibi bir yapı söz konusu olsun:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;
|
||
double c;
|
||
};
|
||
|
||
Bu yapı türünden bir gösteriye sahip olalım:
|
||
|
||
struct SAMPLE s;
|
||
struct SAMPLE *ps = &s;
|
||
|
||
Biz bir yapı göstericisi ile yapının bir elemanına -> operatöryle eriştiğimizde derleyici elemanın yerini nasıl belirlemektedir? Örneğin
|
||
ps göstericisinin içerisindeki adresin sayısal bileşeni 0x1FC10 olsun. ps->c gibi bir ifadede derleyici yapının c elemanına nasıl erişecektir?
|
||
İşte yapının bir elemanına derleyicinin bu biçimde erişebilmesi için yapı elemanlarının ardışıl olması gerekmektedir. Derleyici yapı bildirimine baktığında
|
||
hemen 0x1FC10 adresinden itibaren 4 byte'ın a elemanı olduğunu, sonraki 4 byte'ın b elemanı olduğunu anlar (int türünün 4 byte oluduğunu varsayıyoruz.)
|
||
O halde derleyici ps->c ifadesinde c'nin ps adresinden itibaren 8 byte ileride bulunduğunu ve 8 byte uzunlukta olduğunu bilecektir. Yapı elemanları bellekte
|
||
ardışıl bir biçimde peşi sıra yerleştirilmeseydi derleyici bu elemanın yerini tespit edemezdi.
|
||
|
||
Özetle bir yapı adresinden hareketle yapı elemanlarının yerlerinin bilinebilmesi için derleycinin yapı bildirimini görmesi ve elemanların ardışıl olduğunu
|
||
kabul etmesi gerekir. Aşağıdaki örnekte yapı elemanlarıın adresleri yazdırılmıştır. Tabii buradaki adresler programın o çalışmasına özgü adreslerdir.
|
||
Ancak biz fikir vermesi için adreslerin değerlerini de yazdık.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;
|
||
double c;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
struct SAMPLE *ps = &s;
|
||
|
||
printf("%p\n", ps); /* 0000002B7395FD70 */
|
||
printf("%p\n", &ps->a); /* 0000002B7395FD70 */
|
||
printf("%p\n", &ps->b); /* 0000002B7395FD74 */
|
||
printf("%p\n", &ps->c); /* 0000002B7395FD78 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapı nesnesini bir fonksiyona parametre yoluyla aktarabilmek için iki yöntem kullanılmaktadır. Bunlardan biri kötü teknik, diğeri iyi tekniktir:
|
||
|
||
1) Yapı nesnesinin kopyasının fonksiyona aktarılması yöntemi (kötü teknik)
|
||
2) Yapı nesnesinin adres yoluyla fonksiyona aktarılması yöntemi (iyi teknik)
|
||
|
||
Birinci tekniğe "değer yoluyla aktarım (call by value)", ikinci tekniğe "adres yoluyla aktarım (call by reference)"
|
||
da denilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
1) Yapı nesnesinin kopyasının fonksiyona aktarılması yöntemi: Bu yöntemde fonksiyonun parametre değişkeni bir yapı türünden yapı nesnesi olur. Fonksiyon da
|
||
aynı türden bir yapı nesnesi ile çağrılır. Aynı türden iki yapı nesnesi birbirlerine atanabildiğine göre bu durum geçerlidir. Fonksiyon içerisinde yapı elemanlarına nokta
|
||
operatörüyle erişilir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;fd
|
||
int c;
|
||
};
|
||
|
||
void foo(struct SAMPLE k) /* k = s */
|
||
{
|
||
printf("%d, %d, %d\n", k.a, k.b, k.c);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {10, 20, 30};
|
||
|
||
foo(s); /* kötü teknik */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada aslında k = s gibi bir işlem yapılmaktadır. Bu durumda s'in her elemanı k'ya atanacaktır. Bu tekniğin iki dezavantajı vardır: Birincisi yapı
|
||
nesnesinin her elemanının diğer yapı nesnesine kopyalanmasıdır. Örneğin 100 elemanlı bir yapı olsa bu durum karşılıklı olarak 100 elemanın kopyalanacağı
|
||
anlamına gelir. Bu ise göreli bir zaman kaybı oluşturmaktadır. Ayrıca bu yöntemde fonksiyon içerisinde parametre olarak aktarılan nesnede bir değişiklik
|
||
yapılamamaktadır. Bu teknik genel olarak kötü bir tekniktir. Ancak tabii yapılar küçükse bu teknik uygulanabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;
|
||
int c;
|
||
};
|
||
|
||
void foo(struct SAMPLE k) /* k = s */
|
||
{
|
||
printf("%d, %d, %d\n", k.a, k.b, k.c);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {10, 20, 30};
|
||
|
||
foo(s); /* kötü teknik */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
2) Yapı nesnesinin adres yoluyla fonksiyona aktarılması yöntemi: Bu yöntemde fonksiyonun parametre değişkeni bir yapı türünden gösterici olur. Fonksiyon da aynı
|
||
yapı türünden bir yapı nesnesinin adresiyle çağrılır. Fonksiyonun içersinde yapı elemanlarına ok operatöryle erişilebilir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;
|
||
int c;
|
||
};
|
||
|
||
void foo(struct SAMPLE *ps) /* ps = &s */
|
||
{
|
||
printf("%d, %d, %d\n", ps->a, ps->b, ps->c);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {10, 20, 30};
|
||
|
||
foo(&s); /* iyi teknik */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Bu yöntemde yapı ne kadar büyük olursa olsun aktarılan yalnızca bir adres bilgisidir. Bu yöntemde aynı zamanda fonksiyon içerisinde asıl nesnenin
|
||
elemanlarını değiştirme olanağı da vardır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;
|
||
int c;
|
||
};
|
||
|
||
void set(struct SAMPLE *ps) /* ps = &s */
|
||
{
|
||
ps->a = 100;
|
||
ps->b = 200;
|
||
ps->c = 300;
|
||
}
|
||
|
||
void disp(struct SAMPLE *ps) /* ps = &s */
|
||
{
|
||
printf("%d, %d, %d\n", ps->a, ps->b, ps->c);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s = {10, 20, 30};
|
||
|
||
disp(&s);
|
||
set(&s);
|
||
disp(&s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Tabii aslında böyle bir aktarımın mümkün olabilmesi için yapı elemanlarının ardıllığının garanti edilmiş olması gerekir. Başka bir deyişle eğer yapı elemanları
|
||
ardışıl olmasydı yapı nesnesinin adresini alan fonksiyon elemanların yerlerini tespit edemezdi.
|
||
|
||
Uygulamada hemen her zaman biz fonksiyonun parametre değişkeninin bir yapı göstericisi olduğunu görürüz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day;
|
||
int month;
|
||
int year;
|
||
};
|
||
|
||
void disp_date(struct DATE *pd)
|
||
{
|
||
printf("%d/%d/%d\n", pd->day, pd->month, pd->year);
|
||
}
|
||
|
||
void get_date(struct DATE *pd)
|
||
{
|
||
printf("Day: ");
|
||
scanf("%d", &pd->day);
|
||
|
||
printf("Month: ");
|
||
scanf("%d", &pd->month);
|
||
|
||
printf("Year: ");
|
||
scanf("%d", &pd->year);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct DATE date;
|
||
|
||
get_date(&date);
|
||
disp_date(&date);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Örneğin ekranda bir pixel'in koordinatlarını belirten aşağıdaki gibi bir yapı olsun:
|
||
|
||
struct POINT {
|
||
int x, y;
|
||
};
|
||
|
||
İki noktayla verilen bir doğruyu çizen draw_line isimli fonksiyonun parametrik yapısı şöyle olabilir:
|
||
|
||
void draw_line(struct POINT *pt1, struct POINT *pt2);
|
||
|
||
Biz de bu fonksiyonu şöyle çağırabiliriz:
|
||
|
||
struct POINT pt1 = {10, 2}, pt2 = {20, 4};
|
||
|
||
draw_line(&pt1, &pt2);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi yapılar neden kullanılmaktadır? Yapılar üç nedenden dolayı programcılar tarafından kullanılır:
|
||
|
||
1) Birbirleriyle ilişkili olgular bir yapı olarak ifade edilirse kavramsal kolaylık sağlanır. Örneğin bir tarih kavramı, bir nokta kavramı, bir insanın
|
||
kimlik bilgileri birbirinden kopuk bilgiler değildir. Bunların ayrı nesneler içerisinde tutulması hem anlamlandırmayı zorlaştırır hem de programcıya ek
|
||
bir yük getirir.
|
||
|
||
2) Fonksiyonların çok fazla parametreye sahip olması iyi bir teknik değildir. Örneğin bir insanın kimlik bilgileri farklı türlere ilişkin yirminin üzerinde
|
||
bileşene sahiptir. Bu bilgilerin farklı parametreler yoluyla fonksiyona geçirilmesi hem çok zahmetlidir hem de zaman kaybına yol açar. Eğer bir fonksiyona
|
||
çok sayıda bilgi parametre yoluyla aktarılacaksa burada yapılması gereken şey aktarılacak bilgileri bir yapı olarak organize edip yapı nesnesini adres yoluyla
|
||
fonksiyona geçirmektir.
|
||
|
||
3) Bir fonksiyonun tek bir geri dönüş değeri vardır. Ancak fonksiyonlar bazen birden fazla değeri iletmek isteyebilirler. İşte bu tür durumlarda yapılması gereken şey
|
||
fonksiyonun ileteceği bilgileri bir yapı olarak bildirmeki fonksiyona bu yapı türünden bir yapı nesnesinin adresini geçmektir. Fonksiyon da bu nesnenin içini
|
||
dolduracaktır. C'de bir fonksiyonun birden fazla değeri çağıran fonksiyona iletmesi söz konusu olduğununda hemen aklımıza yapılar gelmelidir. Örneğin
|
||
bir fonksiyonun sistemin o anki tarhini alarak bize verdğiğini düşünelim. Tarih üç bileşene sahiptir. O halde fonksiyon bize üç değer vermelidir. İşte bu
|
||
tür durumlarda aklımıza hemen yapı kullanımı gelmelidir. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
void get_current_date(struct DATE *pd);
|
||
...
|
||
|
||
struct DATE date;
|
||
|
||
get_current_date(&date);
|
||
|
||
Örneğin bir fonksiyon o anki sistem hakkında birtakım bilgileri elde etmek istesin. Örneğin kullanılan CPU'yu, hard disklerin boyutlarını, işletim sisteminin
|
||
versiyonunu vs. Bunun için get_sysinfo isimli bir fonksiyonun bulunduğunu düşünelim. Burada iletilecek bilgiler çok fazladır. O zaman muhtemelen fonksiyonun
|
||
parametrik yapısı şöyle olacaktır:
|
||
|
||
struct SYSINFO {
|
||
/* gerekli bilgiler */
|
||
};
|
||
|
||
void get_sysinfo(struct SYSINFO *si);
|
||
...
|
||
|
||
struct SYSINFO si;
|
||
|
||
get_sysinfo(&si);
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonun geri dönüş değeri de bir yapı nesnesi olabilir. Örneğin:
|
||
|
||
struct COMPLEX {
|
||
double real;
|
||
double imag;
|
||
};
|
||
|
||
struct COMPLEX add_complex(struct COMPLEX *pz1, struct COMPLEX *pz2);
|
||
|
||
Burada add_complex isimli fonksiyon struct COMPLEX türünden bir yapı nesnesi vermektedir. Yani fonksiyonun geri dönüş değeri bileşik bir nesnedir. Tabii
|
||
bu tür durumlarda return ifadesinin de aynı türden bir yapı nesnesi olması gerekir. Böylesi fonksiyonlarının geri dönüş değerlerinib atanacağı nesnesin de
|
||
aynı türden bir yapı nesnesi olması gerekir. Örneğin:
|
||
|
||
struct COMPLEX z1 = {3, 2}, z2 = {5, 6}, z3;
|
||
|
||
z3 = add_complex(&z1, &z2)
|
||
|
||
Fonksiyonun geri dönüş değerinin bir yapı nesnesi olması durumuyla C'de seyrek karşılaşılmaktadır. Çünkü bunun iki önemli dezavantajı vardır: Birincisi
|
||
fonksiyonu yazarken fonksiyonun içerisinde return işleminde geçici nesneye atama yapılırken yapının karşılıklı elemanları birbirine atanacaktır.
|
||
Yapı büyükse bu bir zaman kaybına yol açar. İkincisi ise böylesi fonksiyonların geri dönüş değerlerinin de aynı türden bir yapıya atanması zorunluluğudur.
|
||
Bu da bir zaman kaybına yol açmaktadır. Ancak eğer yapılar küçükse bu teknik de uygulanabilir. Fakat genel olarak programcılar C'de böyle bir teknik kullanmazlar.
|
||
|
||
C derleycileri yukarıddaki gibi durumlarda bazı optimizasyonlarla kodun daha etkin çalışmasını sağlayabilmektedir.
|
||
Örneğin:
|
||
|
||
z3 = add_complex(&z1, &z2)
|
||
|
||
Burada C derleyicisi aslında gizlice z3 nesnesini adresini de fonksiyona gönderebilir. Hiç bu atamaları yapmadan
|
||
sonucun doğrudan z3 nesnesine yerleştirilmesini sağlayabilir. Yani derleyici adeta bizim yapmamız gereken doğru tekniği
|
||
kendisi gizlice uygulayabilir. Tabii derleyicilerin optimizasyonlarına güvenmek ancak spesifik bir derleyici için
|
||
uygun olabilir. Bir derleyicinin yaptığı optimizasyonu diğer bir derleyici yapamayabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct COMPLEX {
|
||
double real;
|
||
double imag;
|
||
};
|
||
|
||
struct COMPLEX add_complex(struct COMPLEX *pz1, struct COMPLEX *pz2)
|
||
{
|
||
struct COMPLEX result;
|
||
|
||
result.real = pz1->real + pz2->real;
|
||
result.imag = pz1->imag + pz2->imag;
|
||
|
||
return result;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct COMPLEX z1 = {3, 4}, z2 = {1, 6}, z3;
|
||
|
||
z3 = add_complex(&z1, &z2);
|
||
printf("%.0f+%.0fi\n", z3.real, z3.imag);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii hiçbir fonksiyon C'de yerel bir nesnenin (static olmayan) adresiyle geri dönmemelidir. Aşağıdaki gibi bir fonksiyon hatalı bir tasarımdır:
|
||
|
||
struct COMPLEX {
|
||
double real;
|
||
double imag;
|
||
};
|
||
|
||
struct COMPLEX *add_complex(struct COMPLEX *pz1, struct COMPLEX *pz2)
|
||
{
|
||
struct COMPLEX result;
|
||
|
||
result.real = pz1->real + pz2->real;
|
||
result.imag = pz1->imag + pz2->imag;
|
||
|
||
return &result;
|
||
}
|
||
|
||
Burada fonksiyon bir yapı nesnesinin adresiyle geri dönmektedir. Ancak fonksiyonun çağrısı bitince bu yerel değişken stack'ten boşaltılacağı için bu durum
|
||
tanımsız davranışa yol açacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapı elemanları bellekte ardışıl yer kapladığına göre biz bir yapı nesnesini malloc, calloc fonksiyonlarıyla heap'te dinamik bir biçimde tahsis edebiliriz.
|
||
Örneğin:
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
struct PERSON *per;
|
||
|
||
if ((per = (struct PERSON *)malloc(sizeof(struct PERSON))) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
Burada malloc fonksiyonu ile heap'te sizeof(struct PERSON) kadar byte tahsis edilmiştir. sizeof operatörüne bir tür ismi, verildiğinde bu operatör
|
||
o türün o sistemde kaç byte yer kapladığını bize veriyordu. O halde aslında sizeof(struct PERSON) ifadesinde bir struct PERSON nesnesinin ilgili sistemde
|
||
kaç byte yer kapladığı bilgisi elde edilmektedir. Tabii kullanım bittikten sonra bu alanın free hale getirilmesi gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON *per;
|
||
|
||
if ((per = (struct PERSON *)malloc(sizeof(struct PERSON))) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
strcpy(per->name, "Ali Serce");
|
||
per->no = 123;
|
||
|
||
printf("%s, %d\n", per->name, per->no);
|
||
|
||
free(per);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonun içerisinde bir yapı nesnesi dinamik olarak tahsis edilip fonksiyon onun başlangıç adresiyle geri dönebilir. Bu durumda fonksiyonu çağıran
|
||
kişi dinamik olarak tahsis edilmiş olan yapı nesnesinin adresini elde edecektir. Bilindiği gibi bir fonksiyonun içerisinde dinamik tahsisat yapılırsa
|
||
fonksiyondan çıkılsa bile o alan tahsis edilmiş bir biçimde kalmaya devam etmektedir. Ta ki free işlemi uygulanana kadar.
|
||
|
||
Aşağıdaki örnekte get_person fonksiyonu bir kişinin adını, soyadını ve numarasını klavyeden (stdin dosyasından) okuyarak dinamik biçimde tahsis edilen bir yapı nesnesinin
|
||
içerisine yerleştirmektedir. Fonksiyonu çağıran kişi bu adresteki nesneye ulaşmış, onu kullanmış ve nihayet onu free hale getirmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
};
|
||
|
||
struct PERSON *get_person(void)
|
||
{
|
||
struct PERSON *per;
|
||
|
||
if ((per = (struct PERSON *)malloc(sizeof(struct PERSON))) == NULL)
|
||
return NULL;
|
||
|
||
printf("Adi soyadi:");
|
||
gets(per->name);
|
||
printf("No:");
|
||
scanf("%d", &per->no);
|
||
|
||
while (getchar() != '\n')
|
||
;
|
||
|
||
return per;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON *per;
|
||
|
||
if ((per = get_person()) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
printf("%s, %d\n", per->name, per->no);
|
||
|
||
free(per);
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
time isimli standart C fonksiyonu <time.h> dosyası içerisinde aşağıdaki gibi bildirilmiştir:
|
||
|
||
time_t time(time_t *pt);
|
||
|
||
Buradaki time_t türü ileride göreceğimiz typedef işlemi ile oluşturulmuştur. Standartlara time_t derleyicileri yazanların
|
||
tamsayı ya da gerçek sayı türlerinden bir tanesi olarak typedef edecekleri bir türü belirtir. Programcının bu türün ne olarak typedef edildiğini
|
||
bilmesine gerek yoktur. Fonksiyon bilgisayarın saatine bakarak belli bir orijinden geçen saniye sayısını bize verir. Bu orijine "epoch" denilmektedir.
|
||
Eğer biz time_t türünden bir nesnenin adresini fonksiyona geçersek fonksiyon bu değeri adresini geçtiğimiz nesneye yerleştirir ve aynı değerle geri döner.
|
||
Ancak parametre olarak NULL adres geçebiliriz. Bu durumda fonksiyon bu değeri parametresiyle belirtilen adrese yerleştirmez ancak saniye sayısını
|
||
yine geri dönüş değeri olarak verir. Programcılar bu fonksiyonu genellikle şöyle çağırırlar:
|
||
|
||
time_t t;
|
||
|
||
t = time(NULL);
|
||
|
||
Ya da şöyle kullanırlar:
|
||
|
||
time_t t;
|
||
|
||
time(&t);
|
||
|
||
Ancak bu saniye sayısını her iki yöntemle de elde etmenin bir anlamı yoktur. Örneğin:
|
||
|
||
time_t t;
|
||
|
||
t = time(&t); /* geçerli ama gereksiz */
|
||
|
||
time fonksiyonun epoch orijini C standartlarında belirtilmemiştir. Ancak geleneksel olarak 01/01/1970 alınmaktadır.
|
||
|
||
Aşağıda time fonksiyonundan elde edilen değer ekrana (stdout dosyasına) yazdırılmıştır. Burada mademki programcı time_t türünün aslında hangi tür olduğunu bilmemektedir. O halde
|
||
en kötü olasılıkla onu double türüne dönüştürerek yazdırdık. printf fonksiyonunda long double türünü yazdırmak için %Lf formak karakterleri kullanılmaktadır.
|
||
Normalde printf float, double ve long double türleri için noktadan sonra 6 basamak yazdırır. Ancak %.0f ya da %.0Lf ""noktadan sonra 0 basamak yazdır" anlamına gelmektedir.
|
||
|
||
Aslında C standartlarında time fonksiyonun bir saniye sayısı vereceği de belirtilmemiştir. Ancak geleneksel bu fonksiyonlar hep 01/01/1970'ten geçen saniye
|
||
sayısını vermektedir. UNIX/Linux sistemlerinde bu durum garanti edilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
time_t t;
|
||
|
||
t = time(NULL);
|
||
|
||
printf("%.0Lf\n", (long double)t);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
54.Ders - 20/12/2022 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapı bildirilirken kapanış küme parantezinden sonra ';' konulmayıp bir değişken listesi yazılırsa hem yapı bildirilmiş olur hem de o yapı türünden
|
||
nesneler tanımlanmış olur. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
} x, y;
|
||
|
||
Bu işlem aşağıdakki ile eşdeğerdir:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
};
|
||
|
||
struct SAMPLE x, y;
|
||
|
||
Örneğin:
|
||
|
||
struct POINT {
|
||
int x, y;
|
||
} pt, *ppt;
|
||
|
||
Bu işlem de aşağıdaki ile eşdeğerdir:
|
||
|
||
struct POINT {
|
||
int x, y;
|
||
};
|
||
|
||
struct POINT pt, *ppt;
|
||
|
||
Tabii burada tanımlanan değişkenler yapı global olarak bildirildiyse (genellikle böyledir) global değişken, yapı yerel olarak bildirildiyse (çok seyrek durum)
|
||
yerel değişkenlerdir. Burada tanımlanan değişkenlere ilkdeğer de verilebilir. Örneğin:
|
||
|
||
struct POINT {
|
||
int x, y;
|
||
} pt = {10, 12}, *pt2;
|
||
|
||
Bir yapı bildirilirken yapıya isim verilmek zorundadır. Örneğin:
|
||
|
||
struct { /* geçersiz */
|
||
int x, y;
|
||
};
|
||
|
||
Bu bildirim geçersizdir. Çünkü bu yapının bir ismi olmadığına göre daha sonra bu yapıyı kullanmanın bir yolu yoktur.
|
||
Ancak C standartlarına göre eğer ';' den mnce birtakım nesne tanımalaması yapılmışsa yapıya isim verilmeyebilir.
|
||
Örneğin:
|
||
|
||
struct { /* geçerli */
|
||
int day, month, year;
|
||
} x, y;
|
||
|
||
Bu bildirim geçerlidir. Çünkü x ve y program içerisinde kullanılabilir. Pekiyi bu durumda x ve y'nin türü nedir?
|
||
İşte C'de bu nesnelerin artık türünü bilmenin açık bir faydası yoktur. Dolayısıyla x ve y'nin derleyici tarafından
|
||
isimlendirilmiş bir yapı türünden olduğunu varsayabiliriz. Tabii burada x ve y aynı türden olduğu için birbirine
|
||
atanabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
} date = {10, 12, 2009}, *pdate = &date;
|
||
|
||
int main(void)
|
||
{
|
||
struct POINT {
|
||
int x, y;
|
||
} pt = {10, 20};
|
||
|
||
printf("%d/%d/%d\n", date.day, date.month, date.year);
|
||
printf("%d/%d/%d\n", pdate->day, pdate->month, pdate->year);
|
||
|
||
printf("%d, %d\n", pt.x, pt.y);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapının bir elemanı başka bir yapı türünden yapı nenesi olabilir. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE bdate;
|
||
};
|
||
|
||
Burada PERSON yapısının bdate (birth date) elemanı struct DATE türündendir. Yani bu eleman da aslında bileşik bir türdür. Örneğin:
|
||
|
||
struct PERSON per;
|
||
|
||
Burada per.name char türden bir adres belirtir. per.no ise int türdendir. per.bdate ifadesi struct DATE türündendir. O halde biz per yapı nesnesinin
|
||
içerisindek, bdate elemanın elemanlarına erişmek için birden fazla nokta operatörü kullanırız. Örneğin per.bdate.day ifadesi per nesnesinin
|
||
içerisindeki bdate nesnesinin day elemanını belirtmektedir:
|
||
|
||
struct PERSON per;
|
||
|
||
strcpy(per.name, "Sacit Mutlu");
|
||
per.no = 123;
|
||
per.bdate.day = 12;
|
||
per.bdate.month = 7;
|
||
per.bdate.year= 1978;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE bdate;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per;
|
||
|
||
strcpy(per.name, "Sacit Mutlu");
|
||
per.no = 123;
|
||
per.bdate.day = 12;
|
||
per.bdate.month = 7;
|
||
per.bdate.year= 1978;
|
||
|
||
printf("%s, %d, %d/%d/%d\n", per.name, per.no, per.bdate.day, per.bdate.month, per.bdate.year);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İç içe yapılarda yapı nesnesine ilkdeğer verilirken iç yapı nesnesi için ayrıca küme parantezi kullanmak zorunlu değildir. Ancak iç yapı nesnesi için
|
||
ayrıca küme parantezinin kullanılması iyi bir tekniktir. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE bdate;
|
||
};
|
||
|
||
struct PERSON per = {"Sacit Mutlu", 123, 12, 7, 1978}; /* geçerli fakat kötü teknik */
|
||
|
||
İç yapı nesnesinin ayrıca küme parantezlerine alınması iyi bir tekniktir:
|
||
|
||
struct PERSON per = {"Sacit Mutlu", 123, {12, 7, 1978}}; /* iyi teknik */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE bdate;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per = {"Sacit Mutlu", 123, {12, 7, 1978}};
|
||
|
||
printf("%s, %d, %d/%d/%d\n", per.name, per.no, per.bdate.day, per.bdate.month, per.bdate.year);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yine aşağıdaki gibi iç içe yapılar söz konusu olsun:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE bdate;
|
||
};
|
||
|
||
Elimizde de struct PERSON türünden bir gösterici olsun:
|
||
|
||
struct PERSON per = {"Sacit Mutlu", 123, {12, 7, 1978}};
|
||
struct PERSON *pper;
|
||
|
||
pper = &per;
|
||
|
||
pper göstericisi ile ok operatörünü kullanarak yapı elemanlarına erişebiliriz. Örneğin pper->name, pper->no gibi. Burada yine pper->bdate yapı nesnesi
|
||
içerisindeki yapı nesnesinin bütününü belirtmektedir. Bunun da elemanlarına erişilecekse pper->bdate.day, pper->bdate.month ve pper->bdate.year biçiminde
|
||
erişim yapılmalıdır. Şu ifadeye dikkat ediniz:
|
||
|
||
pper->bdate.day
|
||
|
||
Burada iki operatör vardır: Nokta ve ok operatörü. Bu iki operatör de soldan sağa eşit önceliklidir. Dolayısıyla önce ok operatörü sonra nokta operatörü
|
||
yapılır:
|
||
|
||
İ1: pper->bdate
|
||
İ2: İ1.day
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE bdate;
|
||
};
|
||
|
||
void disp(struct PERSON *pper)
|
||
{
|
||
printf("%s, %d, %d/%d/%d\n", pper->name, pper->no, pper->bdate.day, pper->bdate.month, pper->bdate.year);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per = {"Sacit Mutlu", 123, {12, 7, 1978}};
|
||
|
||
disp(&per);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi C99 ve sonrasında yapının belli elemanlarına "designated initializer" denilen sentaksla ilkdeğer verebiliyorduk. Bu sentaksta
|
||
önce bir '.' atomu sonra da yapı ismi geliyordu. İç içe yapılarda da designated initializer sentaksı benzerdir. Burada iç yapı nesnesinin
|
||
kendisine bütünsel olarak küme parantezleri ile ilkdeğer verilebileceği gibi iç yapının elemanlarına da birden fazla nokta
|
||
kullanılarak ilkdeğer verilebilir. Örneğin:
|
||
|
||
struct X {
|
||
int a;
|
||
int b;
|
||
int c;
|
||
};
|
||
|
||
struct Y {
|
||
int d;
|
||
struct X e;
|
||
int f;
|
||
int g;
|
||
};
|
||
|
||
struct Y y = {.e = {10, 20, 30}, 100};
|
||
|
||
Burada .e ifadesiyle biz Y yapısının e yapı elemanına ilkdeğer vermiş olduk. Dolayısıyla sonra 100 değeri yapının f
|
||
elemanına yerleştirilecektir. Örneğin:
|
||
|
||
struct Y y = {.e.b = 10, 20, 30};
|
||
|
||
Burada .e.b ifadesiyle biz iç yapının b elemanına ilkdeğer vermiş olduk. Artık eleman ismi belirtmezsek yukarıdan
|
||
aşağıya doğry yapının sırasıyla elemanlarına ilkdeğer verilir. Yani örneğimizde 20 değeri e elemanının c parçasına,
|
||
30 değeri ise f elemanına yerleştirilecektir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE bdate;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per = {"Sacit Mutlu", .bdate.day = 12, .bdate.month = 7, .bdate.year = 1978, .no = 123};
|
||
|
||
printf("%s, %d, %d/%d/%d\n", per.name, per.no, per.bdate.day, per.bdate.month, per.bdate.year);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İç içe yapı nesnelerinin diğer alternatif bir bildirimi de elemana ilişkin yapının elemana sahip yapının içinde bildirilmesidir. Örneğin:
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE {
|
||
int day, month, year;
|
||
} bdate;
|
||
};
|
||
|
||
Burada PERSON yapısının içerisinde hem DATE yapısı bildirilmiştir hem de DATE yapısı türünden bdate elemanı bildirilmiştir. Bu durum tamamen aşağıdaki ile eşdeğerdir:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE bdate;
|
||
};
|
||
|
||
Yapı içinde yapı bildirirken içteki yapı yine bağımsız olarak dışarıda kullanılabilmektedir. Örneğin:
|
||
|
||
struct PERSON {
|
||
char name[32];
|
||
int no;
|
||
struct DATE {
|
||
int day, month, year;
|
||
} bdate;
|
||
};
|
||
|
||
struct PERSON per; /* geçerli */
|
||
struct DATE date; /* geçerli */
|
||
|
||
Burada her ne kadar DATE yapısı PERSON yağısının içerisinde bildirilmiş olsa da ayrı bir faaliyet alanına sahip değildir. Yani DATE yapısı sanki
|
||
dışarıda bildirilmiş gibi ele alınmaktadır. Aslında yapı içerisinde yapının bildirildiği ikinci alternatif oldukça seyrek kullanılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapı kendisi türünden bir elemana sahipo olamaz. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;
|
||
struct SAMPLE c; /* geçersiz! */
|
||
};
|
||
|
||
Eğer böyle bir şey olsaydı bu durumda bu türden bir yapı nesnesi için ne kadar yer ayrılacağı bilinemezdi. Ancak bir yapı kendisi türünden bir gösterici
|
||
elemana sahip olabilir:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;
|
||
struct SAMPLE *c; /* geçerli */
|
||
};
|
||
|
||
Çünkü göstericilerin türü ne olursa olsun onların kapladığı alan derleme sırasında bilinmektedir. Örneğin tipik olarak 32 bit sistemlerde göstericiler
|
||
4 byte, 64 bit sistemlerde 8 byte yer kaplamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yapının bir elemanın kendi türünden bir gösterici olması durumu "bağlı listeler (linked lists)" gibi veri yapılarının gerçekleştirilmesinde sıkça
|
||
kullanılmaktadır. Bağlı listeler C Programalam Dili tarafından doğrudan desteklenmeyen, programcının kendisinin oluşturması gereken bir veri yapısıdır.
|
||
Veri yapıları ve algoritmalar konusu "Sistem Programala ve İleri C Uygulamaları - 1" kursunda ele alınmaktadır. Ancak biz burada ayrıntıya girmeden
|
||
uygulama amaçlı bağlı listelerden biraz bahsedeceğiz.
|
||
|
||
Veri yapıları dünyasında elemanlar arasında öncelik-sonralık ilişkisi olan veri yapılarına genel olarak "liste (list)" denilmektedir. Önceki elemanının
|
||
sonraki elemanı gösteren veri yapılarına ise "bağlı listeler (linked lists)" denilmektedir. Bağlı listelerin elemanlarına daha çok "düğüm (node)" denilmektedir.
|
||
Bağlı listelerde her düğüm sonraki düğümün yerini tutar. Programcı da ilk düğümün yerini tutmaktadır. Böylece programcı her elemanı erişebilmektedir.
|
||
Bağlı listelerin her düğümü genellikle (her zaman değil) heap'te malloc fonksiyonuyla tahsis edilmektedir. Böylece düğümler aslında herhangi bir yerde bulunabilir.
|
||
Ancak önceki eleman sonrki elemanı gösterdiği için bir organizasyon oluşturulmuş olur. Bağlı liste gerçekleştirimlerinde programcılar genellikle son elemanın da
|
||
yerini tutarlar. Böylece sona hızlı bir biçimde eleman ekleyebilirler. Bağlı listelerin dizilere göre avantajları ve dezavantajları şunlardır:
|
||
|
||
- Eleman erişim dizilerde çk hızlı bir biçimde "rastgele" yapılabilmektedir. Yani p bir dizinin başlangıç adresi olmak üzere p[n] ile dizinin n'inci elemanına
|
||
erişim çok hızlıdır. Bir dizinin 0'ıncı lemanına erişimle 1000'inci elemanına erişim arasında bir zaman farkı yoktur. Ancak bağlı listelerde bir elemana
|
||
erişebilmek için önceki eemanların üzerinden geçmek gerekir.
|
||
|
||
- Dizilerde araya eleman eklemek ve aradan eleman silmek dizi kaydırmasına yol açtığından yavaş bir işlemdir. Oysa bağlı listelerde bu işlem
|
||
çok hızlı yapılabilmektedir.
|
||
|
||
- Dizilerin elemanları ardışıl olmak zorundadır. Oysa bazen ardışıl bellek yetersiz olabilir. Bu durumda dizi yerine bağlı listelerden faydalanılır.
|
||
|
||
- Dizilerin belli bir uzunlukları vardır. Her ne kadar diziler dinamik olarak yaratılıp realloc fonksiyonuyla genişletilebilseler de aslında bu işlemler
|
||
oldukça yavaştır. Halbuki bağlı listelerde bir uzunluk kısıtı yoktur. Uzunluk heap alanı kadar olabilir. Dolayısıyla eleman sayısının bilinmediği
|
||
durumlarda bağlı listeler tercih edilebilmektedir.
|
||
|
||
- Diziler toplamda bağlı listelerden daha az yer kaplamaktadır.
|
||
|
||
Sonuç olarak eğer bir sistemde çok fazla insert, delete işlemi yapılıyor ancak elemana erişim fazla yapılmıyorsa bağlı listeler daha iyi bir seçenek oluşturabilmektedir.
|
||
Sınırı baştan bilinmeyen durumlarda da dizi yerine bağlı listeler tercih edilebilmektedir.
|
||
|
||
Bir bağlı listede önceki eleman sonraki elemanı gösterirken sonraki eleman da önceki elemanı gösteriyorsa bu tür bağlı listelere "çift bağlı listeler (doubly linkes lists)"
|
||
denilmektedir. Eğer yalnızca önceki eleman sonraki elemanı gösteriyorsa bu tür bağlı listelere "tek bağlı listeler (single linked lists)" denir.
|
||
|
||
Düğümler bağlı listelerde yapılarla temsil edilir. Elemanlar da heap'te tahsis edilir. Tek bağlı listenin düğümü aşağıdaki gibi bir yapıyla temsil edilebilir:
|
||
|
||
struct NODE {
|
||
int val; /* tutulan değer */
|
||
struct NODE *next;
|
||
};
|
||
|
||
Çift bağlı listenin de bir düğümü şöyle bir yapıyla temsil edilebilir:
|
||
|
||
struct NODE {
|
||
int val;
|
||
struct NODE *next;
|
||
struct NODE *prev;
|
||
};
|
||
|
||
Genellikle bağlı listelerin ilk ve son düğümü ayrıca da içindeki eleman sayısı programcı tarafından tutulmaktadır. Yine genellikle bu bilgiler de bir
|
||
yapıyla temsil edilmektedir:
|
||
|
||
struct LLIST {
|
||
struct NODE *head;
|
||
struct NODE *tail
|
||
size_t count;
|
||
};
|
||
|
||
Aşağıda içerisinde int değerleri tutan tek bağlı liste veri yapısı oluşturulmuştur. Konunun ayrıntıları "Sistem Programalama ve İleri C Uygulamaları-1"
|
||
kursunda ele alınmaktadır. Her ne kadar birden fazla kaynak dopsyayla çalışmayı ileride ele alacaksak da şimdiden buna yönelik de bir örnek
|
||
vermek istiyoruz. Aşağıdaki örnekte üç dosya bulunmaktadır. "llist.h" dosyası diğer iki dosyadan include edilmiştir.
|
||
Eğer Visual Studio IDE'sinde çalışıyorsanız bu örnekteki "llist.c" ve "sample.c" dosyaları proje eklenmiş olmalıdır.
|
||
UNIX/Linux ve macOS sistemlerinde çalışıyorsanız komut satırından derlemeyi şöyle yapabilirsiniz:
|
||
|
||
$ gcc -o sample sample.c llist.c
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/* llist.h */
|
||
|
||
#ifndef LLIST_H_
|
||
#define LLIST_H_
|
||
|
||
#include <stddef.h>
|
||
|
||
/* Type Declarations */
|
||
|
||
struct NODE {
|
||
int val;
|
||
struct NODE *next;
|
||
struct NODE *prev;
|
||
};
|
||
|
||
struct LLIST {
|
||
struct NODE *head;
|
||
struct NODE *tail;
|
||
size_t count;
|
||
};
|
||
|
||
/* Function Prototypes */
|
||
|
||
struct LLIST *create_llist(void);
|
||
struct NODE *add_tail(struct LLIST *llist, int val);
|
||
struct NODE *add_head(struct LLIST *llist, int val);
|
||
void walk_llist(struct LLIST *llist);
|
||
void walk_llist_rev(struct LLIST *llist);
|
||
struct NODE *insert_next(struct LLIST *llist, struct NODE *node, int val);
|
||
struct NODE *insert_prev(struct LLIST *llist, struct NODE *node, int val);
|
||
void remove_node(struct LLIST *llist, struct NODE *node);
|
||
void clear_llist(struct LLIST *llist);
|
||
void destroy_llist(struct LLIST *llist);
|
||
|
||
#endif
|
||
|
||
/* llist.c */
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include "llist.h"
|
||
|
||
struct LLIST *create_llist(void)
|
||
{
|
||
struct LLIST *llist;
|
||
|
||
if ((llist = (struct LLIST *)malloc(sizeof(struct LLIST))) == NULL)
|
||
return NULL;
|
||
llist->head = llist->tail = NULL;
|
||
llist->count = 0;
|
||
|
||
return llist;
|
||
}
|
||
|
||
struct NODE *add_tail(struct LLIST *llist, int val)
|
||
{
|
||
struct NODE *new_node;
|
||
|
||
if ((new_node = (struct NODE *)malloc(sizeof(struct NODE))) == NULL)
|
||
return NULL;
|
||
new_node->val = val;
|
||
new_node->next = NULL;
|
||
|
||
if (llist->head == NULL) /* is list empty? */
|
||
llist->head = new_node;
|
||
else
|
||
llist->tail->next = new_node;
|
||
|
||
new_node->prev = llist->tail;
|
||
llist->tail = new_node;
|
||
++llist->count;
|
||
|
||
return new_node;
|
||
}
|
||
|
||
struct NODE *add_head(struct LLIST *llist, int val)
|
||
{
|
||
struct NODE *new_node;
|
||
|
||
if ((new_node = (struct NODE *)malloc(sizeof(struct NODE))) == NULL)
|
||
return NULL;
|
||
new_node->val = val;
|
||
new_node->prev = NULL;
|
||
|
||
if (llist->head == NULL)
|
||
llist->tail = new_node;
|
||
else
|
||
llist->head->prev = new_node;
|
||
|
||
new_node->next = llist->head;
|
||
llist->head = new_node;
|
||
++llist->count;
|
||
|
||
return new_node;
|
||
}
|
||
|
||
void walk_llist(struct LLIST *llist)
|
||
{
|
||
struct NODE *node;
|
||
|
||
for (node = llist->head; node != NULL; node = node->next)
|
||
printf("%d ", node->val);
|
||
|
||
putchar('\n');
|
||
}
|
||
|
||
void walk_llist_rev(struct LLIST *llist)
|
||
{
|
||
struct NODE *node;
|
||
|
||
for (node = llist->tail; node != NULL; node = node->prev)
|
||
printf("%d ", node->val);
|
||
|
||
putchar('\n');
|
||
}
|
||
|
||
struct NODE *insert_next(struct LLIST *llist, struct NODE *node, int val)
|
||
{
|
||
struct NODE *new_node;
|
||
|
||
if ((new_node = (struct NODE *)malloc(sizeof(struct NODE))) == NULL)
|
||
return NULL;
|
||
new_node->val = val;
|
||
|
||
new_node->prev = node;
|
||
if (node->next != NULL)
|
||
node->next->prev = new_node;
|
||
else
|
||
llist->tail = new_node;
|
||
new_node->next = node->next;
|
||
node->next = new_node;
|
||
|
||
++llist->count;
|
||
|
||
return new_node;
|
||
}
|
||
|
||
struct NODE *insert_prev(struct LLIST *llist, struct NODE *node, int val)
|
||
{
|
||
struct NODE *new_node;
|
||
|
||
if ((new_node = (struct NODE *)malloc(sizeof(struct NODE))) == NULL)
|
||
return NULL;
|
||
new_node->val = val;
|
||
|
||
if (node->prev != NULL)
|
||
node->prev->next = new_node;
|
||
else
|
||
llist->head = new_node;
|
||
new_node->prev = node->prev;
|
||
new_node->next = node;
|
||
node->prev = new_node;
|
||
|
||
++llist->count;
|
||
|
||
return new_node;
|
||
}
|
||
|
||
void remove_node(struct LLIST *llist, struct NODE *node)
|
||
{
|
||
if (node->prev == NULL)
|
||
llist->head = node->next;
|
||
else
|
||
node->prev->next = node->next;
|
||
if (node->next == NULL)
|
||
llist->tail = node->prev;
|
||
else
|
||
node->next->prev = node->prev;
|
||
|
||
--llist->count;
|
||
}
|
||
|
||
void clear_llist(struct LLIST *llist)
|
||
{
|
||
struct NODE *node, *temp_node;
|
||
|
||
node = llist->head;
|
||
while (node != NULL) {
|
||
temp_node = node->next;
|
||
free(node);
|
||
node = temp_node;
|
||
}
|
||
|
||
llist->head = llist->tail = NULL;
|
||
llist->count = 0;
|
||
}
|
||
|
||
void destroy_llist(struct LLIST *llist)
|
||
{
|
||
struct NODE *node, *temp_node;
|
||
|
||
node = llist->head;
|
||
while (node != NULL) {
|
||
temp_node = node->next;
|
||
free(node);
|
||
node = temp_node;
|
||
}
|
||
|
||
free(llist);
|
||
}
|
||
|
||
/* sample.c */
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include "llist.h"
|
||
|
||
int main(void)
|
||
{
|
||
struct LLIST *llist;
|
||
struct NODE *node;
|
||
struct NODE *insert_pos;
|
||
|
||
if ((llist = create_llist()) == NULL) {
|
||
fprintf(stderr, "cannot create linked list!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (int i = 0; i < 100; ++i) {
|
||
if ((node = add_tail(llist, i)) == NULL) {
|
||
fprintf(stderr, "cannot add tail!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
if (i == 0)
|
||
insert_pos = node;
|
||
}
|
||
|
||
if (insert_prev(llist, insert_pos, 1000) == NULL) {
|
||
fprintf(stderr, "cannot insert item!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
walk_llist(llist);
|
||
|
||
destroy_llist(llist);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
55.Ders - 22/12/2022 Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapılar konusunda önemli bir kavram da "yapı elemanlarının hizalanması (structure member alignment)" denilen kavramdır. Bu konunun anlaşılması için 32
|
||
bit ve 64 bit işlemcilerin bellek bağlantısı hakkında bazı ayrıntıların bilinmesi gerek,r. 32 bit işlemcilerin bellek bağlantılarında işlemci
|
||
bellekten bir hamlede 4 byte çekmektedir. Yani işlemci bellekten 2 byte bile okuyacak olsa önce 4 byte'lık bir bilgi çeker. Kendi içerisinde okunacak 2 byte'ı
|
||
elde eder. 32 bit işlemcilere bağlanan bellekler aslında dört byte dört byte yuvalanmaktadır:
|
||
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
....
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
|
||
Böyle bir bellek organizasyonunda her satırın adres bakımından dördün katlarından başladığına dikkat ediniz. Böyle bir bellekte 4 byte
|
||
uzunluğunda bir nesneye (örneğin int bir nesneye) işlemci erişirken o dört byte uzunluktaki nesnenin dört byte'ın neresinden başladığı önemli olmaktadır.
|
||
Örneğin:
|
||
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
....
|
||
XXXX
|
||
XXYY
|
||
YYXX
|
||
XXXX
|
||
|
||
Burada işlemcinin YYYY ile belirtilen 4 byte'a erişmek istediğini düşünelim. Bu durumda işlemci iki hareketle bu 4 byte'ı elde etmektedir. Önce
|
||
XXYY dört byte'ını çeker, sonra YYXX dört byte'ını çeker ve bunları içeride birleştir. Halbuki bu 4 byte dördün katlarında olsaydı erişim tek hamlede yapılabilirdi:
|
||
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
....
|
||
XXXX
|
||
YYYY
|
||
XXXX
|
||
XXXX
|
||
|
||
Bu durumda 32 bit bir mikroişlemcide 4 byte'lık bilgiler eğer dördün katlarında olursa işlemci onlara daha hızlı erişmektedir.
|
||
32 bit işlemcilerde eğer erişilecek bilgi 1 byte ise onun dört byte'ın neresinde olduğunun bir önemi yoktur. 2 byte'lık bilgilerin de
|
||
(örneğin short nesneler) aynı dört byte içerisinde olması hızlı erişime yol açmaktadır. Örneğin aşağıdaki 2 byte'lık Y nesnesine
|
||
erişim de yine yavaş olacaktır:
|
||
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
XXXX
|
||
....
|
||
XXXY
|
||
YXXX
|
||
XXXX
|
||
XXXX
|
||
|
||
Aynı durum 64 bit mikroişlemciler için de geçerlidir. Bu işlemciler de bellekten tek hamlede 8 byte çekmek üzere tasarlanmışlardır:
|
||
|
||
XXXXXXXX
|
||
XXXXXXXX
|
||
XXXXXXXX
|
||
XXXXXXXX
|
||
........
|
||
XXXXXXXX
|
||
XXXXYYYY
|
||
YYYYXXXX
|
||
XXXXXXXX
|
||
|
||
Bu işlemcilerde 8 byte'lık bilgiler (örneğin long long türünden ya da double türden nesneler) 8'in katlarında ise erişim daha hızlı olmaktadır.
|
||
İşte 32 bit ve 64 bit derleyiciler bu durumu bildikleri için 4 byte'lık ve 8 byte'lık nesneleri ve yapı elemanlarını her zaman dördün ve sekizin
|
||
katlarında tutmaktadır. Programcı genellikle bunu fark etmemektedir. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
int a;
|
||
short b;
|
||
int c;
|
||
...
|
||
}
|
||
|
||
Burada a ve c nesneleri aslında derleyici tarafından adres olarak dördün katlarında tutulmaktadır. Fakat programcılar ancak yapılar söz konusu olduğunda
|
||
derleyicilerin uyguladıkları bu hizalamayı fark edebilmektedir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
char a;
|
||
int b;
|
||
char c;
|
||
int d;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
|
||
printf("%zd\n", sizeof s); /* 16 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada int türünün 4 byte olduğu yaygın sistemlerde programcı bu yapı türünden nesnenin kapladığı alanın 10 byte olması gerektiğini sanabilir.
|
||
Ancak bu yapı nesnesi 16 byte yer kaplayacaktır. Çünkü derleyici yapının int elemanlarını işlemci hızlı erişsin diye dördün katlarında tutacaktır.
|
||
Tüm nesneyi de yine dördün katlarına yerleştirecektir. Böylece derleyici hız kazancı sağlansın diye aşağıdaki gibi bir organizasyon yapacaktır:
|
||
|
||
a---
|
||
bbbb
|
||
c---
|
||
dddd
|
||
|
||
Burada yapının b ve c elemanlarının dördün katında tutulması için a ve c'den sonra üçer byte boşluk bırakılmıştır. Tabii programcı yapı elemanlarını aşağıdaki gibi
|
||
organize etseydi bu yapı türünden nesneler daha az yer kaplardı:
|
||
|
||
struct SAMPLE {
|
||
char a;
|
||
char b;
|
||
int c;
|
||
int d;
|
||
};
|
||
|
||
Bu yapı nesnesi artık 12 byte yer kaplacaktır. Çünkü 1 byte'lık nesnelerin dört byte'ın neresinde olduğunun hız bakımından bir önemi yoktur.
|
||
Derleyici yerleşimi şöyle yapacaktır:
|
||
|
||
ab--
|
||
cccc
|
||
dddd
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
char a;
|
||
char b;
|
||
int c;
|
||
int d;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
|
||
printf("%zd\n", sizeof s); /* 12 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Derleyiciler nesnenin sonunda da nesneyi dördün katlarına tamamlamak için boşluk bırakmaktadır. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
char b;
|
||
int c;
|
||
char d;
|
||
};
|
||
|
||
Burada derleyici yerleşemi şöyle yapacaktır:
|
||
|
||
aaaa
|
||
b---
|
||
cccc
|
||
d---
|
||
|
||
32 bit derleyiciler nesnenin tamamını her zaman dördün katlarına tamamlamaktadır.
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
char b;
|
||
int c;
|
||
char d;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
|
||
printf("%zd\n", sizeof s); /* 16 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Derleyicilerin sonraki 4 byte'lık eleman dördün (ya da sekizin) katlarına gelsin diye yapı nesnesi içerisinde bıraktığı boşluklara "padding" denilmektedir.
|
||
Bu işlemin kendisine ise "hizalama (alignment)" denilmektedir. Hizalamada 2 byte'lık nesnelerin de 2'nin katlarında bulunması (yani aynı 4 byte ya da 8 byte'ıın
|
||
içinde bulunması) uygun olur. Böylece en hızlı erişim için her nesnenin kendi uzunluunun katlarına yerleştirilmesi gerekir. Ynai örneğin 32 bit bir işlemcide
|
||
int nesneler 4'ün katlarına, short nesneler 2'nin katlarına ve char nesneler 1'in katlarına yrleştirilirse en hızlı erişim sağlanır.
|
||
Burada önemli bir noktaya dikkatinizi çekmek istiyoruz: Hizalanmamış nesnelere erişim yine tek bir makine komutuyla yapılmaktadır. Örneğin:
|
||
|
||
MOV EAX, [adres]
|
||
|
||
Bu makine komutu Intel işlemcilerinde belirtilen adresten itibaren 4 byte'ı işlemcinin içerisindeki EAX isimli yaznaca çeker. Burada bellek adresi 4'e
|
||
hizalanmış olsa da olmasa da bu işlem tek bir makine komutuyla yapılmaktadır. Buradaki sorun bu makine komutunun hizalanmamış nesnelerde daha yavaş
|
||
hizalanmış nesnelerde daha hızlı çalışmasıdır.
|
||
|
||
Hizalama Intel işlemcilerinde "isteğe bağlı (optional)" bir durumdur. Yani örneğin 32 bit Intel işlemcileri hizalanmamış bir 4 byte'lık
|
||
bilgiye erişebilmektedir ancak nano saniye mertebesinde yavaş erişmektedir. Halbuki bazı işlemciler (örneğin ARM işlemcileri) hizalanmamış bilgilere
|
||
hiç erişememektedir. İşlemcinine hizalanmamış bir bilgiye erişmesi istendiğinde işlemci bir kesme oluşturmakta ve işletim sistemi de prosesi sonlandırmaktadır.
|
||
|
||
Derleyiciler yapı nesnelerini hizalarken nesnenin bütününü hizalamay uygun biçimde yerleştirip nesnenin toplamının bu hizalamanın katı uzunluğunda olmasını
|
||
sağlamaktadır. Tabii bu özellik standart bir özellik değildir.
|
||
|
||
Biz daha önce "yapı elemanlarının ilk yazılan eleman düşük adreste olacak biçimde ardışıl yerleştirildiğini" belirtmiştik. Hizalama bu durumu ihlal etmekte midir?
|
||
Aslında hizalama derleyici tarafından zaten kontrollü biçimde yapıldığı için (yani derleyici hangi elemanların arasına padding byte'ları eklediğini bildiği için)
|
||
ardışıllık bu anlamda bozulmamaktadır. (Yani siz padding byte'larında aslında yapıda kullanılmayan elemanların bulunduğunu varsayabilirsiniz.)
|
||
|
||
C'de aynı türden iki yapı nesnesi birbirine atandığında elemanlar arasındaki padding byte'larının da biribirine atanacağının bir garantisi yoktur.
|
||
Yani örneğin programcı padding byte'larına bir şey yerleştirmişse (iyi bir teknik değildir) bu yapı nesnesini başka bir nesneye atadığında bu yereştirdiği
|
||
bilgiler hedef nesneye atanmayabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bazen programcı hizalama üzerinde kontrol sağlamak isteyebilir. Fakat hizalama konusu C standartlarında her platformda
|
||
söz konusu olabilecek standart bir konu değildir. Bu nedenle hizalama üzerinde kontrol sağlamak derleyicinin eklenti
|
||
özellikleriyle (extensions) sağlanmaktadır.
|
||
|
||
Derleyiciler genel olarak beş farklı hizalama stratejisi izleyebilmektedir:
|
||
|
||
1 Byte Hizalama (Byte Alignment)
|
||
2 Byte Hizalama (Word Alignment)
|
||
4 Byte Hizalama (Double Word Alignment)
|
||
8 Byte Hizalama (Quad Word Alignemnet)
|
||
16 Byte Hizalama (Double Quad Word Alignment)
|
||
|
||
N byte hizalama şu anlama gelmektedir: "Nesnenin uzunluğu ve N değerinin hangisi küçükse nesne o değerin katlarına hizalanır". Nesnenin
|
||
tamamı da N'in katlarına hizalanmaktadır. Örneğin 4 byte hizalama söz konusu olsun. Bu durumda 1 byte'lık bir nesne (örneğin char bir
|
||
nesne) 1'in katlarına, 2 byte'lık bir nesne (örneğin short bir nesne) 2'nin katlarına, 4 byte'lık bir nesne (örneğin int bir nesne) 4'ün
|
||
katlarına ve 8 byte'lık bir nesne 4'ün katlarına yerleştirilir. Örneğin 8 byte (Quad Word alignment) söz konusu olsun. Bu durumda 1 byte'lık
|
||
nesne 1'in katlarına, 2 byte'lık bir nesne 2'nin katlarına, dört byte'lık bir nesne 4'ün katlarına, 8 byte'lık bir nesne 8'in katlarına hizalanır.
|
||
"1 byte hizalamanın aslında hizalama yapmamakla" aynı anlama geldiğine dikkat ediniz. Dolayısıyla programcı hizalamayı kaldıracaksa derleyiciyi
|
||
1 byte hizalama ayaralayabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Derleyicinin yaptığı default hizalamalar bazen programcının işine gelmeyebilir. Yani programcı çeşitli gerekçelerle derleyicinin hizalama yapmasını
|
||
istemeyebilir. Hizalamanın programcı tarafından kontrolü genellikle derleyicilerin sunduğu ek özelliklerle sağlanmaktadır. Microsoft
|
||
derleyicilerinde hizalama komut satırından derleme yapılırken /ZpN (buradaki N 1, 2, 4, 8, 16 olabilir) seçeneği ile ayarlanmaktadır.
|
||
Hizalama Visual Studio IDE'sinde proje seçeneklerinden "C-C++/Code Generation/Struct Member Alignment" combo box seçneğinden ayarlanabilmektedir.
|
||
Eğer bu ayarlamalar yapılmazsa (default durum) 32 bit derleme için default durum /Zp8 (yani quad word alignment), 64 bit derleme için /Zp16 biçimindedir.
|
||
|
||
gcc ve clang derleyicilerinde de default durumda Microsoft'taki gibi 32 bit derleyiciler için 8 byte hizalama, 64 bit derleyiciler için 16
|
||
byte hizalama kullanılmaktadır. Hizalamayı değiştirmek için -fpack-struct=N komut satırı seçeneği kullanılmalıdır. Örneğin
|
||
|
||
gcc -fpack-struct=1 -o sample sample.c
|
||
|
||
Hizalamayı değiştirmek için hem Microsoft hem gcc hem de clang derleyicilerinde #pragma pack(N) direktifi de kullanılabilmektedir. Bu direktif
|
||
komut satırında belirtilen hizalama seçeneğine göre daha yüksek önceliklidir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#pragma pack(1)
|
||
|
||
struct SAMPLE {
|
||
char a;
|
||
int b;
|
||
char c;
|
||
int d;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
|
||
printf("%zd\n", sizeof s); /* 10 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
#pragma pack direktifi sonraki #pragma pack direktifine kadar etkili olmaktadır. Böylece programcı isterse programın farklı yerlerinde farklı hizalamalar kullanabilir.
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#pragma pack(1)
|
||
|
||
struct SAMPLE {
|
||
char a;
|
||
int b;
|
||
char c;
|
||
int d;
|
||
};
|
||
|
||
#pragma pack(8)
|
||
|
||
struct MAMPLE {
|
||
char a;
|
||
int b;
|
||
char c;
|
||
int d;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
struct MAMPLE m;
|
||
|
||
printf("%zd\n", sizeof s); /* 10 */
|
||
printf("%zd\n", sizeof m); /* 16 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Bir #pragma pack uygulandıktan sonra default duruma geri dönmek için direktif parametresiz bir biçimde kullanılır.
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#pragma pack(1)
|
||
|
||
struct SAMPLE {
|
||
char a;
|
||
int b;
|
||
char c;
|
||
int d;
|
||
};
|
||
|
||
#pragma pack()
|
||
|
||
struct MAMPLE {
|
||
char a;
|
||
int b;
|
||
char c;
|
||
int d;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
struct MAMPLE m;
|
||
|
||
printf("%zd\n", sizeof s); /* 10 */
|
||
printf("%zd\n", sizeof m); /* 16 */
|
||
|
||
return 0;
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C11 ile birlikte bir nesnenin (bir yapının elemanı için de söz konusu olabilir) hizalaması _Alignas(N) belirleyicisi ile değiştirilebilmektedir.
|
||
_Alignas(N) belirleyicisi yapı ilgili nesnenin ya da yapı elemanının N'nin katlarına yerleştirileceği anlamına gelir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
_Alignas(8) int b;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
struct SAMPLE s;
|
||
|
||
printf("%zd\n", sizeof s); /* 16 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada yapının b elemanının önüne _Alignas(8) belirleyicisi getirilmiştir. Bu belirleyici aslında dördün katlarına yerleştirilecek int nesnesnin 8'in katlarına
|
||
yerleştirilmesini sağlamaktadır. Ancak C11 standartlarına göre _Alignas(N) belirleyicisi ile yüksek bir hizalama gereksinimi düşük bir hizalamaya çevrilemez. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
_Alignas(1) int b; /* geçersiz! */
|
||
};
|
||
|
||
_Alignas ile belli nesnelerin ya da belli yapı elemanlarının hizalama gereksinimlerinin değiştirilebildiğine dikkat ediniz.
|
||
|
||
Genellikle derleyiciler tüm yapı nesnesini yapının en büyük elemanının hizalama gereksinimine ilişkin değerin katlarına tamamlamaktadır. Ancak
|
||
bu özellik standart bir özellik değildir. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
short a;
|
||
int b;
|
||
_Alignas(32) char c;
|
||
int d;
|
||
};
|
||
|
||
Burada bu yapının sizeof değerinin 64 çıktığını görürseniz şaşırmayınız. Burada yapının c elemanı 32'in katlarına hizalanacaktır. Ancak tüm nesne
|
||
için 32'nin katı olacak biçimde yer ayrılacak ve yapı nesnesnin sonunda da buna uygun padding bulundurulacaktır.
|
||
|
||
Ayrıca C11 ile birlikte _Alignof(tür_ismi) isminde bir operatör de dile eklenmiştir. Bu operatör o anda o tür için derleyicinin uyguladığı hizalamayı
|
||
bize vermektedir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
printf("%zu\n", _Alignof(int)); /* 4 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
_Alignas belirleyicisi bir tür ismiyle de kullanılabilmektedir. Örneğin:
|
||
|
||
_Alignas(int) char c;
|
||
|
||
_Alignas(tür_ismi) aslında _Alignas(_Alignof(tür_ismi)) anlamına gelmektedir. Yani bir _Alignas(int) dediğimizde int türünün hizalama gereksinimi neyse
|
||
o sayıyı parantez içerisine yazmış gibi oluruz.
|
||
|
||
<stdalign.h> dosyası içerisinde alignas makrosu _Alignas biçiminde alignof mmakrosu ise _Alignof biçiminde define
|
||
edilmiştir. Yani bu başlık dosyası include edilirse biz _Alignas yerina alignas, _Alignof yerine alignof makrolarını
|
||
kullanabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de her ifadenin bir türü vardır. Bu tür de sembolik bir biçimde gösterilebilmektedir.
|
||
|
||
- Temel türlerin isimleri ilgili anahtar sözcüklerle oluşturulmaktadır. Örneğin int, unisgned long, double gibi.
|
||
|
||
- Bir dizi türü C'de bir tür sonra köşeli parantez içerisinde bir sabit ifadesi ile temsil edilmektedir. Örneğin:
|
||
|
||
int a[10];
|
||
|
||
Burada a 10 elemanlı bir dizinin bütününü temsil etmektedir. Ancak anımsanacağı dizi isimleri bir ifadede kullanıldığında o dizinin başlangıç adresi
|
||
anlamına gelmektedir. Başka bir deyişle dizi isimleri ifadede kullanıldığında derleyici tarafından oronatik olarak dizinin başlangıç adresine
|
||
dönüştürülmektedir. Yukarıdaki bildirimde a 10 elemanlı bir dizidir. a'nın türü sembolik olartak int[10] ile gösterilmektedir. Ancak a ifade içerisinde
|
||
kullanılırsa artık int * türünden bir değer olarak işeleme sokulur. O halde bir dizinin ismini belirtip türü sorulursa biz ona T[N] demeliyiz. Ancak
|
||
ifade içerisindeki türü sorukursa biz ona T * demeliyiz.
|
||
|
||
- Bir adresin ya da göstericinin türü adresin tür bileşeni T olmak züere T * biçiminde gösterilir. Örneğin:
|
||
|
||
int *pi;
|
||
|
||
Burada pi, int * türündendir. Pekiyi aşağıdaki dizinin türü nedir?
|
||
|
||
int *a[10];
|
||
|
||
Burada a'nın türü, int *[10] biçiminde temsil edilmektedir. Örneğin:
|
||
|
||
int a, *pi, *b[10];
|
||
|
||
Burada a,'nın türü int, pi'nin türü int * ve b'nin türü int *[10] biçiminde belirtilmektedir.
|
||
|
||
- Daha önceden de belirttiğimiz gibi bir yapının türü "struct" anaktar sözcüğü ile "yapı isminin" birleşimindne oluşmaktadır. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b;
|
||
};
|
||
|
||
struct SAMPLE s;
|
||
|
||
Burada oluşturduğumuz tür SAMPLE biçiminde isimlendirilmez, "struct SAMPLE" biçiminde isimlendirilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bir türe tamamen onun yerini tutabilecek alternatif isim verilmesine "typedef bildirimi" denilmektedir. typedef anahtar sözcüğü C'nin sentaksında
|
||
"yer belirleyicisi (storage class specifier)" grubunda bulunmaktadır. (Bu konu ileride ele alınacaktır.) Her bildirimin önüne typedef getirilebilir. Örneğin:
|
||
|
||
int A;
|
||
|
||
Bu geçerli bir bildirimdir. Biz bunun önüne typedef belirleyicisini getirebiliriz:
|
||
|
||
typedef int A;
|
||
|
||
Örneğin:
|
||
|
||
int A, *PA;
|
||
|
||
Bu geçerli bir bildirimdir. Biz bunun önüne typedef belirleyicisni getirebilriz. typedef belireyicisi "bir bildirimdeki değişken ismini o değişkenin türünü
|
||
belirten tür ismi haline" getirmektedir. Örneğin:
|
||
|
||
int I;
|
||
|
||
Burada I int türden bir değişkendir. Şimdi typedef getirelim:
|
||
|
||
typedef int I;
|
||
|
||
Artık I int türü ile tamamen aynı anlama gelen bir tür ismidir. Yani:
|
||
|
||
int a;
|
||
|
||
ile
|
||
|
||
I a;
|
||
|
||
aynı anlamdadır.
|
||
|
||
Örneğin:
|
||
|
||
int *PI;
|
||
|
||
Burada PI int * türündendir. Başına typedf belirleyicisi getirelim:
|
||
|
||
typedef int *PI;
|
||
|
||
Şimdi artık PI ismi int * türünü temsil etmektedir. Yani örneğin:
|
||
|
||
int *pi;
|
||
|
||
ile
|
||
|
||
PI pi;
|
||
|
||
aynı anlamdadır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
typedef char *STR;
|
||
|
||
int main(void)
|
||
{
|
||
STR s = "ankara"; /* char *s = "ankara"; */
|
||
|
||
printf("%s\n", s); /* ankara */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Örneğin:
|
||
|
||
int I, *PI;
|
||
|
||
Burada I int türdendir, PI ise int * türündendir. Şimdi bildirimin başına typedef belirleyicisini getirelim:
|
||
|
||
typedef int I, *PI;
|
||
|
||
Burada artık I int türünü temsil eden, PI ise int * türünü temsil eden tür isimleri haline getirilmiştir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
typedef int I, *PI;
|
||
|
||
int main(void)
|
||
{
|
||
I a = 10; /* int a = 10; */
|
||
PI pi; /* int *pi; */
|
||
|
||
pi = &a;
|
||
|
||
printf("%d\n", *pi);
|
||
|
||
return 0;
|
||
}
|
||
|
||
typedef bir tanımala oluşturmaz, bir bildirimn işlemidir. Yani derleyici typedef bildirimini gördüğünde yalnızca bilgi edinmektedir. Bir yer ayırmamaktadır.
|
||
Örneğin:
|
||
|
||
int A[5];
|
||
|
||
Burada A 5 elemanlı int türden bir dizidir. Sembolik olarak A'nın türü C'de int[5] ile gösterilmektedir. Şimdi bu bildirimin başına typedef belirleyicisini
|
||
getirelim:
|
||
|
||
typedef int A[5];
|
||
|
||
Artık burada A ismi 5 elemanlı int bir dizi türünü temsil etmektedir. Yani artık A int[5] türü ile aynı anlamdadır. Örneğin:
|
||
|
||
int a[5];
|
||
|
||
ile,
|
||
|
||
A a;
|
||
|
||
aynı anlamdadır.
|
||
|
||
Yukarıda da belirtildiği gibi typedef anahtar sözcüğü C standartlarında "yer belirleyicisi (storage class specifier)" grubundadır. Bildirimde (bu konu ileride
|
||
ele alınacak) yer belirleyicileri ile tür belirleyicileri yer değiştirebildiği için aslında typedef anahtar sözcüğünün başta bulunması zorunlu değildir. Yani örneğin:
|
||
|
||
typedef int I;
|
||
|
||
Bu typedef bildirimi şöyle de yapılabilirdi:
|
||
|
||
int typedef I;
|
||
|
||
Tabii genel alışkanlık typedef belirleyicisni başa getirmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
56. Ders 27/12/2022 - Sali
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında typedef belirleiyicisi prototipler için de kullanılabilmektedir. Örneğin:
|
||
|
||
void F(void);
|
||
|
||
Burada F, geri dönüş değeri void parametresi void olan bir fonksiyon türündendir. Başına typedef getirelim:
|
||
|
||
typedef void F(void);
|
||
|
||
Artık burada F geri dönüş değeri void parametresi void olan bir fonksiyon türünü temsil etmektedir. Örneğin:
|
||
|
||
F f;
|
||
|
||
ile,
|
||
|
||
void f(void);
|
||
|
||
aynı anlamddır. Ancak bu biçimde prototip bildirilebilir ancak fonksiyon tanımlanamaz. Örneğin:
|
||
|
||
f() /* geçerli değil */
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
typedef bildirimi global düzeyde ya da yerel düzeyde yapılabilmektedir. Eğer typedef bildirimi global düzeyde yapılırsa typedef ismi bir tür ismi olarak
|
||
tüm fonksiyonlarda kullanılabilir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
typedef char *STR;
|
||
|
||
int main(void)
|
||
{
|
||
STR s = "ankara"; /* geçerli, STR ismi buarada kullanılabilir */
|
||
|
||
puts(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo()
|
||
{
|
||
STR k = "izmir"; /* geçerli, STR ismi burada da kullanılabilir */
|
||
|
||
puts(k);
|
||
}
|
||
|
||
Eğer typedef bildirimi yerel düzeyde yapılırsa typedef ismi yalnızca o blokta kullanılabilir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
typedef char *STR;
|
||
|
||
STR s = "ankara"; /* geçerli, STR ismi burada kullanılabilir */
|
||
|
||
puts(s);
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo()
|
||
{
|
||
STR k = "izmir"; /* geçersiz! STR ismi burada kullanılamaz */
|
||
|
||
puts(k);
|
||
}
|
||
|
||
C programcıları hemen her zaman typedef bildirimini global düzeyde yani programın tepesinde ya da bir başlık dosyasının içerisinde yapmaktadır:
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapılarla birlikte typedef çok sık kullanılmaktadır. Bu sayede programcı "struct" anahtar sözcüğünü kullanmak zorunda kalmaz. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
};
|
||
|
||
struct SAMPLE SMP;
|
||
|
||
Burada SMP "struct SAMPLE" türündendir. Bu bildirimin başına typedef getirelim:
|
||
|
||
typedef struct SAMPLE SMP;
|
||
|
||
Artık SMP "struct SAMPLE" türünü temsil eden tür ismi olmuştur. Yani:
|
||
|
||
struct SAMPLE s;
|
||
|
||
ile,
|
||
|
||
SMP s;
|
||
|
||
tamamen eşdeğerdir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
typedef struct DATE DT;
|
||
|
||
int main(void)
|
||
{
|
||
DT d = {10, 12, 1990};
|
||
|
||
printf("%d/%d/%d\n", d.day, d.month, d.year);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi yapı bildirimi ';' ile kapatılmadan bir değişken listesi de yazılırsa aynı zamanda tanımlama da yapılmış olmaktadır. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
} SMP;
|
||
|
||
Burada SMP "struct SAMPLE" türünden bir değişkendir. O halde bunun da başına typedef belirleyicisini getirebiliriz:
|
||
|
||
typedef struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
} SMP;
|
||
|
||
Burada SMP artık "struct SAMPLE" türünü temsil etmektedir. Yani:
|
||
|
||
struct SMAPLE s;
|
||
|
||
ile,
|
||
|
||
SMP s;
|
||
|
||
aynı anlamdadır. Gerçekten de yapı bildirimi yaparken aynı zamanda typedef belirleyicisi ile tür ismi de oluşturmak çok sık kullanılan bir
|
||
kalıptır. Programcılar genel olarak yapı ismiyle tyoedef ismini farklı oluştururlar. Örneğin Microsoft kendi kodlarında yapı isimlerinin başında "tag"
|
||
öneki getirir. typedef isimlerinin başına bu öneki getirmez:
|
||
|
||
typedef struct tagMSG {
|
||
...
|
||
} MSG;
|
||
|
||
Aslında daha önce de gördüğümüz gibi yapı ismiyle aynı isimli bir değişken bildirilebilir. Çünkü yapı isimleri tek başlarına değil "struct" anahtar sözcüğü
|
||
ile birlikte kullanılmaktadır. Örneğin:
|
||
|
||
struct test {
|
||
...
|
||
};
|
||
|
||
int test; /* geçerli */
|
||
|
||
test = 10; /* geçerli, int olan test anlaşılır */
|
||
|
||
struct test t; /* geçerli, tür ismi kullanılmış */
|
||
|
||
Buradan hareketle aşağıdaki gibi bir durum da söz konusu olabilir:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
};
|
||
|
||
typedef struct SAMPLE SAMPLE; /* geçerli */
|
||
|
||
Ynai typedef ismi ile yapı ismi de aynı olabilir. Bu durumda aşağıdaki iki bildirim de geçerlidir:
|
||
|
||
struct SAMPLE s; /* geçerli */
|
||
SAMPLE k; /* geçerli */
|
||
|
||
Tabii böylesi bir durumda artık tür ismini ""struct SAMPLE" biçiminde kullanmanın da bir anlamı kalmaz. O halde aşağıdaki
|
||
bildirim de geçerlidir:
|
||
|
||
typedef struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
} SAMPLE;
|
||
|
||
Benzer biçimde aşağıdaki iki bildirim de geçerlidir:
|
||
|
||
struct SAMPLE s;
|
||
SAMPLE k;
|
||
|
||
Tabii programcılar genellikle yapı ismi ile typedef ismini biribirinden ayırmaktadır. Örneğin:
|
||
|
||
typedef struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
} SMP;
|
||
|
||
Anımsanacağı gibi eğer yapı bildirimi saırasında aynı zamanda o yapı türünden değişkenler bildiriliyorsa yapıya isim vermek de zorunlu değildir.
|
||
Örneğin:
|
||
|
||
struct {
|
||
int a;
|
||
double b;
|
||
} s, k;
|
||
|
||
Bu bildirim geçerlidir. Burada s ve k aynı türdendir. Ama tür ismini programcı vermemiştir. O halde bundan sonra da artık bu türden yeni bir değişken tanımlayamayız.
|
||
Bu tür bildirimlerde tür isminin derleyici tarafından benzersiz (unique) biçimde oluşturulduğunu düşünebilirsiniz. Ancak C'de eğer yapı türünden değişken bildirilmiyorsa
|
||
yapıya isim verilmesi zorunludur. Aşağıdaki bildirim geçerli değildir:
|
||
|
||
struct {
|
||
int a;
|
||
double b;
|
||
};
|
||
|
||
Zaten böyle bir bildirim bir anlam ifade etmektedir. O halde typedef işlemi yapılırken yapıya isim verilmesi de zorunlu değildir. Örneğin:
|
||
|
||
struct {
|
||
int a;
|
||
double b;
|
||
} SAMPLE;
|
||
|
||
Bu bildirim geçerlidir. Burada SAMPLE ilgili yapı türündendir. Şimdi bildirimin başına typedef getirelim:
|
||
|
||
typedef struct {
|
||
int a;
|
||
double b;
|
||
} SAMPLE;
|
||
|
||
Burada yapının bir ismş yoktur. Ancak türün ismi vardır. Biz bu yapı türünden değişkenler bildirebiliriz:
|
||
|
||
SAMPLE s, k; /* geçerli */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapı türünden göstericiler sık kullanılmaktadır. Çünkü yapılar fonksiyonlara birincil yöntem olarak adres yoluyla aktarılırlar. Bu nedenle
|
||
yapı türünden adres türlerinin de typef edilmesi ile sık karşılaşılmaktadır. Örneğin:
|
||
|
||
typedef struct tagDATE {
|
||
int day, month, year;
|
||
} DATE;
|
||
|
||
DATE *PDATE;
|
||
|
||
Burada PDATE "struct tagDATE *" türünden bir değişkendir. Başına typedef getirelim:
|
||
|
||
typedef DATE *PDATE;
|
||
|
||
Artık PDATE "struct tagDATE *" türünü temsil etmektedir. Örneğin:
|
||
|
||
void disp_date (PDATE pdate)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Bu fonksiyon aşağıdakiyle eşdeğerdir:
|
||
|
||
void disp_date (struct tagDATE *pdate)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Bazı programcılar yapı için typedef bildirmi yaparken aynı zamanda bu yapı türünden adres için de typedef bildirimi yaparlar. Örneğin:
|
||
|
||
struct tagDATE {
|
||
int day, month, year;
|
||
} DATE, *PDATE;
|
||
|
||
Burada DATE "struct tagDATE" tründendir. Benzer biçimde PDATE de "struct tagDATE *" türündendir. O halde bildirimin başına typedf getirelim:
|
||
|
||
typedef struct tagDATE {
|
||
int day, month, year;
|
||
} DATE, *PDATE;
|
||
|
||
Artık burada DATE "struct tagDATE" türünü, PDATE ise "struct tagDATE *" türünü temsil etmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi typedef bildiriminin faydası nedir? Bunları şöyle özetleyebiliriz:
|
||
|
||
1) typedef bildirmi tür isimlerini kısaltmak amacıyla kullanılabilmektedir. Örneğin:
|
||
|
||
char *names[10];
|
||
|
||
yerine:
|
||
|
||
typedef char ASTR[10];
|
||
|
||
ASTR names;
|
||
|
||
gibi. Benzer biçimde typedef bildirimi yapılar söz konusu olduğunda struct anahtar sözcüğünü elimine edebilmektedir. Örneğin:
|
||
|
||
typedef struct tagDATE {
|
||
int day, month, year;
|
||
} DATE;
|
||
|
||
DATE s;
|
||
|
||
Burada struct tagDATE yerine yalnızca DATE diyebilmekteyiz. Henüz görmemiş olsak da typedef bildirimi fonksiyon göstericileri söz konusu olduğunda
|
||
önemli yazım kolaylığı sağlamaktadır.
|
||
|
||
2) typedef bildirimi taşınabilirliği (portability) sağlamak için de kullanılmaktadır. Örneğin biz bir kütüphane yazmış olalım. Orada bir handle türü bazı sistemlerde
|
||
int, bazı sistemlerde long olsun. Programcının bundan etkilenmemesini sağlamak istiyorsak programcının doğrudan tür ismi yerine typedef ismini kullanmasını
|
||
teşvik ederiz. Örneğin:
|
||
|
||
#include "xlib.h"
|
||
...
|
||
HANDLE h;
|
||
|
||
gibi. Eğer o sistemde HANDLE int ise ve programcı bunu int olarak kullanırsa kodunu HANDLE türünün long olduğu başka sisteme götürdüğünde
|
||
sorun oluşur. Örneğin kütüphanede şöyle bir fonksiyon olsun:
|
||
|
||
HANDLE create_handle(void);
|
||
|
||
Biz de fonksiyonu şöyle çağıralım:
|
||
|
||
HANDLE h;
|
||
|
||
h = create_handle();
|
||
|
||
İlgili sistemde HANDLE türünün int olarak typedef edildiğini biliyor olalım. O zaman bu kodun ilgili sistemde aşağıdakinden bir farkı kalmaz:
|
||
|
||
int h;
|
||
|
||
h = create_handle();
|
||
|
||
Ancak bu kodu bu biçimde HANDLE türünün long olduğu bir sisteme götürürsek sorun oluşacaktır. İşet biz hep HANDLE türünü kullanmalıyız:
|
||
|
||
#include "xlib.h"
|
||
|
||
HANDLE h;
|
||
|
||
h = create_handle();
|
||
|
||
Bu sayede biz kodumuzun iki sistemde de geçerli olmasını sağlamış olmaktayız. Çünkü kütüphaneyi yazanlar zaten bu "xlib.h" içerisini değiştirip
|
||
HANDLE türünü o sisteme uygun biçimde typedef etmiş olacaklarıdır.
|
||
|
||
3) typedef bildirimi okunabilirliği artırmak için de kullanılabilmektedir. Örneğin türlere onların kullanım amaçlarına uygun isimler verilebilir.
|
||
Bu isimler de kodu daha anlamlı hale getirebilir. Örneğin:
|
||
|
||
typedef void *HBITMAP;
|
||
typedef void *HBRUSH;
|
||
typedef void *HPEN;
|
||
|
||
Burada HBITMAP, HBRUSH ve HPEN aynı türdendir. O zaman neden bunlara farklı tür isimleri verilmiştir. İşte okunabilirliği artırmak için:
|
||
|
||
HBITMAP a;
|
||
HBRUSH b;
|
||
HPEN c;
|
||
|
||
Burada a, b, ve c değişkenlerinin hangi niyetle oluşturuldukları tür isimlerinden anlaşılmaktadır. Aslında bu bildirimlerin hepsi aynı türdendir:
|
||
|
||
void *a;
|
||
void *b;
|
||
void *c;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
typedef isimleri ile orijinal tür isimleri tamamen aynı türleri belirtmektedir. typedef bildirimi yalnızca türe alternatif isim vermek için kullanılmaktadır.
|
||
Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
double b;
|
||
};
|
||
|
||
typedef struct SAMPLE SMP;
|
||
|
||
struct SAMPLE s;
|
||
SMP k;
|
||
|
||
Burada s ile k aynı türdendir. SMP tür ismi tamamen struct SAMPLE anlamına gelmektedir. Örneğin:
|
||
|
||
typedef int I;
|
||
...
|
||
I a;
|
||
double b = 2.3;
|
||
|
||
a = (I)b;
|
||
|
||
typedef isimleri orijinal tür isimleri nerede kullanılırsa orada kullanılabilir. Örneğin:
|
||
|
||
typedef int I, *PI;
|
||
|
||
PI pi;
|
||
|
||
pi = (PI)malloc(sizeof(I) * 10);
|
||
|
||
Burada I "int" türünü, PI ise "int *" türünü temsil etmektedir. Tabii PI ile "int *" eşdeğer olduğuna göre "I *" da eşdeğerdir.
|
||
|
||
C'de aynı typedef ikinci kez yapılırsa bu durum geçersiz kabul edilmektedir. Ancak pek çok C derleyicisi bu durumu geçerli kabul edebilmektedir.
|
||
Örneğin:
|
||
|
||
typedef int I;
|
||
typedef int I;
|
||
|
||
Bu durum C'de geçersizdir. Ancak C++'ta geçerli kabul edilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C standartlarında sistemden sisteme değişebilecek bazı türler taşınabilirliği sağlamak için typedef edilmiştir. C'nin bu typedef tür isimlerinin
|
||
sonuna _t getirilmiş durumdadır. Bu biçimdeki sonu _t ile biten isimleri çeşitli başlık dosyalarında typedef edilmiş durumdadır.
|
||
Bunlar anahtar sözcük olmadığı için eğer bu tür isimlerini kullandığımızda bu türlerin typedef edilmiş olduğu başlık dosyalarını include
|
||
etmezsek derleme işleminde hata oluşur.
|
||
|
||
C'nin typedef isimlerinin önemli bir bölümü minimal bir başlık dosyası olarak <stddef.h> içerisinde bildirilmiştir. Ancak bu typedef isimlerinin
|
||
bazıları başka başlık dosyalarında da include edilmiş durumdadır. Biz daha önce size_t türünü kullanmıştık. Burada bu standart typedef türleri hakkında
|
||
bilgiler vereceğiz.
|
||
|
||
size_t: Bu tür ilgili sistemdeki "teorik bellek büyüklüğü" ile ilişkilendirilmiştir. Dolayısıyla bellek büyüklüğü ile doğrudan ya da dolaylı bir ilgisi bulunan
|
||
bağlamlarda programcılar bu türü kullanmayı tercih ederler. Örneğin bir dizinin uzunluğu, bir dizi indeksi bu türle ifade edilir. Standratlara göre size_t
|
||
işaretsiz bir tamsayı türü olmak koşuluyla derleyicileri yazanlar tarafından onların isteği doğrultusunda herhangi bir tür olarak typedef edilebilir.
|
||
Bu nedenle bazı sistemlerde örneğin size_t "unsigned int" olarak typedef edilmişken bazı sistemlerde ""unsigned long int" olarak typedef edilmiş olabilir.
|
||
Standartlarda size_t şu başlık dosyalarında typedef edilmiş olmak zorundadır:
|
||
|
||
<stddef.h>
|
||
<stdio.h>
|
||
<stdlib.h>
|
||
<string.h>
|
||
<time.h>
|
||
<uchar.h> (C11 ile birlikte)
|
||
<wchar.h>
|
||
|
||
Ayrıca anımsayacağınız gibi C standartlarında bazı standart C fonksiyonlarının parametrik yapılarında size_t kullanılmıştır. Örneğin:
|
||
|
||
size_t strlen(const char *str);
|
||
void *malloc(size_t size);
|
||
void *memcpy(void *dest, const void *source, size_t n);
|
||
...
|
||
|
||
Bu durumda örneğin strlen fonksiyonunun geri dönüş değerinin türü "o sistemde size_t hangi tür olarak typedef edilmişse o türdendir". Tabii bizim de
|
||
taşınabilirlik bakımından strlen fonksiyonunun geri dönüş değerini size_t türünden bir nesneye atamamız uygun olur. Örneğin:
|
||
|
||
char s[] = "ankara";
|
||
size_t len;
|
||
...
|
||
|
||
len = strlen(s);
|
||
|
||
printf fonksiyonunda size_t türü %z (yanında d, u, ve x de gelebilir) format karakteriyle yazdırılmaktadır. Bu özellik C99 ile birlikte
|
||
C'ye eklenmiştir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
size_t len;
|
||
|
||
len = strlen(s);
|
||
printf("%zu\n", len);
|
||
|
||
return 0;
|
||
}
|
||
|
||
ptrdiff_t: C'de aynı türden olmak koşuluyla iki adres birbirinden çıkartılabilir. Ancak toplanamaz. Bu durumda adresin sayısal bileşenleri birbirinden çıkarrılır.
|
||
Elde edilen değer adresin türünün uzunluğuna bölünür. Yani işlem soncunda her zaman aradaki eleman sayısı elde edilir. Örneğin:
|
||
|
||
T a[10];
|
||
|
||
Burada &a[5] - &a[0] işleminden T türü ne olursa olsun her zaman 5 değeri elde edilecektir. Tabii bu işlem ters de yapılabilir: &a[0] - &a[5] buradan ise -5
|
||
elde edilir. Aynı türden iki adres birbirinden çıkartıldığında elde edilen değer sistemde teorik bellek büyüklüğü ile ilgilidir. İşte bu tür ptrdiff_t
|
||
ile temsil edilmiştir. Biz C'de aynı türden iki adresi çıkartırsak sonuç o sistemde derleyicileri yazanlar ptrdiff_t türünü nasıl typedef etmişlerse o türden
|
||
elde edilecektir. Standartlarda ptrdiff_t türünün derleyicileri yazanlar tarafından işaretli olmak koşuluyla herhangi bir tamsayı türü olarak typedef edilebleceği
|
||
belirtilmiştir. Bu tür yalnızca <stddef.h> içerisinde typedef edilmiştir. C'de aynı türden iki adresin farkı bir nesnede saklanacaksa onun ptrdiff_t türünden
|
||
olması en uygun durumdur. Örneğin:
|
||
|
||
#include <stddef.h>
|
||
|
||
...
|
||
int a[10];
|
||
ptrdiff_t diff;
|
||
...
|
||
diff = &a[5] - &a[0];
|
||
|
||
printf fonksiyonunda ptrdiff_t türünü yazdırmak için %t (yanına d, u, x gelebilir ) format karakteri kullanılmaktadır. Bu format karakteri de C99 ile
|
||
C'ye eklenmiştir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <stddef.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[10];
|
||
ptrdiff_t diff;
|
||
|
||
diff = &a[5] - &a[0];
|
||
|
||
printf("%td\n", diff);
|
||
|
||
return 0;
|
||
}
|
||
|
||
time_t: Bu tür time fonksiyonun verdiği belli bir orijinden geçen zamanı ifade etmek için kullanılmaktadır. Sistemlerin hemen hepsinde orijin (epoch)
|
||
01/01/1970 00:00 kabul edilmektedir. time fonksiyonu da bu tarihten geçen saniye sayısını tamsayı olarak vermektedir. Ancak standratlarda bu türün tamsayı ya da
|
||
gerçek sayı türlerinden olabileceği belirtilmiştir. (Başka bir deyişle geçen bu zaman saniye cinsinden değil örneğin mili saniye cinsinden de olabilir ya da
|
||
saniye cinsinden ancak noktalı da olabilir.) Derleyicilerin büyük çoğunluğunda bu tür tamsayı türü olarak typedef edilmiştir.
|
||
|
||
wchar_t: Daha önceden de belirtildiği gibi C standartlarında hangi karakter tablosunu ve encoding'i temsil ettiği belirlenmemişse de "geniş karakter (wide character)"
|
||
diye de bir karakter türü vardır. (Geniş karakter sabitleri L'x' biçiminde, geniş karakter string'leri L"text" biçimde belirtilmektedir.) İşte geniş karakterler
|
||
wchar_t türündendir. wchar_t tür ismi Microsoft derleyicilerinde unsigned short int, gcc ve clang derleyicilerinde unsigned int biçiminde typedef edilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
57. Ders 29/12/2022 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C99 ile birlikte ilgili sistemde N bit uzunluğunda tamsayı türlerini temsil etmek için <stdint.h> içerisinde yeni typedef isimleri dile eklenmiştir.
|
||
Örneğin 32 bitlik bir nesneye ihtiyacımız olsun. Bu 32 bitlik nesne DOS sisteminde long türüyle, Linux ve Windows sistemlerinde int türüyle temsil edilmektedir.
|
||
İşte kodun taşınabilirliğini sağlamak için bu N bit uzunlukta tamsayı türleri <stdint.h> içerisinde derleyicileri yazanlar tarafından typedef edilmiştir. Bu türler
|
||
aşağıda belirtilmiştir:
|
||
|
||
- N (8, 16, 32, 64 olmak üzere) intN_t isimleri tam olarak N bit uzuluğunda işaretli tamsayı türlerini uintN_t isimleri de tam olarak N bit uzunluğunda
|
||
işaretsiz tamsayı türlerini belirtir. Bu türler şunlardır:
|
||
|
||
int8_t
|
||
uint8_t
|
||
int16_t
|
||
uint16_t
|
||
int32_t
|
||
uint32_t
|
||
int64_t
|
||
uint64_t
|
||
|
||
- intmax_t ve uintmax_t ilgili derleyicide bulunan en büyük işaretli ve işaretsiz tamsayı türleri olarak typedef edilmiş olmalıdır. Her ne kadar C99 ile birlikte
|
||
en büyük işaretli ve işaretsiz tamsayı türleri long long ve unsigned long long ise de C standratlarında derleyicilerin daha büyük tamsayı türleri
|
||
bulundurabileceği (bir eklenti olarak) belirtilmiştir. printf fonksiyonunda intmax_t ve uintmax_t %j (yanına d, u, o ve x getirilebilir) format karakteriyle yazdırılabilmektedir.
|
||
Bu format karakteri de C99 ile birlikte C'ye eklenmiştir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
|
||
int main(void)
|
||
{
|
||
intmax_t a = 12345678;
|
||
|
||
printf("%jd\n", a);
|
||
|
||
return 0;
|
||
}
|
||
|
||
- N sembolü 8, 16, 32 ve 64 sayılarından biri olmak üzere int_leastN_t ve uint_leastN_t türleri N bite sahip en küçük türler olarak typedef edilmiştir.
|
||
Bu türler şunlardır:
|
||
|
||
int_least8_t
|
||
uint_least8_t
|
||
int_least16_t
|
||
uint_least16_t
|
||
int_least32_t
|
||
uint_least32_t
|
||
int_least64_t
|
||
uint_least_64_t
|
||
|
||
Bu least türleri şu anlama gelmektedir: Bir least türü en azından N bit uzunluğundadır. Ancak ondan daha kısa bir int türü yoktur. Örneğin int_least16_t
|
||
türü 32 bit bir sistemde short olabilir ancak int olamaz. Örneğin int_least32_t türü 32 biti içeren en düşük tür olmak zorundadır. Fakat örneğin belli bir sistemde
|
||
short türü de int türü de 32 bit olsun. Bu sistemde int_least32_t türü short da olabilir int de olabilir. Çünkü her ikisi de 32 bittir ve o sistemde 16 biti kapsayan
|
||
32 bittten daha küçük bir tamsayı türü yoktur. Yani int_leastN_t ya da uint_leastN_t türleri N biri içeren en küçük tamsayı türleridir. Örneğin
|
||
int32_t türü ile int_least32_t türü arasında şöyle bir farklılık vardır: int32_t türü tam olarak 32 bittir. Ancak int_least32_t türü 32 bitten büyük olabilir,
|
||
ancak küçük olamaz. 32 bitten büyükse 32 bitlik tamsayı türünün olmaması gerekir. Tabii pek çok sistemde intN_t ile int_leastN_t türleri zaten aynıdır.
|
||
|
||
- N 8, 16, 32 ve 64 sayılarını belirtmek üzere int_fastN_t ve uint_fastN_t türleri N biti kapsayan en hızlı tamsayı türleri olarak typedef edilir.
|
||
Bazı sistemlerde bazı uzunluktaki bilgiler üzerinde daha hızlı işlemler yapılabilmektedir. Örneğin bizim 16 bitlik bir işleme ihtiyacımız olsun.
|
||
Falanca sistemde iki byte short türü, 4 byte'lık int türünden daha yavaş işleme sokuluyor olabilir. Bu durumda programcı zaten 4 byte 2 byte'ı
|
||
kapsadığı için bu sistemde hız bakımından int türünü kullanmak isteyebilir. İşte bu fast türleri en az N bit uzunluğunda olan en hızlı işleme
|
||
sokulma potansiyelinde olan türleri temsil etmektedir. Intel, ARM gibi işlemcilerde düşük uzunluklu tamsayı türleriyle yüksek uzunluklu tamsayı türleri
|
||
işlemcinin yazmaç uzunluklarını geçmemek koşuluyla aynı zamanda işleme sokulmaktadır. Ancak çeşitli mimarilerde bu durum değişebilir. fast türleri
|
||
şunlardır:
|
||
|
||
int_fast8_t
|
||
uint_fast8_t
|
||
int_fast16_t
|
||
uint_fast16_t
|
||
int_fast32_t
|
||
uint_fast32_t
|
||
int_fast64_t
|
||
uint_fast_64_t
|
||
|
||
- intptr_t ve uintptr_t türleri ilgili sistemde bir göstericinin sayısal bileşenini saklayabilecek uzunlukta tamsayı türleri olark typedef edilmektedir.
|
||
Anımsanacağı gibi genel olarak 32 bit sistemlerde göstericiler 32 bit, 64 bit sistemlerde ise 64'tir. Örneğin:
|
||
|
||
char *ptr;
|
||
...
|
||
|
||
Burada ptr göstericisinin içeisinde bir adres bilgisi olsun. Bu adres bilgisi üzerinde bit düzeyinde bir işlem yapmak
|
||
isteyelim. C'de adres türleri bir operatörleriyle işleme sokulamamaktadır. Bu durumda bizim önce bu adres bilgisini
|
||
onu kapsayacak uzunlukta bir tamsayı türünden nesneye aktarmamız, o nesneyle bit işlemini yapıp onu yeniden adres
|
||
türüne dönüştürmemzi gerekir:
|
||
|
||
char *ptr;
|
||
uintptr_t val;
|
||
...
|
||
|
||
val = (uintptr_t)ptr;
|
||
/* bit işlemleri yapılıyor */
|
||
ptr = (char *)val;
|
||
|
||
Tabii uintptr_t türü ve intptr_t türü o sistemdeki adres bilgisini ifade edebilecek tamsayı türünden daha büyük türler
|
||
biçiminde de typedef edilmiş olabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi C99'a kadar C'de bool biçiminde bir tür yoktu. C99 ile birlikte C'ye _Bool isminde bir bool türü eklenmiştir. Ancak bu isim biraz
|
||
tuhaf harflendirildiğinden dolayı <stdbool.h> içerisinde aşağıdaki makrolar da oluşturulmuştur:
|
||
|
||
#define bool _Bool
|
||
#define false 0
|
||
#define true 1
|
||
|
||
bool ismi C'de bir typedef ismi değildir. (Zaten typedef ismi olsaydı bool_t biçiminde isimlendirilirdi.) Biz bool yazdığımız zaman önişlemci onu _Bool
|
||
biçimine dönüştürmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İzleyen paragraflarda C'de bildirim işleminin bazı ayrıntıları el elaınacaktır. Bu bağlamda bildirimle kullanılabilen
|
||
"yer belirleyicileri (storage class specifiers)" ve "tür niteleyicileri (type qualifiers)" ele açıklanacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Biz daha önce bildirim işleminin şöyle yapıldığını görmüştük:
|
||
|
||
<tür belirleyicisi> <dekleratör listesi>;
|
||
|
||
Tür belirleyicisi (type specifier) tür belirten anahtar sözcükler olabildiği gibi struct anahtar sözcü ve yapı ismi de olabilir, typedef ismi de olabilir.
|
||
Bir bildirim işleminde tür belirleyicisinin dışındaki ve ilkdeğer ifadesinin dışındaki atom gruplarına "dekleratör (declarator)" ddenilmektedir. Örneğin:
|
||
|
||
int a[3], *pi, b;
|
||
|
||
Burada int tür belirleyicisidir. a[3], *pi ve b birer dekleratördür. Dekleratörü '=' ile bir ilkdeğer ifadesi izleyebilir. İlkdeğer dekleratöre dahil değildr.
|
||
Örneğin:
|
||
|
||
int a[3] = {10, 20, 30} *pi, b = 20;
|
||
|
||
Burada yine dekleratörler a[3], *pi ve b'dir. O halde bildirim işleminin biraz daha iyileştirilmiş genel biçimi şöyle olabilir:
|
||
|
||
<tür belirleycisi> <dekleratör> [ = ilkdeğer], <dekleratör> [ = ilkdeğer], ...;
|
||
|
||
Bu biçim de aslında gerçeği tam olarak yansıtmamaktadır. Çünkü bir bildirimde tür belirleyicisinin yanı sıra "yer belirleyici (storage class specifer)" ve
|
||
"tür niteleyicisi (type qualifer)" de kullanılabilmektedir. O halde bildirim işleminin genel biçimi aslında şöyledir:
|
||
|
||
[tür niteleyicileri] [yer belirleyicisi] <tür belirleyicisi> <dekleratör listesi (ilkdeğer verilmiş olabilir);
|
||
|
||
Tür belirleyicisi, yer belirleyicisi ve tür niteleyicileri aslında bildirimde herhangi bir sırada bulunabilirler. Ancak programcıların çoğu bunları şöyle
|
||
sıralandırmaktadır: Önce tür niteleyicileri, sonra yer belirleyicileri sonra da tür belirleyicileri. C99'a kadar eğer bir bildirimde en az bir yer belirleyicisi
|
||
ya da tür niteleyicisi varsa tür belirleyicisi bulunmak zorunda değildi. Bu durumda tür belirleyicisi default int kabul ediliyordu. Örneğin:
|
||
|
||
const static a = 10; /* C90'da geçerli ancak C99 ve sonrasında ve C++'ta geçersiz */
|
||
|
||
Burada const bir tür niteleyicisidir. static ise bir yer belirleyicisidir. İşte C90'da bu durum geçerli kabul ediliyordu ve tür belirleyicisi belirtilmediği
|
||
için default tür belirleyicisi int kabul ediliyordu. Ancak C99 ve sonrasında ve C++'ta bu kural kaldırılmıştır. Tür belirleyicisinin bulundurulması zorunlu
|
||
hale getirilmiştir. Örneğin:
|
||
|
||
const static int a = 10;
|
||
|
||
Yukarıda da belirtildiği gibi bildirimde tür niteleyicileri, yer belirleyicileri ve tür belirleyicileri herhangi bir sırada bulunabilir. Örneğin:
|
||
|
||
static int const a = 10;
|
||
|
||
Birden fazla anahtar sözcükten oluşan türlerde de anahtar sözcüklerin sırasının bir önemi yoktur. Örneğin:
|
||
|
||
int const unsigned static a = 10;
|
||
|
||
"unsigned int" türü "int unsigned" biçiminde de belirtilebilir.
|
||
|
||
Anımsanacağı gibi diziler, yapılar gibi bileşik türlere (aggregate types) ilkdeğer verirken küme parantezleri kullanılıyordu. Örneğin:
|
||
|
||
int a[3] = {1, 2, 3};
|
||
|
||
Aslında C standartlarına gör bileşik olmayan türlere de küme parantezleriyle ilkdeğer verilebilmektedir. Örneğin:
|
||
|
||
int a = {0}; /* geçerli */
|
||
|
||
Tabii böyle bir şeye hiç gerek yoktur. Bu nedenle C programcıları genellikle bu durumun geçerli olduğunu bile bilmeyebilirler. Tabii dizi ve yapılara küme
|
||
parantezi olmadan ilkdeğer verilememektedir. Örneğin:
|
||
|
||
int a[1] = 10; /* geçersiz! */
|
||
|
||
Şİmdi de bildirimdeki "yer belirleyicileri (storage class specifiers)" ve "tür niteleyicileri (type qualifer)" konusu üzerinde duracağız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bildirimde kullanılabilecek dört yer belirleyicisi anahtar sözcük vardır:
|
||
|
||
auto
|
||
register
|
||
extern
|
||
static
|
||
|
||
İki de tür niteliyici anahtar sözcük vardır:
|
||
|
||
const
|
||
volatile
|
||
|
||
Bir bildirimde iki yer belirleyicisi bir arada kullanılmaz. Ama iki tür niteleyicisi bir arada kullanılabilir. Örneğin:
|
||
|
||
static extern int a; /* geçersiz! */
|
||
static const volatile b = 10; /* geçerli */
|
||
extern auto int c; /* geçersiz! */
|
||
|
||
Şimdi izleyen paragraflarda yer ve tür belirleyicilerinin işlevleri üzerinde duracağız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
auto yer belirleyicisi gereksiz ve artık bugün için anlamsız bir belirleyicidir. Geçmişe doğru uyumu korumak için hala muhafaza edilmektedir. auto yer belirleyicisi
|
||
global değişkenlerle ve parametre değişkenleriyle kullanılamaz. Yalnızca yerel değişkenlerle kullanılabilir. Yerel değişkenin blok bittiğinde yol edileceği
|
||
anlamına gelir. Zaten yerel değişkenler blok bittiğinde yok edilmektedir. O halde auto belirleyicisinin bir önemi yoktur. C++'ta C++11 ile birlikte
|
||
zaten anlamsız olan auto anahtar sözcüğüne "tür belirleyicisi (type specifier)" anlamı yüklenmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
register yer belirleyicisi de artık gereksiz bir belirleyici haline gelmiştir. Bu belirleyicinin anlamını anlayabilmek için "register (yazmaç)" kavramını bilmek
|
||
gerekir. CPU'nun içerisinde aritmetik, karşılaştırma, mantıksal ve bit işlemlerini yapan elektrik devreleri (mantık devreleri) bulunmaktadır. CPU'nun
|
||
bu kısmına "Aritmetik Lojik Birim (Arithmetic Logic Unit)" denir ve İngilizce "ALU" biçiminde kısaltılır. Yazmaçlar CPU içerisindeki ALU için iskele görevi
|
||
gören küçük bellek bölgeleridir. Bir işlemin yapılabilmesi için işleme sokulacak operand bellekten CPU içerisindeki yazmaçlara çekilir.
|
||
Çünkü ALU içerisindeki elektrik devreleri girdileri yazmaçlardan alacak biçimde tasarlanmıştır. Bu elektrik devreleri sonucu da yine yazmaçlara yerleştirmektedir.
|
||
Yazmaçların uzunlukları ve sayıları CPU'lar için önemlidir. Yazmaçların uzunlukları aynı zamanda bir CPU'nun tek hamlede hangi büyüklükte bilgiler üzerinde
|
||
işlem yapabileceğini belirtmektedir. Örneğin "32 bit işlemci" demek tek hamlede 32 bit işlem yapabilen işlemci demektir. Dolayısıyla 32 bit işlemcilerdeki yazmaçlar da 32 bittir.
|
||
Örneğin 64 bit iki sayıyı 32 bit işlemcide tek hamlede toplayamayız. Önce onun düşük anlamlı 32 bitini sonra da yüksek anlamlı 32 bitini toplayıp sonucu buluruz.
|
||
Halbuki 64 bit bir işlemcide tek hamlede 64 bit iki sayı toplanabilmektedir. Dünyanın ilk mikroişelmsici 1974 yılında üretilen Intel'in 8080'i kabul eedilmektedir.
|
||
8080 8 bit bir mikroşlemciydi. Bunu 16 bit 8086, 32 bit 30386 ve 64 bit Pentium'un ileri modelleri izledi. Intel'in 32 bit işlemcilerine aile olarak x86 denilmektedir.
|
||
64 bit işlemcilerine ise X64 denilmektedir. ARM işlemcileri de ilkin 32 bit işlemciler olarak tasarlandı. Sonra onlar da 64 bite yükseltildi. Bugün 64
|
||
bit işlemciler yaygın kullanılmaktadır. 128 bit işlemlere fazlaca gereksinim duyulmadığı için şimdilik 128 bit işlemciler yaygın biçimde kullanılmamaktadır.
|
||
Örneğin aşağıdaki gibi bir C kodu olsun:
|
||
|
||
c = a + b;
|
||
|
||
Burada a, b, ve c nesneleri RAM'dedir. Intel işlemcileri için derleme yapan derleyiciler de bu işlemi yapacak makina komutları üretir:
|
||
|
||
MOV reg1, a
|
||
MOV reg2, b
|
||
ADD reg1, reg2
|
||
MOV c, reg1
|
||
|
||
Burada önce bellekteki a ve b CPU içerisindeki yazmaçlara çekilip ADD makine komutu ile ALU devreye sokularak iki yazmaçtaki değer toplanıp yeniden yazmaca
|
||
aktarılmıştır. En sonunda da bu yazmaçtaki değer yeniden belleğe aktarılmıştır.
|
||
|
||
Intel gibi CISC tarzı işlemcilerde makine komutları iki operand'a sahiptir. Operand'lardan biri yazmaç diğeri bellek olabilir. Örneğin aşağıdaki
|
||
gibi bir makine komutu Intel işlemcilerinde geçerlidir:
|
||
|
||
ADD reg, mem_addr
|
||
|
||
Burada yazmaçtaki değerle ilgili adresteki değer toplanıp sonuç yazmaçtaki değer bozularak yeniden yazmaca yerleştirilmektedir. Halbuki ARM gibi MIPS
|
||
gibi RISC tarzı işlemcilerde işlem yapan (aktarım yapan değil) makine komutları genel olarak üç operand'lıdır. Bu işlemciler de operand'ların ikisi de
|
||
yazmaca çekilmiş olmalıdır. Örneğin:
|
||
|
||
c = a + b;
|
||
|
||
işlemi için derleyiciler RISC işlemcilerinde aşağıdaki makine komutlarını üretmektedir:
|
||
|
||
LD reg1, a
|
||
LD reg2, b
|
||
ADD reg3, reg1, reg2
|
||
ST c, reg3
|
||
|
||
Bir değişken register anahtar sözcüğü ile tanımlanırsa bu şu anlama gelmektedir: "Derleyici ben bu değişkeni çok sık kullanacağım. Dolayısıyla
|
||
sen onu RAM'de tutmak yerine doğrudan CPU yazmaçlarının içinde tutmaya çalış. Böylece bu değişken işleme sokulduğunda her defasında yeniden CPU yazmaçlarının içerisine
|
||
çekilip (load işlemi) yeniden belleğe geri yazılmasın (store işlemi)". Ancak "register" belirleyicisi bir "emir" değil "rica" niteliğindedir.
|
||
Yani derleyici değişken register anahtar sözcüğü ile tanımlanmış olsa bile onu CPU yazmaçlarında tutmayabilir. Normal bir değişken gibi yine
|
||
onı RAM'de tutabilir. Bu durumda bir uyarı da vermeyebilir. Bugün derleyicilerin kod optimizasyonları çok iyileştirilmiştir. Dolayısıyla derleyiciler
|
||
hangi değişkeni ne kadar süre hangi yazmaçlarda tutacağını iyi bir hesap ile zaten programcıdan çok daha iyi belirleyebilmektedir. (Buna kod optimizasyonunda
|
||
"yazmaç optimizasyonu (register optimization)" denilmektedir.) Dolayısıyla bugünkü yaygın derleyiciler "register" anahtar sözcüğünü hiç dikkate
|
||
almamaktadır. Bu anahtar sözcük de artık kullanım gereğini kaybetmiştir.
|
||
|
||
Yazmaçların adresleri yoktur. Dolayısıyla register anahtar sözcüğü ile tanımlanmış bir nesnenin adresini alamayız. (O nesne yazmaçta tutulmuyor olsa bile
|
||
onun adresini alamayız.) alamayız. register belirleyicisi ile tanımlanmış bir değişkenin adresi alınmaya çalışılırsa bu durum geçersizdir (tipik olarak
|
||
"compile time error").
|
||
|
||
Bazı mimarilerde CPU içerisinde az sayıda yazmaç bulunmaktadır. Örneğin Intel'in Pentium modeline kadar işlemcilerinde çok az yazmaç bulunmaktaydı. Bir değişkenin
|
||
bu yazmaçlardan birini sürekli kullanması genel performansı zaten bu sistemlerde düşürebilmektedir. Öte yandan RISC tabanlı ARM gibi MIPS gibi işlemcilerde
|
||
daha fazla yazmaç bulunma eğilimindedir. Derleyiciler nesneleri bu mimarilerde daha uzun süre yazmaçlarda tutabilmektedir.
|
||
|
||
C'de register yer belirleyicisi yerel değişkenlerle ve parametre değişkenleriyle kullanılabilir. Ancak global değişkenlerle kullanılamaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
58.Ders - 03/01/2023 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Büyük bir projeyi tek bir kaynak dosya biçiminde gerçekleştirmek iyi bir teknik değildir. Bunun iyi bir teknik olmamasının temel nedenleri şunlardır:
|
||
|
||
- Çok büyük kaynak dosyaların edit edilmesi ve kodun bakımının yapılması güçtür.
|
||
|
||
- Çok büyük kaynak dosyalarda bir değişiklik yapıldığında tüm dosyanın yeniden derlenmesi gerekir. Büyük dosyaların derlenmesi dakikalarca ve hatta saatlerce
|
||
zaman alabilmektedir.
|
||
|
||
- Bir projenin tek bir kaynak dosya biçiminde oluşturulması projenin değişik parçalarının paralel bir biçimde değişik kişiler tarafından yazımını
|
||
engellemektedir.
|
||
|
||
İşte eğer büyük bir proje birden fazla kaynak dosyaya bölünürse bu kaynak dosyalar diğerlerinden bağımsız olarak derlenebilir. Sonra birlikte link
|
||
edilerek çalıştırılabilir dosya elde edilebilir. Böylece bir değişiklik yapıldığında bütün projenin yeniden derlenmesine gerek kalma<z. Yalnızca değişikliğin
|
||
yapıldığı kaynak dosya yeniden derlenip yine tüm amaç dosyalar birlikte link edilebilirler.
|
||
|
||
Link işlemi birden fazla amaç dosyayı (object files) alıp tek bir "çalıştırılabilir (executable)" dosya oluşturabilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir projeyi oluşturan kaynak dosyaların her birine C standartlarında "derleme birimi (translation unit)" denilmektedir. Biz burada "modül" terimini de kullanacağız.
|
||
Örneğin biz 100000 satırlıkk bir projei 10 kaynak dosyaya (translation unit) bölmülş olalım. Bu dosyalar da yaklaşık 10000 civarı satırdan oluşsun.
|
||
Bu dosyalara a1.c, a2.c, a3.c, ..., a19.c ve a10.c isimlerinin verildiğini varsayalım. O zaman bu dosyaları diğerlerinden bağımsız olarak derleyip
|
||
amaç dosya oluşturacağız ve bu amaç dosyayı da link işlemine sokacağız. Bu amaç dosyaların a1.obj, a2.obj, a3.obj, ..., a9.obj, a10.obj isminde
|
||
olduğunj varsayalım. Şimdi biz bu amaç dosyaları link işlemine sokup tek bir "çalıştırılabilir dosya (executable file)" elde edeceğiz.
|
||
İşte burada iki önemli durum ortaya çıkacaktır. Buradaki modüller diğerlerindne bağımzı bir biçimde derlendiğine göre biz bir modülde başka bir modüldeki
|
||
fonksiyonları nasıl çağıracağız ve başka bir modüldeki global değişken leri nasıl kullanacağız? Çünkü bu proje tek kaynak dosya biçiminde yazılmış
|
||
olsaydı bir global değişkeni her yerden kullanabilirdik. Oysa biz bu projeyi 10 parçaya böldüğümüz zaman bir global değişken bir modülde kalacaktır,
|
||
diğer modülden nasıl kullanılacaktır?
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir global değişkenin ya da bir fonksiyonun başka modüllerden kullanılabilirliğine C standartlarında "linkage" denilmektedir. Bir global değişken ya da
|
||
fonksiyon yalnızca tanımlandığı modülde kullanılabiliyorsa buna "internal linkage", tanımlandığı modülün dışındaki modüllerden de kullanılabiliyorsa
|
||
buna da "external linkage" denilmektedir. Global değişkenler ya da fonksiyonlar "internal" ya da "external" linkage'a sahip olabilirler. Yerel
|
||
değişkenlerin ve parametre değişkenlerinin "linkage'ı yoktur". Bunlar zaten hiçbir zaman başka bir modül tarafından kullanılamazlar.
|
||
|
||
Bir global değişken ya da fonksiyon default durumda "external" linkage'a sahiptir. Global değişkeni ya da fonksiyonu "internal linkage'a" sahip
|
||
hale getirebilmek için "static" yer belirleyicisinin kullanılması gerekir. Örneğin:
|
||
|
||
int g_x; /* external linkage, yani başka modüllerden kullanılabilir */
|
||
static int g_y; /* internal linkage, başka bir modüllerden kullanılamaz */
|
||
|
||
void foo(void) /* foo external linkage'a sahip, başka modüllerden kullanılabilir */
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
static void bar(void) /* bar internal linkage'a sahip başka modüllerden kullanılamaz */
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Pekiyi bir modülde tanımlanmış olan external linkage'a sahip bir global değişkeni başka bir modülde nasıl kullanabiliriz? İşte bunun için "extern" yer
|
||
belirleyici anahtar sözcükten faydalanılmaktadır. Bir global değişken extern olarak bildirilirse bu durumda programcı derleyiciye adeta şunu
|
||
demektedir: "Derleyici bu global değişken için yer ayırma, çünkü bu global değişken aslında başka bir modülde tanımlanmış bir global değişkendir.
|
||
Yani bu global değişken için yer başka bir modülde ayrılmış durumdadır. Ben bu global değişkeni kullandığımda aslında başka bir modülde yeri ayrılmış
|
||
olan o global değişkeni kullanmış oluyorum. Dolayısıyla ben bu global değişkeni kullandığımda bana kızma". İşte derleyici de extern olarak bildirilmiş
|
||
bir global değişken için yer ayırmaz. Dolayısıyla extern bildirimi bir tanımlama oluşturmamaktadır. Örneğin:
|
||
|
||
extern int g_x;
|
||
|
||
int main(void)
|
||
{
|
||
g_x = 10;
|
||
printf("%d\n", g_x);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada derleyici g_x için yer ayırmayacaktır. Çünkü onun yeri zaten başka bir modülde ayrılmıştır. Pekiyi derleyici g_x'in yerini bilmediğine göre
|
||
nasıl makine komutları üretmektedir? İşte derleyici extern bir değişken kullanıldığında onun için ürettiği makine kodlarında bazı yerleri boş bırakmaktadır.
|
||
Ancak nereleri boş bıraktığını DA amaç dosyanın belli bir kısmına yazmaktadır. Bu değişkeni diğer modüllerde arayıp bulmak ve derleyicinin boş bıraktığı
|
||
makine komutlarındaki yerleri doldurmak linker'ın görevidir. Pekiyi linker ya global değişkenin tanımlamasını başka bir modülde bulamazsa ne olur?
|
||
İşte bu durumda link aşamasında "error" ortaya çıkacaktır. Tabii global değişken başka bir modülde tanımlanmış olabilir ancak "internal" linkage'a sahip
|
||
olabilir. Bu durumda da link aşamasında eror oluşacaktır. Şimdi de şöyle bir senaryo üzerinde duralım. Projemiz "a.c" ve "b.c" isimli iki kaynak
|
||
dosyadan oluşsun. "a.c" dosyasında da g_x global değişkeni tanımlanmış olsun, "b.c" dosyasında da g_x global değişkenş tanımlanmış olsun. Bu durumda
|
||
her iki modül de bağımsız olarak derlenir. Her iki modülde de g_x için yer ayrılır. İki derleme de başarılı olur. Ancak link aşamasında sorun çıkar.
|
||
Çünkü linker link işlemi sırasında aynı isimli birden fazla extern linkage'a sahip değişken görürse "error" oluşturmaktadır. O halde bir global değişken
|
||
tek bir modülde extern linkage'a sahip olacak biçimde global olarak tanımlanmalı, kullanılacak modüllerde extern olarak bildirilmelidir. Global değişkenin tüm modüllerde
|
||
extern bildirilmesi ancak hiçbir modülde global tanımlamanın yapılmaması yine link aşamasında "error" ile sonuçlanacaktır. Benzer biçimde global
|
||
değişkenin birden fazla modülde extern linkage'a sahip biçimde global olarak tanımlanması da link aşamasında "error" ile sonuçlanacaktır.
|
||
Global değişkenin hangi modülde extern linkagae'a sahip biçimde tanımlanmış olduğunun hiçbir önemi yoktur.
|
||
|
||
Anımsanacağı gibi C'de derleyicinin yer ayırdığı bildirimlere "tanımlama (definitions)" deniyordu. O halde bildirimde extern kullanıldığında
|
||
bu bir tanımlama anlamına gelmemektedir. Örneğin:
|
||
|
||
int g_x; /* hem bildirim hem de tanımlama, g_x için yer ayrılıyor */
|
||
extern int g_y; /* bildirim ama tanımlama değil,i g_y için yer ayrılmıyor */
|
||
|
||
C standartlarına göre extern bildirimlerinde bildirilen değişkene ilkdeğer verilirse artık bu durum "tanımlama" anlamına gelmektedir Dolayısıyla burada
|
||
"extern" anahtar sözcüğünün bir etkisi kalmamaktadır. Örneğin:
|
||
|
||
extern int g_x; /* bildirim tanımlama dğeil */
|
||
exrtern int g_y = 10; /* özel durum, artık bu bir tanımalamdır, çünkü ilkdeğer verilmiştir. g_y için yer ayrılacaktır */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyonlar için gerçekten "extern" anahtar sözcüğünün bir etkisi yoktur. Çünkü fonksiyon prototipleri zaten extern bildirimi kabul edilmektedir.
|
||
Başka bir deyişle biz bir modülde (translation unit) bir fonksiyonun prototipini yazmışsa ancak tanımlamasını yapmamışsak zaten derleyici
|
||
onun başka bir modülde tanımlanmış bir fonksiyon olduğunu düşünmektedir. Bunun için extern bildirimine ayrıca gerek yoktur. Örneğin:
|
||
|
||
void foo(void);
|
||
|
||
int main(void)
|
||
{
|
||
foo(); /* foo zaten diğer moüllerde aranacak */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada derleyici zaten foo fonksiyonun tanımını görmediğinden amaç dosyaya linker için "foo'nun başka modüllerde aranması gerektiği notunu"
|
||
yazmaktadır. Fonksiyon prototiplerinin başına extern getirmeye gerek yoktur. Ancak getirilmesi de hata oluşturmaz. Örneğin:
|
||
|
||
extern void foo(void); /* extern'e gerek yok ancak kullanılabilir */
|
||
|
||
Bu durumda C'de olmayan bir fonksiyonu çağırsak bile hata derleme aşamasında ortaya çıkmaz. Link aşamasında linker'ın fonksiyonu başka modüllerde bulamaması biçiminde
|
||
ortaya çıkar. Anımsanacağı gibi derleyici bir fonksiyonun tanımlamasını ya da prototipini görmeden fonksiyon çağrılmışsa bu durum C99 ve sonrasında
|
||
geçersizdir. Ancak C90'da fonksiyonun sanki geri dönüş değerinin int paranmetre parantezinin de boş olacak biçimde protipinin yazıldığı varsayılmaktadır.
|
||
|
||
O halde bir projenin bir modülünde başka bir modülünde tanımlanmış olan fonklsiyonu çağırmak için tek yapılacak şey fonksiyonun prototipinin
|
||
bulundurulmasıdır. Prototipte extern belirleyicisinin ayrıca kullanılmasına gerek yoktur.
|
||
|
||
Anımsanacağı gibi C'de bir global değişken birden fazla kez aynı modülde (tanslation unit) tanımlanırsa bu duruma "tentative definition" deniliyordu.
|
||
Bu durumu derleyici ""sanki tanımlama bir kez yapılmış gibi" ele alıyordu. Örneğin:
|
||
|
||
int g_x;
|
||
int g_x;
|
||
int g_x;
|
||
|
||
Bu durum C'de geçerlidir. Burada g_x için bir tane yer ayrılacaktır. Tentative definition durumunda bu tanımlamaların yalnızca birinde ilkdeğer verilebilir.
|
||
Örneğin:
|
||
|
||
int g_x;
|
||
int g_x = 10;
|
||
int g_x;
|
||
|
||
Derleyici burada g_x'e ilkdeğer olarak 10 verilmiş olduğunu varsaymaktadır. Ancak birden fazla kez ilkdeğer verilemez. Örneğin:
|
||
|
||
int g_x = 10;
|
||
int g_x = 20; /* geçersiz! */
|
||
int g_x;
|
||
|
||
Ancak standartlara göre farklı modüllerde tentative definition geçerli değildir. Fakat derleyicilerin bir bölümü bunu geçerli olarak
|
||
ele alabilmektedir. Örneğin:
|
||
|
||
/* a.c */
|
||
|
||
int g_x;
|
||
...
|
||
|
||
/* b.c */
|
||
|
||
int g_x;
|
||
...
|
||
|
||
Bu durum C standartlarına geçersizdir. Ancak Microsoft derleyicileri bu durumda bir hata mesajı vermemektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
59.Ders - 05/01/2023 Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
extern bildirimi genellikle global düzeyde programın tepesinde yapılmaktadır. Ancak C'de programcı isterse extern bildirimini yerel bir blokta da
|
||
yapabilir. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
extern int g_x;
|
||
|
||
/* ... */
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
g_x = 10; /* geçersiz! */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
extern bildiriminin yerel bir blokta yapılması kişilere tuhaf gelebilmektedir. Çünkü extern ile bildirilen değişken başka bir modüldeki
|
||
global bir değişkendir. O halde başka bir modüldeki global bir değişkenin yerel biçimde extern olarak bildirilmesinin ne anlamı olabilir? İşte extern bildirimi
|
||
yerel bir blokta yapılırsa yine değişken global bir değişken olur. Ancak o global değişken yalnızca o blokta kullanılabilir. Yukarıdakii örnekte
|
||
g_x başka bir modülde tanımlanmış olan global bir değişkendir. Ancak biz bu g_x değişkenini yalnızca foo içerisinde kullanabiliriz. Dolayısıyla bu
|
||
değişkenin bar içerisinde kullanımı da geçersizdir. Tabii yukarıda da belirttiğimiz gibi en normal durum extern bildiriminin programın tepesinde yapılmasıdır.
|
||
Örneğin:
|
||
|
||
extern int g_x;
|
||
|
||
void foo(void)
|
||
{
|
||
g_x = 20; /* geçerli */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
g_x = 10; /* geçerli */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
extern belirleyicisi ile bildirilen değişkene ilkdeğer verilmesi durumunda extern belirleyicinin bir anlamı kalmıyordu. İşte yerel düzeyde extern olarak
|
||
bildirilen değişkenlere ilkdeğer verilememektedir. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
extern int g_x = 10; /* geçersiz! */
|
||
|
||
/* ... */
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Birden fazla modülle çalışma nasıl yapılmaktadır? IDE'li sistemlerde bu çok kolaydır. Programcının tek yapacağı şey projeye birden fazla kaynak dosyayı
|
||
eklemektir. Biz örneğin Visual Studio IDE'sinde projeye "sample.c" ve "mample.c" biçiminde iki kaynak dosya eklemiş olalım. Bu durumda Ctrl+F5
|
||
tuşuna bastığımızda IDE önce bu "sample.c" ve "mample.c" dosyalarını bağımsız bir biçimde derler ve bunlardan "sample.obj" ve "mample.obj"
|
||
biçiminde iki amaç dosya oluşturur. Sonra da bu iki amaç dosyayı birleştirerek proje ismi ile aynı isimli "exe" dosya oluşturur.
|
||
|
||
Genel olarak IDE'lerde "build" ya da "make" işlemi "yalnızca değişmiş olan kaynak dosyaları yeniden derle ve hepsini birden link işlemine sok"
|
||
anlamına gelmektedir. Örneğin biz Visual Studio IDE'sinde projemize 100 tane kaynak dosya eklemiş olalım. Bu projeyi ilk kez çalıştırdığımızda
|
||
bu dosyaların hepsi mecburen derlenecektir. Ancak daha sonra hangi kaynak dosyalar üzerinde değişiklik yapılmışsa "build" işlemi sırasında yalnızca
|
||
o kaynak dosyalar yeniden derlenir ve hep birlikte tüm amaç dosyalar link işlemine katılır. Bu bizim istediğimiz bir şeydir. Biz Ctrl+F5 tuşuna
|
||
bastığımızda ya da Build menüsünden Build seçeneklerini seçtiğimizde tüm kaynak dosyalar değil yalnızca değişmiş olanlar yeniden derlenmektedir.
|
||
Yalnızca Visual Studio'da değil tüm C IDE'lerinde durum böyledir.
|
||
|
||
IDE'lerde ayrıca bir de "Build All" ya da "Rebuild" seçenekleri de vardır. Bu seçenekler "yalnızca değişmiş olan dosyaların değil projedeki tüm
|
||
dosyaların kayıt şartsız yeniden derlenmesi ve amaç dosyaların link edilmesi" anlamına gelmektedir. Bazen build sisteminde sorunlar oluştuğunda
|
||
programcı şüphe altında kalırsa tüm kaynak dosyaları yeniden derlemek isteyebilir. Tabii her defasında "rebuild" işleminin yapılması çalışma
|
||
döngüsü bakımından anlamsızdır.
|
||
|
||
Pekiyi build sistemi bir kaynak dosyanın değişip değişmediğini nasıl anlamaktadır? IDE'ler build işlemini yapmadan önce kesinlikle kaynak dosyaları
|
||
save etmektedir. Build sisteminin tek yaptığı şey "kaynak dosyanın son değiştirilme tarih ve zaman bilgisi ile amaç dosyanın son değiştirilme tarih ve zaman
|
||
bilgisini" karşılaştırmaktır. Eğer kaynak dosyanın son değiştirilme tarih ve zaman bilgisi amaç dosyanınkinden daha ileride ise bu durum kaynak dosyanın değişmiş olduğu
|
||
anlamnına gelmektedir. Bazen patolojik durumlarda dosyaların tarih ve zamana bilgileri bozulabilir. Ya da işletim sisteminin tarih ve zaman
|
||
bilgisi değiştirilmiş olabilir. Bu tür durumlarda build sistemi de bozulabilir. İşte programcı şüphe altında kaldığı bu tür durumlarda "rebuild"
|
||
işlemi yapmalıdır.
|
||
|
||
IDE'lerde "build", "rebuild" seçeneklerinin yanı sıra bir de "clean" biçiminde bir seçenek bulunabilmektedir. Build terminolojisinde "clean" işlemi demek
|
||
"tüm amaç dosyaları ve çalıştırılabilen dosyaları silmek" demektir. Dolayısıyla biz "clean" yaptığımızda sanki hiç derleme yapmamış gibi bir durumda oluruz.
|
||
Tabii bu duurmda "build" yapılırsa mecburen tüm kaynak dosyalar yeniden derlenecektir. Pekiyi "clean" işlemine neden gereksinim duyulmaktadır?
|
||
İşte genellikle programcı projeyi birisine vermek ya da onu saklamak için "clean" işlemini tercih edebilmektedir. Bu sayede proje amaç dosyalardan
|
||
ve çalıştırılabilir dosyadan arındırılmış olmaktadır. Ayrıca clean işlemi build otomasyon sistemlerinde "rebuild" işlemine zorlamak için de kullanılabilmektedir.
|
||
|
||
Visual Studio IDE'sinde bir "solution" içerisinde birden fazla proje bulunabilmektedir. Ancak bu projelerin yalnızca bir tanesi aktif olmaktadır.
|
||
(Aktif proje siyahrenkle gösterilir) Build menüsündeki "Build Solution", "Rebuild Solution" ve "Clean Solution" menü elemanları solution içerisindeki tüm
|
||
projeler için eylem belirtir. Halbuki "Build XXX", "Rebuild XXX" ve "Clean XXX" (burada XXX aktif projenin ismidir) yalnızca aktif olan projede eylem
|
||
belirtmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi birden fazla modülle çalışırken IDE kullanmıyorsak bu işlemleri nasıl yapabiliriz? Bunun iki yolu olabilir:
|
||
|
||
1) Değişen dosyaları komut satırında manuel derlemek ve linker programını ayrıca açalıştırarak link işlemini de manuel biçimde yapmak.
|
||
2) Bu işlemi otomatize eden ve ismine "build automation tool" denilen özel araçları kullanmak.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Örneğin "sample.c" ve "mample.c" dosyalarından oluşan bir projeyi Microsoft derleyicileri ile manuel bir biçimde derlemek isteyelim. Dosyalar şöyle olsun:
|
||
|
||
/* sample.c */
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(void);
|
||
|
||
extern int g_x;
|
||
|
||
int main(void)
|
||
{
|
||
g_x = 10;
|
||
|
||
foo();
|
||
|
||
printf("%d\n", g_x);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/* mample.c */
|
||
|
||
#include <stdio.h>
|
||
|
||
int g_x;
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo\n");
|
||
|
||
g_x = 20;
|
||
}
|
||
|
||
Bu işlem manuel olarak şöyle yapılabilir:
|
||
|
||
cl /c sample.c
|
||
cl /c mample.c
|
||
|
||
link /OUT:test.exe sample.obj mample.obj
|
||
|
||
Burada /c seçeneği "only compile" anlamına gelmektedir Yani yalnızca derleme yapılır ancak link işlemi yapılmaz. Microsoft'un linker programı "link.exe"
|
||
isimli programdır. Burada /OUT seçeneği oluşturulacak çalıştırılabilir dosyaya isim vermek amacıyla kullanılmaktadır. Diğer bir seçenek de "cl.exe"
|
||
derleyicisini çalıştırırken tüm kaynak dosyaları vermek olabilir. Ancak bu durumda verilen tüm kaynak dosyalar koşulsuz derlenip link işlemine
|
||
sokulmaktadır. Dolayısıyla bu seçenek az sayıda kaynak dosya için uygulanabilir bir seçenktir. Örneğin:
|
||
|
||
cl /Fe:test.exe sample.c mample.c
|
||
|
||
gcc ve clang derleyicilerinde işlemler benzerdir. "Only compile" için "-c" seçeneği kullanılır. Ancak bu sistemlerdeki "ld" isimli linker
|
||
maalesef kütüphane dosyalarını ve "start-up" dosyaları link işlemine katmamaktadır. Bunların link işlemine katılması biraz< zahmetlidir. Bunun yerine
|
||
link işlemi de "gcc" programına yaptırılabilir. Tabii "gcc" derleyicisi link için çalıştırılınca aslında yine "gcc" programı arka planda "ld"
|
||
linker'ını çalıştırmaktadır. Ancak bu linker'ı kütüphane dosyalarını ve start-up dosyaları vererek çalıştırmaktadır. İşlemler şöyle yapılabir:
|
||
|
||
gcc -c sample.c
|
||
gcc -c mample.c
|
||
gcc -o test sample.o mample.o
|
||
|
||
gcc ve clang derleyicilerinin genel kullanımı aynıdır. UNIX/Linux sistemlerinde amaç dosyaların uzantılarının ".obj" biçiminde değil ".o" biçiminde
|
||
olduğuna dikkat ediniz. gccve clang derleyicilerinde de birden fazla kaynak dosya yine komut satırında belirtilebilir. Örneğin:
|
||
|
||
gcc -o test sample.c mample.c
|
||
|
||
Burada yine her iki dosya da şartsız derlenecek ve birlikte link işlemine sokulacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Manuel bir biçimde komut satırından yalnızca değişen kaynak dosyaların derlenerek link edilmesi az sayıda kaynak dosya olduğunda uygulanabilecek
|
||
bir yöntemdir. Ancak yüzlerce kaynak dosyanın bulunduğu bir projede (böyle çok proje vardır) build işleminin manuel yapılması çok zahmetlidir.
|
||
İşte bu nedenle bu işlemi otomatize eden ve ismine "build automation tools" denilen araçlar kullanılmaktadır. Aslında IDE'ler de arka planda bu build araçlarını
|
||
kullanmaktadır. Yani örneğin biz Visual Studio'da Ctrl+F5 tuşlarına bastığımızda ya da "Build" menüsünden "Build" seçeneğini seçtiğimizde arka planda
|
||
aslında IDE Microsoft'un "MSBuild" denilen build otomasyon aracını devreye sokmaktadır.
|
||
|
||
Build işlemini otomatize eden pek çok araç geliştirilmiştir. Build işlemi programlama dili ile de ilgilidir. Dolayısıyla örneğin Java'da kullanılan
|
||
build araçları ile C'de kullanılan build araçları birbirinden farklı olabilmektedir. C ve C++ dünyasında en çok kullanılan build araçları
|
||
Microsoft'un "MSBuild" denilen aracı ile GNU'nun "make" denilen aracıdır. Aslında "make" aracının orijinali eski UNIX sistemlerine dayanmaktadır.
|
||
Ancak bu araç GNU projesi kapsamında "GNU make" ismiyle yeniden yazılmıştır. Microsoft'un da "make" benzeri bir aracı vardır. Ona da "nmake" denilmektedir.
|
||
Tabii Microsoft uzun süredir XML tabanlı "MSBuild" aracını kullanmaktadır.
|
||
|
||
Build araçlarında çalışma biçimi şöyledir: Programcı build işlemi sırasında yapılacak şeyleri belli bir sentaks ve semantiği olan bir dilde
|
||
bir dosya içerisine yazar. Sonra bu dosyayı işleten programı çalıştırır. Örneğin GNU Make denilen araç ile çalışılırken programcı önce
|
||
ismine "Makefile" denilen bir dosya oluşturur. Bu dosyanın içerieine yönergeleri yazar. Sonra "make" denilen programı bu dosyayı vererek çalıştırır.
|
||
make programı da bu makle dosyasını okur yönergeleri yerine getirir. Make dosyaı oluştmanın bazı yarıntılı kurallaerı vardır. Yani bu küçük bir dil gibidir.
|
||
Make aracının kullanımı "Sistem Programlama ve İleri C Uygulamaları" kursunda anlatılmaktadır. Microsoft'un MSBuild aracı da XML tabanlı bir
|
||
dil kullanmaktadır. Yine biz Visual Studio IDE'sinde projeye bir kaynak dosya ekleyip, proje ayarlarını değiştirdiğimizde aslında Visual Studio
|
||
arka planda bir XML dosyasını oluşturmaktadır. MSBuild aracı bu XML dosyasını okuyarak işlemlerini yapmaktadır.
|
||
|
||
Bazı build araçlarına "üst düzey build araçları" denilmektedir. Bunlar aslında daha aşağı seviyeli build araçlarını kullanmaktadır. Bunların en ünlüleri
|
||
"cmake" ve "qmake" isimli araçlardır. Bu iki araç aslında ürün olarak make dosyaları üretmektedir. Bu araçların ürettikleri make dosyaları ayrıca "make"
|
||
programıyla işletilmelidir. Ancak "cmake" ve "qmake" araçları daha basit bir yapıya sahiptir. Dolayısıyla aslında bu araçlar "make dosyası yazmayı"
|
||
otomatize eden ve kullanımı daha basit olan araçlardır. Örneğin biz cmake ile çalışmak istediğimizde yine "cmake" diline uygun bir dosya oluştururuz. Bunu
|
||
"cmake" programına işletiriz. "cmake" programı buradan bir make dosyası oluşturur. Bu make dosyasını da "make" programıyla işleme sokarız.
|
||
|
||
Windows sistemlerinde GNU'nun make programının Windows port'u kullanılabilir. Ancak Microsoft'un GNU make programına benzer "nmake" denilen
|
||
bir aracı da vardır. Windows sistemlerinde nmake kullanmak daha uygun olabilmektedir. macOS sistemlerinde de ağırlıklı olarak "GNU make", "cmake"
|
||
ve "qmake" araçları kullanılmaktadır. Ayrıca XCode'un kedni proje formatı da vardır.
|
||
|
||
Aşağıda "sample.c" ve "mample.c" dosyalarından oluşan basit projenin build edilmesi için bir make dosyasu örneği verilmiştir. (make programı default durumda
|
||
eğer dosya ismi belirtilmezse "Makefile" isimli bir dosyayı işleme sokmaktadır.)
|
||
|
||
# Makefile
|
||
|
||
test: sample.o mample.o
|
||
gcc -o test sample.o mample.o
|
||
sample.o: sample.c
|
||
gcc -c sample.c
|
||
mample.o: mample.c
|
||
gcc -c mample.c
|
||
clean:
|
||
rm -f *.o
|
||
rm -f test
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
static yer belirleyicisi yerel değişkenlerle ve global değişkenlerle kullanılabilmektedir. Ancak parametre değişkenleriyle kullanılamamaktadır.
|
||
static belirleyicisinin yerel değişkenlerle kullanımı "static yerel değişkenler", global değişkenlerle kullanımı "static global değişkenler"
|
||
biçiminde isimlendirilebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir global değişken "static" anahtar sözcüğü ile tanımlanırsa global değişken "internal linkage" özelliğine sahip olur. Yani bu global değişken başka bir
|
||
modülde extern olarak bildirilse bile kullanılamaz. Yalnızca o modülde (tanslation unit) kullanılabilir.
|
||
|
||
Aynı durum fonksiyonlar için de geçerlidir. Çünkü fonksiyonlar da "global değişkenler gibi" ele alınmaktadır. Yani biz bir fonksiyonun tanımlamasında
|
||
static belirleyicisini kullanırsak o fonksiyonu yalnızca o modülde herhangi bir yerde çağırabiliriz. Başka bir modülden o fonksiyonu çağıramayız.
|
||
static fonksiyonlar da "internal" linkage özelliğine sahiptir.
|
||
|
||
Farklı modüllerde (translation unit'lerde) aynı isimli static global değişkenler ya da fonksiyonlar bulunabilir. Bu durum link aşamasında bir sorun oluşturmaz.
|
||
|
||
Aşağıdaki örnekte her iki modülde de g_x isminde static global nesneler tanımlanmıştır. Ancak her iki modüldeki nesneler farklı nesnelerdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/* sample.c */
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(void);
|
||
void bar(void);
|
||
|
||
static int g_x;
|
||
|
||
int main(void)
|
||
{
|
||
g_x = 100;
|
||
|
||
foo();
|
||
bar();
|
||
|
||
printf("%d\n", g_x); /* 100 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
#include <stdio.h>
|
||
|
||
static int g_x = 10;
|
||
|
||
void foo(void)
|
||
{
|
||
g_x = 20;
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
g_x = 30;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
60.Ders - 10/01/2023 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
static global bildiriminde şöyle bir kural vardır: Bir global değişken ya da fonksiyon extern olarak bildirildiyse bu global değişken
|
||
ya da fonksiyon daha önce hangi linkage ile bildirilmişse o linkage'a sahip olur. Yani örneğin biz bir fonksiyonun prototipinde static anahtar sözcüğünü
|
||
belirtmişsek daha sonra static anahtar sözcüğünü belirtmesek bile bu fonksiyon static kabul edilir. Örneğin:
|
||
|
||
static void foo(void);
|
||
|
||
/* ... */
|
||
|
||
void foo(void) /* burada hiçbir şey yazılmmamışsa fonksiyonlar için sanki extern yazılmış gibi işlem yapılır, dolayısıyla linkage "internal" durumdadır */
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Tabii okunabilirlik bakımından static anahtar sözcüğünün hem prototipte hem de tanımlamada belirtilmesi iyi bir tekniktir.
|
||
|
||
Ancak yukarıdaki durumun tersi geçerli değildir. Yani biz bir fonksiyonun önce external linkage olarak prototipini yazıp sonra onu internal linkage
|
||
ile tanımlayamayız. Örneğin:
|
||
|
||
void foo(void);
|
||
|
||
static void foo(void) /* geçerli değil! */
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Aynı dıırm global değişkenler için de geçerlidir. Örneğin:
|
||
|
||
static int g_x;
|
||
int g_x;
|
||
|
||
Burada g_x static global ve internal linakage'a sahiptir. Ancak aşağıdaki tanımlama geçersizdir:
|
||
|
||
int g_x;
|
||
static int g_x; /* geeçersiz! */
|
||
|
||
Benzer biçimde bir global değişken ya da fonksiyon önce static belirleyicisi ile tanımlanıp sonra extern belirleyici ile tanımlansa da
|
||
yine tanımlama geçerlidir. Örneğin:
|
||
|
||
static int g_x;
|
||
extern int g_x;
|
||
|
||
Burada g_x internal linkage'a sahiptir:.
|
||
|
||
Yukarıdaki tanımlamalar "tentative" biçimde kabul edilmektedir. Yani bu örneklerde yalnızca bir tane nesne oluşturulmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi bir global değişken ya da fonksiyon ne zaman static yapılmalıdır? Eğer programcı tek bir kaynak dosya üzerinde çalışıyorsa ya da az sayıda
|
||
kaynak dosya ile çalışıyorsa global değişkenleri ya da fonksiyonları static yapmadan "external linkage"a sahip olarak yazabilir. Ancak büyük projelerde
|
||
ya da çok sayıda kaynak dosya ile proje geliştirirken programcı başka bir modülden kullanılmayacak olan global değişkenleri ve fonksiyonları
|
||
static yapmalıdır. Bu sayede "isim kirliliğinin (name pollution)" önüne geçilebilir. Eğer büyük projelerde başka bir modül tarafından kullanılmayacağı halde
|
||
bir global değişken ya da fonksiyon static yapılmazsa tesadüfen başka bir modülde aynı isimli bir global değişken ya da fonksiyon olabilir ve bu durum
|
||
link aşamasında soruna yol açabilir.
|
||
|
||
static fonksiyonların prototiplerinin başlık dosyalarına (header files) yerleştirilmesi anlamsızdır. Çünkü başlık dosyaları birden fazla kaynak dosyadan
|
||
include edilmek üzere oluşturulurlar. static fonksiyonlar zaten tek bir modülde kullanılabilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yerel değişkenin başına static belirleyicisi getirilirse böyle değişkenlere "static yerel değişkenler" denir. Buradaki static belirleyicisinin
|
||
global değişkenler ya da fonksiyonlardaki static belirleyicisi ile anlam bakımından hiçbir benzerliği yoktur. Bir yerel değişken static yapılırsa
|
||
bu yerel değişken statik ömürlü olur. Yani program yüklendiğinde yaratılır, program sonlanana kadar bellekte kalır. C'de static yerel değişkenlere verilen ilkdeğerler
|
||
sabit ifadesi olmak zorundadır. Çünkü bu ilkdeğerler derleme aşamasında derleyici tarafından hesaplanıp çalştırılabilen dosyaya yerleştirilmektedir.
|
||
Program çalışırken static yerel değişkenlere verilen ilkdeğerler artık etki göstermez. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
static int i = 10;
|
||
|
||
++i;
|
||
|
||
printf("%d\n", i);
|
||
}
|
||
|
||
Bir static terel değişkene verilen ilkdeğerler fonksiyonun her çağrılmasında o değişkene atanmamaktadır. Derleyiciler bu ilkdeğerleri derleme aşamasında
|
||
ele almaktadır. Programın çalışma zamanı sırasında bu ilkdeğerler adeta koddan kaldırılmaktadır.
|
||
|
||
Burada foo çağrılsa da çağrılmasa da yerel i değişkeni static düzeyde program yüklendiğinde yaratılmış olacaktır. Bu fonksiyon çağrılıp sonlansa bile i
|
||
değişkeni değerini koruyacaktır. Çünkü static yerel değişkenler blok bittiğinde yok edilmezler.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(void)
|
||
{
|
||
static int i = 10;
|
||
|
||
++i;
|
||
|
||
printf("%d\n", i);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
foo(); /* 11 */
|
||
foo(); /* 12 */
|
||
foo(); /* 13 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
static yerel değişkenlere ilkdeğer verilmeyebilir. Bu durumda onların içerisinde 0 değeri bulunur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void foo(void)
|
||
{
|
||
static int i;
|
||
|
||
++i;
|
||
|
||
printf("%d\n", i);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
foo(); /* 1 */
|
||
foo(); /* 2 */
|
||
foo(); /* 3 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
static yerel değişkenlerle global değişkenlerin her ikisi de statik ömürlüdür. Yani hem static yerel değişkenler hem de global değişkenler program
|
||
belleğe yüklendiğinde yaratılırlar. Program sonlanana kadar bellekte kalırlar. Ancak global değişkenler her yerde kullanılabilirken (hatta extern yapılarak
|
||
başka bir modülden de kullanılabilir) yerel değişkenler yalnızca o blokta kullanılabilirler.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi C'de bir fonksiyonun yerel bir değişkenin ya da dizinin adresiyle geri dönmemesi gerekmektedir. Çünkü fonksiyon bittiğinde bu
|
||
yerel değişken ya da dizi bellekten boşaltılacağı için artık geri döndürülen adres ratgele bir adres gibi olmaktadır. Ancak bir fonksiyonun static yerel bir
|
||
değişkenin ya da dizinin adresiyle geri dönmesinde bir sakınca yoktur. Çünkü fonksiyondan çıkılsa bile static yerel değişkenler ve diziler yaşamaya
|
||
devam ederler. Tabii bu tür durumlarda biz fonksiyonu her çağırdığımızda aynı adresi elde ederiz. Aşağıdaki örnekte bu durum gösterilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
char *getname()
|
||
{
|
||
static char name[64];
|
||
|
||
printf("Adi soyadi:");
|
||
gets(name);
|
||
|
||
return name; /* tamamen normal ve geçerli bir durum, dizi static */
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char *name;
|
||
|
||
name = getname();
|
||
printf("%p, %s\n", name, name);
|
||
|
||
name = getname();
|
||
printf("%p, %s\n", name, name);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi static yerel değişkenlere ne zaman gereksinim duyulmaktadır? İşte biz fonksiyon bittiği halde bir değişkenin değerini korumasını isteyebiliriz.
|
||
Bu durumda static yerel değişkenleri kullanbiliriz. Global değişkenler de statik ömürlüdür. Ancak eğer değişken yalnızca bir fonksiyon içerisinde kullanılacaksa
|
||
onun gereksiz bir biçimde global olarak tanımlanması kötü teknik olur. Tabii bir gerekçe yoksa static yerel değişken kullanmak da kötü bir tekniktir.
|
||
static yerel değişkenler sürekli yer kaplamaya devam ederler.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Adrese geri dönen bir fonksiyon için üç durum söz konusu olabilir:
|
||
|
||
1) Fonksiyon bizim argüman olarak verdiğimiz adresin aynısına geri dönüyor olabilir. Bizim ona verdiğimiz nesne fonksiyonun çıkışında da yaşadığına göre bir sorun
|
||
yoktur. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
char *revstr(char *str)
|
||
{
|
||
int n;
|
||
char ch;
|
||
|
||
for (n = 0; str[n] != '\0'; ++n)
|
||
;
|
||
|
||
for (int k = 0; k < n / 2; ++k) {
|
||
ch = str[k];
|
||
str[k] = str[n - k - 1];
|
||
str[n - k - 1] = ch;
|
||
}
|
||
|
||
return str;
|
||
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char s[] = "ankara";
|
||
char *str;
|
||
|
||
str = revstr(s);
|
||
|
||
puts(s);
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
Burada revstr zaten bizim verdiğimiz adrese geri dönmektedir. Kodda herhangi bir tanısmsız davranış yoktur.
|
||
|
||
2) Fonksiyon dinamik bir biçimde tahsis edilen bir alanın adresiyle geri dönüyor olabilir. Bu durumda fonksiyon sonlansa bile dinamik alan yaşamaya devam edeceğine göre
|
||
burada da bir sorun yoktur. Ancak bu durumda alanın free getirilmesi fonksiyonu çağıranın sorumluluğunda olur. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
char *getname(void)
|
||
{
|
||
char buf[1024];
|
||
char *name;
|
||
|
||
printf("Adi soyadi:");
|
||
gets(buf);
|
||
|
||
if ((name = (char *)malloc(strlen(buf) + 1)) == NULL)
|
||
return NULL;
|
||
|
||
strcpy(name, buf);
|
||
|
||
return name;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char *name;
|
||
|
||
if ((name = getname()) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
puts(name);
|
||
|
||
free(name);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Buradaki getame fonksinu her çağrıldığında bize farklı bir adres verecektir.
|
||
|
||
3) Fonksiyon static yerel bir nesnenin ya da dizinin adresiyle geri dönmektedir. Bu durumda fonksiyonu her çağırdığımızda bize hep aynı adresi verecektir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
char *getname()
|
||
{
|
||
static char name[64];
|
||
|
||
printf("Adi soyadi:");
|
||
gets(name);
|
||
|
||
return name; /* tamamen normal ve geçerli bir durum, dizi static */
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
char *name;
|
||
|
||
name = getname();
|
||
printf("%p, %s\n", name, name);
|
||
|
||
name = getname();
|
||
printf("%p, %s\n", name, name);
|
||
|
||
return 0;
|
||
}
|
||
|
||
static yerel nesnelerin adresleriyle geri dönmek çok thread'li uygulamalarda fonksiyonun "thread güvenli (thread safe)" olmamamasına yol açmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
static yerel nesnelerin adresleriyle geri dönen fonksiyonlara iyi bir örnek "localtime" isimli standart C fonksiyonudur. Fonksiyonun prototipi <time.h>
|
||
dosyası içerisindedir:
|
||
|
||
struct tm *localtime(const time_t *pt);
|
||
|
||
Burada const anahtar sözcüğü izleyen bölümlerde ele alınmaktadır. Fonksiyon bizden time fonksiyonundan elde edilmiş olan değerin bulunduğu nesnenin
|
||
adresini alır. Pek çok sistemde time fonksiyonu 01/01/1970'ten geçen saniye sayısını vermektedir. localtime fonksiyonu bilgisayarın saatine bakmaz.
|
||
time fonksiyonu bilgisayarın saatine bakmaktadır. localtime fonksiyonu bu saniye sayısını yıl, ay, gün, saat, dakika, saniye bileşenlerine ayırır.
|
||
struct tm isimli static bir yapı nesnesinin içerisine yerleştirir ve bu static yapı nesnesinin adresiyle geri döner. struct tm yapısı da <time.h>
|
||
dosyası içerisinde aşağıdaki gibi bildirilmiştir:
|
||
|
||
struct tm {
|
||
int tm_sec; /* seconds */
|
||
int tm_min; /* minutes */
|
||
int tm_hour; /* hours */
|
||
int tm_mday; /* day of the month */
|
||
int tm_mon; /* month */
|
||
int tm_year; /* year */
|
||
int tm_wday; /* day of the week */
|
||
int tm_yday; /* day in the year */
|
||
int tm_isdst; /* daylight saving time */
|
||
};
|
||
|
||
Yapının tm_sec, tm_min ve tm_hour elemanları zaman bilgisine ilişkindir. tm_mday elemanı ayın kaçıncı günü olduğunu belirtir. tm_mon elemanı
|
||
0 orijinlidir ve ayı belirtmektedir. tm_year elemanı 1900 orijinli olarak yılı belirtmektedir. tm_wday tarihin haftanın kaçıcncı günü olduğunu belirtir.
|
||
Burada 0 = Pazar anlamına gelmektedir. tm_yday elemanı tarihin 0 orijinli olarak o yılın kaçıncı günü olduğunu belirtmektedir. tm_isdst elemanı pozitif bir değerdeyse
|
||
tarih "ileri saat uygulamasının içerisinde" kalmaktadır. 0 ise "ileri saat uygulamasının içerisinde" kalmamaktadır. Negatif ise bu konuda bir bilgi yoktur.
|
||
localtime fonksiyonu geçersiz bir parametre için NULL adrese geri dönmektedir.
|
||
|
||
Aşağıdaki örnekte önce time fonksiyonu ile epoch'tan (01/01/1970) geçen saniye sayısı bulunup localtime fonksiyonu ile tarih zaman bilgisine dönüştürülmüştür.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
time_t t;
|
||
struct tm *pt;
|
||
char *days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
|
||
|
||
t = time(NULL);
|
||
pt = localtime(&t);
|
||
|
||
printf("%02d/%02d/%04d %02d:%02d:%02d - %s\n", pt->tm_mday, pt->tm_mon + 1, pt->tm_year + 1900, pt->tm_hour, pt->tm_min, pt->tm_sec, days[pt->tm_wday]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
61. Ders - 12/01/2023 Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
ctime isimli standart C fonksiyonu epoch orijininden geçen saniye sayısının bulunduğu nesnesin adresini alarak static yerel char türden bir dizinin adresiyle
|
||
geri dönmektedir. Bu dizi de ilgili tarih ve zamanın yazısal karşılığı bulunmaktadır. Fonksiyonun prototipi şöyledir:
|
||
|
||
char *ctime(const time_t *pt);
|
||
|
||
Fonksiyonun geri döndürdüğü yazı şu formattadır:
|
||
|
||
Thu Jan 12 20:30:45 2023
|
||
|
||
Gün ve ay bilgisi İngilizce ve üçer karakterdir.
|
||
|
||
Aşağıda fonksiyonun kullanımına yönelik bir örnek verilmiştir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
time_t t;
|
||
char *str;
|
||
|
||
t = time(NULL);
|
||
str = ctime(&t);
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
asctime isimli standart C fonksiyonu ctime fonksiyonun yaptığının aynısını yapmaktadır. Ancak parametre olarak struct tm yapısı türünden bir adres
|
||
alır. Prototipi şöyledir:
|
||
|
||
char *asctime(const struct tm *tm);
|
||
|
||
asctime fonksiyonu ctime(localtime(&t)) ile tamamen eşdeğerdir. Fonksiyon başarısızlık durumunda NULL adrese geri dönmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
time_t t;
|
||
char *str;
|
||
|
||
t = time(NULL);
|
||
str = asctime(localtime(&t));
|
||
puts(str);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
mktime isimli standart C fonksiyonu localtime fonksiyonunun ters işlemini yapmaktadır. Yani fonksiyon bizden bir struct tm yapı nesnesinin adresini alır.
|
||
Bize epoch'tan geçen saniye sayısını (bunun saniye sayısı olması standartlarda garanti edilmemiştir) verir. Fonksiyonun prototipi şöyledir:
|
||
|
||
time_t mktime(struct tm *tm);
|
||
|
||
time fonksiyonu bilgisayarın saatine bakarak o anki zamana ilişkin epoch'tan geçen değeri bize verir. (Biz belli bir zamana ilişkin epoch'tan geçen değeri
|
||
elde etmek isteyebiliriz. Örneğin 01/01/1990'a ilişkin epoch'tan geçen saniye sayısını elde etmek isteyebiliriz.)
|
||
|
||
Fonksiyon başarısızlık durumunda -1 değerine geri dönmektedir.
|
||
|
||
mktime fonksiyonunu çağırırken biz struct tm yapısının tm_wday ve tm_yday elemanlarını doldurmayız. Fonksiyon çıkışta bu elemanları diğer elemanlardan harketele
|
||
doldurmaktadır. struct tm yapısının tm_isdst elemanına biz pozitif bir değer geçersek ilgili tarihte ileri saat uygulamasının olduğu anlamına gelir. 0 değeri
|
||
olmadığı anlamına gelmektedir negatif bir değer ise fonksiyonun bu belirlemeyi kendisinin yapacağını belirtmektedir.
|
||
|
||
Yapının tm_year elemanı 1900'den itibaren yıl belirtmesi gerekir. tm_mon elemanında yine Ocak 0'dan başlatılmaktadır.
|
||
mktime fonksiyonu normalizasyon yapmaktadır. Yani biz struct tm yapısına büyük değerler girebiliriz. Bu durum fonksiyon
|
||
tarafından ele alınabilmektedir. (Örneğin yapının tm_min elemanına 500 gibi bir değer girsek bu durumda bu değer 60'a
|
||
bölünüp fazlalık yapının tm_hour kısmına aktarılacaktır.)
|
||
|
||
Aşağıdaki mktime fonksiyonun kullanımına bir örnek verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
time_t t;
|
||
struct tm tm = {0};
|
||
struct tm *pt;
|
||
|
||
char *days[] = {"Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"};
|
||
|
||
tm.tm_year = 80;
|
||
tm.tm_mon = 0;
|
||
tm.tm_mday = 1;
|
||
tm.tm_isdst = -1;
|
||
|
||
t = mktime(&tm);
|
||
pt = localtime(&t);
|
||
|
||
printf("%02d/%02d/%04d %02d:%02d:%02d - %s\n", pt->tm_mday, pt->tm_mon + 1, pt->tm_year + 1900, pt->tm_hour, pt->tm_min, pt->tm_sec, days[pt->tm_wday]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Örneğin biz mktime fonksiyonu ile belli bir tarihten itibaren belli bir zaman ileriyi ya da geriyi elde edebiliriz. Ya da örneğin iki tarih arasında geçen süreyi de
|
||
yine bu sayade elde edebiliriz. Örneğin 17 Ağustos 1999 depreminden (03:02) ne kadar süre geçtiğini hesaplamaya çalışalım. Bunu şöyle yapabiliriz:
|
||
|
||
time_t now, eqwk, result;
|
||
struct tm tm = {0};
|
||
struct tm *pt;
|
||
|
||
tm.tm_year = 99;
|
||
tm.tm_mon = 7;
|
||
tm.tm_mday = 17;
|
||
tm.tm_isdst = -1;
|
||
tm.tm_hour = 3;
|
||
tm.tm_min = 2;
|
||
tm.tm_sec = 0;
|
||
|
||
now = time(NULL);
|
||
eqwk = mktime(&tm);
|
||
result = now - eqwk;
|
||
|
||
pt = localtime(&result);
|
||
|
||
printf("%d yıl, %d ay, %d gün, %d saat, %d dakika, %d saniye\n", pt->tm_year - 70, pt->tm_mon + 1, pt->tm_mday, pt->tm_hour, pt->tm_min, pt->tm_sec);
|
||
|
||
Tabii epoch orijini C'de standart bir biçimde belirlenmemiştir. Ancak UNIX/Linux sistemlerinde 01/01/1970 biçiminde
|
||
standart bir belirleme yapımıştır. Dolayısıyla kodun C standartları bağlamında taşınabilirliği yoktur.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
time_t now, eqwk, result;
|
||
struct tm tm = {0};
|
||
struct tm *pt;
|
||
|
||
tm.tm_year = 99;
|
||
tm.tm_mon = 7;
|
||
tm.tm_mday = 17;
|
||
tm.tm_isdst = -1;
|
||
tm.tm_hour = 3;
|
||
tm.tm_min = 2;
|
||
tm.tm_sec = 0;
|
||
|
||
now = time(NULL);
|
||
eqwk = mktime(&tm);
|
||
result = now - eqwk;
|
||
|
||
pt = localtime(&result);
|
||
|
||
printf("%d yıl, %d ay, %d gün, %d saat, %d dakika, %d saniye\n", pt->tm_year - 70, pt->tm_mon + 1, pt->tm_mday,
|
||
pt->tm_hour, pt->tm_min, pt->tm_sec);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
time_t türünün saniye belirtmesi ve dolayısıyla da iki time_t değerinin çıkartılması sonucunda elde edilen değerin
|
||
saniye belirtmesi standartlarca garanti edilmemiştir. Bu nedenle eğer iki time_t değeri arasındaki zaman farklı
|
||
saniye cinsindne bulunacaksa difftime fonksiyonu tercih edilmelidir. Fonksiyonun prototipi şöyledir:
|
||
|
||
double difftime(time_t time1, time_t time0);
|
||
|
||
Fonksiyon iki time_t değerinin farkını o sistemdeki epoch birimi dikkate alarak hesaplamaktadır.
|
||
|
||
Ancak gerek Windows sistemlerinde gerekse UNIX/Linux sistemlerinde time fonksiyonu 01/01/1970'den geçen saniye
|
||
sayısını vermektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
strftime isimli standart C fonksiyonu adeta sprintf fonksiyonunun tarih ve zaman için tasarlanmış biçimi gibidir. Bu fonksiyon belli format karakterlerine
|
||
uygun olarak struct tm içerisindeki tarih bilgisini bir yazı biçiminde programcının verdiği adreste oluşturmaktadır. Fonksiyonun prototipi şöyledir:
|
||
|
||
size_t strftime(char *s, size_t max, const char *format, const struct tm *tm);
|
||
|
||
Fonksiyonun birinci parametresi yazının yerleştirileceği dizinin bşlangıç adresini belirtir. İkinci parametre bu dizinin uzunluğunu belirtmektedir.
|
||
Fonksiyon diiyi taşırmamak için bu ikinci parametreyi almaktadır. Üçücncü parametresi format karakterlerinin bulunduduğu yazının adresini belirtir.
|
||
Bu parametre genellikle string biçiminde oluşturulmaktadır. Son parametre ilgili tarih ve zamanı belirten struct tm türünden yapının adresini belirtir.
|
||
Format karakterlerinin listesini ilgili dokümanlardan elde edebebilirsiniz. Fonksiyon başarı durumunda diziye yerleştirilen karakter sayısına geri dönmektedir.
|
||
Bu sayıya null karakter dahil değildir. Fonksiyon eğer ilgili yazı ikinci parametresiyle belirtilen uzunluğu aşarsa 0 ile geri dönmektedir. Bu duurmda dizinin
|
||
içerisinde ne olacağının bir garantisi yoktur.
|
||
|
||
Aşağıdaki örnekte tarih zaman bilgisi ctime formatında ekrana yazdırılmıştır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
|
||
int main(void)
|
||
{
|
||
char buf[4096];
|
||
time_t t;
|
||
struct tm *pt;
|
||
|
||
t = time(NULL);
|
||
pt = localtime(&t);
|
||
|
||
strftime(buf, 4096, "%a %b %d %H:%M:%S %Y", pt);
|
||
|
||
puts(buf);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
62. Ders - 17/01/2023 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi C'de bildirimde kullanılabilen iki "tür niteleyici (type qualifier)" anahtar sözcük vardı: const ve volatile. Bu bölümde bu iki niteleyici
|
||
üzerinde duracağız. Bu iki anahtar sözcüğe "tür niteleyicisi" denilmesinin nedeni bunların tür bilgisini değiştirmesindedendir. Yani örneğin int türü ile
|
||
const int türü uyumlu olsa da farklı türlerdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir nesne const anahtar sözcüğü kullanılarak tanımlanmışsa o nesneye ilkdeğer vermenin dışında bir daha atanamaz. Yani const
|
||
nesneleri tanımlarken ilkdeğer verebiliriz ancak onlara daha sonra değer atayamayız. Örneğin:
|
||
|
||
const int a = 10; /* geçerli */
|
||
...
|
||
a = 20; /* geçersiz! */
|
||
|
||
const bir nesne tipik olarak ilkdeğer verilerek tanımlanır. Nesnenin ilkdeğer verilmeden tanımlanması geçerli olsa da anlamsızdır. Örneğin:
|
||
|
||
{
|
||
const int a; /* geçerli ama anlamsız! */
|
||
...
|
||
}
|
||
|
||
C++'ta const nesnelere ilkdeğer verilmesi zorunlu tutulmuştur. Yukarıda da belirttiğimiz gibi Tür niteleyicileri türün bilgisinin bir paröasını
|
||
oluşturmaktadır. Yani bu haliyle const niteleyicisi dekleratöre ilişkin değil türe ilişkindir. Örneğin:
|
||
|
||
const int a = 10, b = 20, c = 30;
|
||
|
||
Burada a, b ve c nesnelerinin hepsi const nesnelerdir. Bu nesnelerin hepsi const int türündendir. Ancak const int türü int türü tamamen birbirleriyle
|
||
uyumlu türleridr. Yani int türü ile yapılabilen her işlem (atama dışında) const int türüyle de yapılabilmektedir. Her ne kadar int türü ile const int
|
||
türü farklı türler belirtiyorsa da bunları aynı türden kabul edebiliriz. Çünkü birinin kullanıldığı yerde diğeri de kullanılabilmektedir.
|
||
İstisna durumlar izleyen paragraflarda ele alınmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii daha önceden de belirttiğimiz gibi bildirimde tür belirleyicisi ile tür niteleyicileri farklı sıralarda belirtilebilirler. Örneğin:
|
||
|
||
const int a = 10;
|
||
|
||
ile,
|
||
|
||
int const a = 10;
|
||
|
||
tamamen eşdeğerdir.
|
||
|
||
Bir dizi const yapılırsa dizinin tüm elemanları const yapılmış olur. Örneğin:
|
||
|
||
const int a[] = {1, 2, 3, ,4, 5};
|
||
|
||
a[2] = /* geçersiz! */
|
||
|
||
Bir yapı nesnesi const yapılırsa o nesnenin tüm paraçaları const yapılmış olur. Örneğin:
|
||
|
||
struct DATE {
|
||
int day;
|
||
int month;
|
||
int year;
|
||
};
|
||
...
|
||
const struct DATE date = {10, 12, 2007}; /* geçerli */
|
||
|
||
date.month = 10; /* geçersiz! nesnenin tüm parçaları const */
|
||
|
||
Yapı bildiriminde yapının yalnızca bazı elemanları const yapılabilir. Bu durumda nesneyi tanımlarken const anahtar sözcüğü kullanılmasa bile
|
||
o elemanlar yine const olur. Ancak böyle bir kullanım amaç dışı ve çoğu kez anlamsız olduğu için programcılar tarafından kullanılmamaktadır. Örneğin:
|
||
|
||
struct DATE {
|
||
int day;
|
||
const int month;
|
||
int year;
|
||
};
|
||
...
|
||
struct DATE date = {10, 12, 2007}; /* geçerli */
|
||
|
||
date.day = 21; /* geçerli */
|
||
date.year = 2009; /* geçerli */
|
||
date.month = 7; /* geçersiz! */
|
||
|
||
C standartlarına göre aynı tür niteleyicisi bildirimde birden fazla kez belirtilse bile bu durum geçerlidir. Yalnızca bir kez belirtilmiş gibi
|
||
işlem görür. Örneğin:
|
||
|
||
const int const a = 10; /* geçerli ama anlamsız */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de const değişkenler sabit ifadeleri gereken yerlerde kullanılamazlar. Örneğin:
|
||
|
||
const int g_size = 10;
|
||
int g_a[g_size]; /* geçersiz! */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
const nesnelerin kullanılmasının üç nedeni vardır:
|
||
|
||
1) const nesneler okunabilirliği artırmaktadır. Bir nesnenin const olduğunu geren kişi onun içerisindeki değerin bir daha değiştirilmeyeceğini anlar
|
||
ve kodu daha iyi anlamlandırır. Örneğin:
|
||
|
||
const int number_of_students = 121;
|
||
|
||
Burada öğrenci sayısının program içerisinde değiştirilmeyeceği anlaşılmaktadır.
|
||
|
||
2) const nesneler kullanıldığında derleyiciler duruma göre daha iyi bir optimizasyon yapabilmektedir. Bunun nedeni derleyicilerin const nesnelerin
|
||
değiştirilmeyeceğini bilmesinden kaynaklanmaktadır. Örneğin derleyici const bir nesneyi bir yazmaca çemişse bir süre sonra onun değerinin dolaylı bir
|
||
biçimde bile değiştirilmeyeceğini bildiği için nesneyi yeniden yazmaca yüklemez.
|
||
|
||
3) const nesneler yanlışlıkla programcı tarafından değiştirilme durumlarında derleme zamanında bu yanlışlığın ortaya çıkartılmasını da sağlamaktadır.
|
||
Yani aslında dğeiştirilemeyecek bir nesneyi programcı yanlışlıkla değiştirirse hata derleme aşamasında ortaya çıkacaktır.
|
||
|
||
Ancak const tür niteleyicisi aslında daha çok göstericilerle birlikte kullanılmaktadır.İzleyen paragraflarda const
|
||
niteleyicisinin göstericilerle kullanımı ele alınmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir gösterici belirtildiğinde const olma durumu üç biçimde kendini göstermektedir. Örneğin:
|
||
|
||
int *pi;
|
||
|
||
Burada pi göstericisi bir nesnedir, int türden bir adres bilgisi tutmaktadır. Öte yandan pi göstericisinin gösterdiği yere *pi ya da pi[n] ifadeleriyle
|
||
de erişebilmekteyiz. Yani pi göstericisinin gösterdiği yerler de birer nesnedir. İşte pi'nin kendisinin mi yoksa gösterdiği yerin mi yoksa hem kendisinin hem de
|
||
gösterdiği yerin mi const olduğu önemlidir. const göstericiler üçe ayrılmaktadır:
|
||
|
||
1) Kendisi değil gösterdiği yer const olan const göstericiler
|
||
2) Gösterdiği yer değil kendisi const olan const göstericiler
|
||
3) Hem kendisi hem de gösterdiği yer const olan const göstericiler
|
||
|
||
En çok kullanılan const göstericiler "kendisi değil gösterdiği yer const olan const göstericilerdir". Biz önce bu üçü türü de tanıtıp daha sonra "kendisi
|
||
değil gösterdiği yer const olan const göstericiler" üzerinde biraz daha detaylı bir biçimde duracağız. Halk arasında "const gösterici" denildiğinde
|
||
default olarak "kendisi değil gösterdiği yer const olan const göstericler" anlaşılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
const göstericinin nasıl bir const gösterici olduğu const anahtar sözcüğünün nereye getirildiği ile ilgilidir.
|
||
|
||
1) Eğer const anahtar sözcüğü dekleratörle değil türle ilgili kullanılmışsa bu "kendisi değil gösterdiği yer const olan" const gösterici anlamına gelmektedir.
|
||
Yani burada const anahtar sözcüğü *p ifadesinin soluna getirilmiş durumdadır. Örneğin:
|
||
|
||
const int *p;
|
||
|
||
Burada p nesnesi const değildir. Biz p'nin içerisine istediğimiz zaman bir adres yerleştirebiliriz. Burada const olan p'nin gösterdiği yer, yani *p nesnesidir.
|
||
Örneğin:
|
||
|
||
int a[100];
|
||
const int *p;
|
||
|
||
p = &a[0]; /* geçerli p const değil */
|
||
p = &a[1] /* geçerli, p const değil */
|
||
|
||
*p = 100; /* geçersiz! *p const bir nesne */
|
||
p[20] = 100; /* geçersiz! derleme aşamasından geçilemez! */
|
||
|
||
Burada dikkat edilmesi gereken bir nokta şudur. Örneğimizde p değil *p const durumdadır. Ancak bir göstericinin gösterdiği yerin const olması demek
|
||
* operatörü ile tam gösterdiği yerin const olması demek değildir. Bu gösterici ile erişilen her yer const durumdadır. Yani biz p göstericisi ile
|
||
nereye erişirsek erişelim orası const durumdadır. Örneğin:
|
||
|
||
const int *p;
|
||
|
||
Bu tanımlamada p'nin kendisi değil gösterdiği yer const durumdadır. Yani biz p'ye herhangi bir adres atayabiliriz. Ancak p'ye hangi adresi
|
||
atamış olursak olalım onun gösterdiği yeri değiştiremeyiz. Burada mademki p'nin kendisi const değildir. O zaman p'ye ilkdeğer vermeye gerek yoktur.
|
||
|
||
Gösterdiği yer const olan const göstericinin türü belirtilirken bu const olma durumu da belirtilir. Örneğin:
|
||
|
||
const char *s;
|
||
|
||
Burada s "const char *" türündendir. "char *" türünden değildir. "const char *" türü demek "gösterdiği yer const olan char türden bir adres türü" demektir.
|
||
|
||
Kendisi değil gösteridiği yer const olan const göstericilerde const anahtar sözcüğünün başa gelmesi zorunlu değildir. Önemli olan *'ın soluna gelmesidir.
|
||
Örneğin:
|
||
|
||
const int *p;
|
||
|
||
ile,
|
||
|
||
int const *p;
|
||
|
||
tamamen eşdeğeridir.
|
||
|
||
Tabii gösterdiği yer const olan const gösterici bildirilirken birden fazla dekleratör bulundurulabilir. Örneğin:
|
||
|
||
const int *p, a = 10;
|
||
|
||
Burada p göstericisinin kendisi değil gösterdiği const durumdadır. Ancak a const bir nesnedir.
|
||
|
||
2) Eğer bildirimde const anahtar sözcüğü *'ın soluna değil sağına getirilirse, yani const anahtar sözcüğü tür ile değil gösterici dekleratörü ile
|
||
ilişkilendirilirse bu durumda "gösterdiği yer değil kendisi const olan" const bir gösterici oluşturulmuş olur. Örneğin:
|
||
|
||
int a;
|
||
int * const p = &a;
|
||
|
||
Burada p const bir nesnedir. Dolayısıyla biz ilkdeğer verdikten sonra p'ye başka bir adres atayamayız. Anck pi'nin gösterdiği yeri değiştirebiliriz.
|
||
Örneğin:
|
||
|
||
int a, b;
|
||
int * const p = &a;
|
||
|
||
*p = 10; /* geçerli, *p const değil! */
|
||
printf("%d\n", a); /* 10 */
|
||
|
||
p = &b; /* geçersiz! p const
|
||
|
||
Şimdi bildirimde başka dekleratörler de bulunduralım:
|
||
|
||
int * const p = &a, x, y;
|
||
|
||
Burada p const bir nesnedir ancak x ve y const nesneler değildir. Gösterdiği yer değil kendisi const olan const göstericilerde tür bilgisi belirtilirken
|
||
yine const anahtar sözcüğü belirtilir ancak *'dan sonraya getirilir. Örneğin:
|
||
|
||
int * const p = &a;
|
||
|
||
Burada p "int * const" türündendir.
|
||
|
||
3) Eğer const anahtar sözcüğü hem *'ın soluna hem de sağına getirilirse bu durumda "hem kendisi hem de gösterdiği yer const olan" const gösterici
|
||
oluşturulmuş olur. Örneğin:
|
||
|
||
int a;
|
||
const int * const p = &a;
|
||
|
||
Burada artık biz p'ye de onun gösterdiği yere de atama yapamayız. Şimdi bildirimde birden fazla dekleratör bulunduralım:
|
||
|
||
int a;
|
||
const int * const p = &a, b = 20;
|
||
|
||
Burada p "hem kendisi hem de gösterdiği yer const olan" const bir göstericidir. b de const bir nesnedir. Böyle göstericilerde tür bilgisi belirtilirken
|
||
her iki const anahtar sçzcüğü de belirtilir.
|
||
|
||
int a;
|
||
const int * const p = &a;
|
||
|
||
Burada p "const int * const" türündedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi const bir nesnenin adresini bir göstericiye yerleştirip bu gösterici yoluyla const nesneyi değiştirebilir miyiz? Örneğin:
|
||
|
||
const int a = 10;
|
||
int *pi;
|
||
|
||
pi = &a; /* buraya dikkat! */
|
||
*pi = 20;
|
||
|
||
Burada aslında çaktırmadan a değiştirilmeye çalışılmıştır. Dolayısıyla programcı derleyiciye verdiği sözü kuralların arkasından dolaşarak ihlal etmektedir.
|
||
Fakat örneğin:
|
||
|
||
int a = 10;
|
||
const int *pi;
|
||
|
||
pi = &a;
|
||
|
||
Burada bir kötüye kullanım yoktur. Çüğkü a zaten const değildir. İşte birinci örnekteki kötüye kullanımı ortadan kaldırmak için C'de otomatik dönüştürmeye
|
||
yönelik şöyle bir kural bulunmaktadır: "T bir tür belirtmek üzere const T * türünden T * türüne otomatik dönüştürme yoktur. Böylece biz const bir nesnenin
|
||
adresini gösterdiği yer const olmayan bir göstericiye zaten atayamayız. Örneğin:
|
||
|
||
const int a = 10;
|
||
int *pi;
|
||
|
||
pi = &a; /* geçersiz! */
|
||
*pi = 20;
|
||
|
||
Tabii "const T *" türünden "const T *" türüne otomatik dönüştürme vardır. Zaten bunlar aynı türlerdir. Yani biz const bir nesnenin adresini
|
||
gösterdiği yer const olan bir göstericiye atayabiliriz. Örneğin:
|
||
|
||
const int a = 10;
|
||
const int *pi;
|
||
|
||
pi = &a;
|
||
|
||
Burada atamanın iki tarafının türü de "const int *" biçimindedir. Bu atamada bir kötüye kullanım yoktur. Çünkü zaten pi ile artık onun gösterdiği
|
||
yer değiştirilemeyecektir. Eğer biz pi'yi kullanarak onun gösterdiği yeri değiştirmeye çalışsak derleme zamanında error ile karşılaşırız.
|
||
|
||
Tabii biz const bir nesnenin adresini kendisi const olan bir gösteriye yine atayamayız. Yani "const T *" türünden "T * const" türüne otomatik
|
||
dönüştürme yoktur. Burada önemli olan göstericinin gösterdiği yerin const olmasıdır:
|
||
|
||
const int a = 10;
|
||
int * const pi = &a; /* geçersiz! */
|
||
|
||
Burada pi'nin gösterdiği yer const olmadığı için bu durum kötüye kullanıma açıktır.
|
||
|
||
Tabii const olmayan bir nesnenin adresinin, gösterdiği yer const olan bir göstericiye atanmasında bir kötüye kullanım yoktur. Dolayısıla "T *" türünden
|
||
"const T *" türüne otomatik dönüştürme vardır. Örneğin:
|
||
|
||
int a = 10;
|
||
const int *pi;
|
||
|
||
pi = &a; /* geçerli, kötüye kullanım yok */
|
||
|
||
Tabii biz burada pi göstericisini kullanarak yine onun gösterdiği yeri değiştiremeyiz.
|
||
|
||
Burada bir noktaya dikkatinizi çekmek istiyoruz. const T * türünden bir göstericiye hem T * türünden hem de const T *
|
||
türünden adresler atanabilmektedir. Örneğin:
|
||
|
||
int a = 10;
|
||
const int b = 20;
|
||
const int *pi;
|
||
|
||
pi = &a; /* geçerli, "const T * = T *" */
|
||
pi = &b; /* geçerli, "const T * = const T *" */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
const bir dizinin ismi ifade içerisinde kullanıdığında gösterdiği yer const olan bir adres belirtmektedir. Örneğin:
|
||
|
||
const int a[] = {1, 2, 3, 4, 5};
|
||
|
||
Burada a ifadesi "const int *" türündedir. Dolayısıyla aşağıdaki işlemler geçersizdir:
|
||
|
||
*a = 10; /* geçersiz! */
|
||
a[2] = 20; /* geçersiz */
|
||
|
||
const T * türünden T * türüne otomatik dönüştürme olmadığını anımsayınız:
|
||
|
||
int *pi;
|
||
|
||
pi = a; /* geçersiz! */
|
||
|
||
Burada const int * türünden adres int * türünden bir göstericiye atanmak istenmiştir. Tabii burada a adresini biz
|
||
gösterdiği yer const olan bir göstericiye atayabiliriz:
|
||
|
||
const int a[] = {1, 2, 3, 4, 5};
|
||
const int *pi;
|
||
|
||
pi = a; /* geçerli */
|
||
|
||
Fakat artık bu gösterici yoluyla da dizi elemanlarını değiştiremeyiz. ""
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Her ne kadar const T * türünden T * türüne otomatik dönüştürme yoksa da biz tür dönüştürme operatörü ile bu
|
||
dönüştürmeyi yapabiliriz. Örneğin:
|
||
|
||
const int a = 10;
|
||
int *pi;
|
||
|
||
pi = &a; /* geçersiz! */
|
||
|
||
Bu atama geçersizdir. Çünkü burada const T * türünden T * türüne otomatik dönüştürme yapılmak istenmiştir. Ancak
|
||
eğer bu dönüştürmeyi yapmak istiyorsak tür dönüştürme operatörünü kullanarak yapabiliriz:
|
||
|
||
pi = (int *)&a; /* geçerli */
|
||
|
||
Burada artık const int * türü tür dönüştürme operatörüyle int * türüne dönüştürülmüştür. Bu dönüştürme tamamen geçerlidir.
|
||
Bu tür dönüştürmelere "const'luğu atan dönüştürmeler (const away cast)" de denilmektedir. Tabii biz T * türünden
|
||
tür dönüştürme operatörüyle const T * türüne de dönüştürme uygulayabiliriz. Ancak bu gereksizdir çünkü zaten T *
|
||
türünden const T * türüne otomatik dönüştürme vardır. Örneğin:
|
||
|
||
int a = 10;
|
||
const int *pi;
|
||
|
||
pi = (const int *)&a; /* geçerli */
|
||
|
||
Burada zaten böylesi bir dönüştürmeye gerek yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C standartlarına göre const bir nesnenin herhangi bir yolla (tipik olarak ir gösterici yoluyla) değerinin değiştirilmesi
|
||
tanımsız davranışa (undefined behavior) yol açmaktadır. Örneğin:
|
||
|
||
const int a = 10;
|
||
int *pi;
|
||
|
||
pi = (int *)&a; /* geçerli */
|
||
*pi = 20; /* geçerli, ama tanımsız davranış */
|
||
|
||
Burada C'ce geçersiz hiçbir nokta yoktur. Ancak sonuçta const bir nesne güncellenmiştir. İşte bu durum tanımsız
|
||
davranışa yol açmaktadır. Buradaki tanımsız davranış ağaıdaki deyimde ortaya çıkmaktadır:
|
||
|
||
*pi = 20; /* geçerli, ama tanımsız davranış */
|
||
|
||
Gerçekten de pek çok derleyici (Micrsoft C derleyicisi, gcc ve clang derleyicileri) örneğin global const nesneleri
|
||
const bir bellek alanında tutmaktadır. Bu bellek alanına yazma yapılmak istendeiğinde program çökebilmektedir.
|
||
|
||
Tabii aklınıza "madem const bir nesnenin gösterici yoluyla değiştirilmesi tanımsız davranışa yol açıyor, bu durumda
|
||
const bir nesnenin adresinin tür döüştürmesi operatöryle const olmayan bir göstericiye atanmasının ne anlamı var"
|
||
sorusu gelebilir. Bazen aslında const olmayan bir nesnenin adresi gösterdiği yer const olan bir gösteriye atanmış
|
||
olabilir. Sonra bunu biz const olmayan bir göstericiye atamak isteyebiliriz. Nasıl olsa nesnenin orijinali const
|
||
değildir ve onun güncellenmesi bir soruna yol açmayacaktır. Yani gösterdiği yer const olan bir göstericinin gösterdiği
|
||
yerde const bir nesne olmak zorunda değildir. Örneğin:
|
||
|
||
int a = 10;
|
||
const int *pci;
|
||
int *pi;
|
||
|
||
pci = &a;
|
||
|
||
/* .... */
|
||
|
||
pi = (int *)pci; /* geçerli */
|
||
|
||
*pi = 20; /* geçerli */
|
||
|
||
printf("%d\n", a);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
63. Ders - 24/01/2023 Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Daha önceden de belirttiğimiz gibi "const gösterici" denildiğinde default olarak "gösterdiği yer const olan const göstericiler" anlaşılmaktadır.
|
||
const göstericilerin en fazla kullanıldığı yer fonksiyobn parametreleridir. const göstericilerin fonksiyonların parametre değişkenlerinde kullanılması
|
||
önemli bir durum oluşturmaktadır.
|
||
|
||
Bir fonksiyonun parametre değişkeninin const bir gösterici (gösterdiği yer const olan bir gösterici) olması "fonksiyonun bizden bir nesnenin
|
||
ya da dizinin adresini alacağı, ancak adresini aldığı nesnede ya da dizide dir değişiklik yapmayacağı" anlamına gelmektedir. Örneğin:
|
||
|
||
void foo(const int *pi);
|
||
|
||
Burada foo fonksiyonu bizden int türden bir nesnenin adresini almaktadır. Ancak foo fonksiyonu bu nesne üzerinde değişiklik yapmayacaktır. Çünkü
|
||
gösterdiği yer const olan gösterilerle o göstericilerin gösterdiği yerlerde değişiklik yapılamaz. Örneğin:
|
||
|
||
int a = 10;
|
||
|
||
foo(&a);
|
||
|
||
Burada biz fonksiyonun ne yaptığını bilmesek bile onun adresini verdiğimiz nesne üzerinde değişiklik yapmayacağını onu yalnızca kullanacağını anlarız.
|
||
Fonksiyon çıkışında a'da bir değişiklik olmayacaktır. Örneğin:
|
||
|
||
int getmax(const int *pi, size_t size);
|
||
|
||
Burada getmax fonksiyonu int türden bir dizinin başlangıç adresini ve uzunluğunu almaktadır. Ancak fonksiyon dizi üzerinde değişiklik yapmayacaktır.
|
||
|
||
Bir fonksiyon yazarken eğer fonksiyonun parametresi bir gösterici ise ve programcı adresiyle aldığı nesne ya da dizi üzerinde değişiklik yapmayacaksa
|
||
kesinlikle onu const bir gösterici yapmalıdır. Nesne üzerinde üzerinde değişiklik yapmadığı halde göstericinin const yapılmaması kötü bir tekniktir.
|
||
Örneğin:
|
||
|
||
size_t mystrlen(char *str); /* kötü teknik, göstericinin const olması gerekirdi */
|
||
|
||
Burada mystrlen bir yazının karakter uzunluğuna geri dönüyor olsun. Bu fonksiyonun parametresiyle aldığı yazıda bir değişiklik yapma iddiası
|
||
yoktur. O halde onun parametresinin const bir gösterici olması gerekirdi. Bu uygulama kötü bir tekniktir. Fonksiyonun şöyle tasarlanması gerekirdi:
|
||
|
||
size_t mystrlen(const char *str); /* doğru teknik */
|
||
|
||
const anahtar sözcüğünün fonksiyon parametresi olan göstericilerde kararlı bir biçimde kullanılması artık "const olmayan gösterici parametrelerinin"
|
||
nesne ya da dizide üzerinde kesinlikle değişiklik yapacağı" anlamına gelir. Çünkü eğer fonksiyon adresiyle aldığı nesnede ya da dizide değişiklik
|
||
yapmayacak olsaydı onun parametresini const gösterici yapardı. Örneğin:
|
||
|
||
void foo(int *pi, size_t size);
|
||
|
||
Burada fonksiyon int türden bir dizinin başlangıç adresini ve uzunluğunu bizden parametre olarak almaktadır. Eğer fonksiyon bu dizide değişiklik
|
||
yapmayacak olsaydı programcı fonksiyonun parametresini const bir gösterici yapardı. Demek ki fonksiyon parametresiyle aldığı dizide değişiklik yapacaktır.
|
||
Görüldüğü gibi artık "const olmayan gösterici parametrelerinin adresini aldığı nesnede değişiklik yapacağı" anlamı çıkmaktadır.
|
||
|
||
Pekiyi fonksiyon parametrelerindeki göstericilerde const niteleyicisinin uygun kullanılmamasının nasıl bir dezavantajı olabilir? Örneğin:
|
||
|
||
void myputs(char *str);
|
||
|
||
Bu fonksiyon bizden adresini aldığı char bir dizinin karakterlerini yan yana null karakter görene kadar ekrana yazdırıyor olsun. Bu durumda fonksiyonun gösterici
|
||
parametresinin const olması gerekirdi. Ancak programcı kötü bir teknik uygulayarak parametreyi const gösterici yapmamıştır. İşte bunun iki önemli
|
||
dezavantajı vardır:
|
||
|
||
1) Fonksiyon aslında pek ala const bir dizi ile kullanılabilecekken artık kullanılamaz hale gelmiştir. Örneğin:
|
||
|
||
const char s[] = "ankara";
|
||
|
||
myputs(s); /* geçersiz */
|
||
|
||
Bu çağrı geçersizdir. Çünkü const bir dizinin adresi const olmayan bir göstericiye atanamaz. Halbuki fonksiyon aşağıdaki gibi tanımlanabilirdi:
|
||
|
||
void myputs(const char *str);
|
||
|
||
Artık biz bu fonksiyonu const bir diziyle de const olmayan bir diziyle de çağırabilirdik:
|
||
|
||
const char s[] = "ankara";
|
||
|
||
myputs(s); /* geçerli */
|
||
|
||
2) Böyle bir tasarım kodu ineleyen kişileri yanıltmaktadır. Örneğin:
|
||
|
||
void myputs(char *str);
|
||
|
||
Kodu inceleyen kişiler fonksiyonun parametresinin const olmayan gösterici olduüunu gördüklerinde onun adersi ile verilen dizide değişiklik yapacağını
|
||
sanırlar ve fonksiyonu anlamlandırmada zorluk çekerler.
|
||
|
||
O halde doğru teknik şudur: "Programcı parametresiyle aldığı adresteki nesneyi değiştirmeyecekse kesinlikle parametre değişkenini const gösterici
|
||
yapmalıdır, eğer parametresiyle aldığı adresteki nesneyi değiştirecekse parametre değişkenini const yapmamalıdır (zaten yapamaz da)".
|
||
|
||
Şimdi parametre değişkeni olan göstericideki const olma durumu üzerinde çeşitli alıştırmalar yapalım.
|
||
|
||
- strcpy fonksiyonunda birinci parametreye belirtilen hedef adrese ilişkin gösterici const olmamalıdır. Ancak ikinci parametreyle belirtilen
|
||
kaynak adrese ilişkin gösterici const olmalıdır. Gerçekten de fonksiyonun orijinal prototipi şöyledir:
|
||
|
||
char *strcpy(char *dest, const char *source);
|
||
|
||
- strchr fonksiyonu bizim adresini verdiğimiz yazıda karakter aramaktadır. Dolayısıyla fonksiyonun adresini verdiğimiz yazıda değişiklik yapma
|
||
gibi bir niyeti yoktur. O halde fonksiyonunun gösterici parametresinin const olması gerekir:
|
||
|
||
char *strchr(const char *str, int ch);
|
||
|
||
- int bir diziyi sıraya dizen sort isimli bir fonksiyon yazılacak olsa dizinin başlangıç adresini alan göstericinin const olmaması gerekir. Örneğin:
|
||
|
||
void sort(int *pi, size_t size);
|
||
|
||
Çünkü fonksiyon dizide değişiklik yapmaktadır.
|
||
|
||
- gets fonksiyonu klavyeden (stdin dosyasından) alınan karakterleri char türden bir diziye yerleştirip sonuna null karakteri de eklemektedir.
|
||
Bu durumda gets fonksiyonunun parametresinin const olmayan bir gösterici olması gerekir:
|
||
|
||
char *gets(char *str);
|
||
|
||
- Bir yazıyı ters çeviren strrev isimli bir fonksiyon bulunuyor olsun. Fonksiyonun parametresi const bir gösterici olamaz:
|
||
|
||
char *strrev(char *str);
|
||
|
||
- double bir dizinin ortalaması ile geri dönen mean isimli bir fonksiyon bulunuyor olsun. Fonksiyonun parametresi const gösterici olmalıdır.
|
||
Örneğin:
|
||
|
||
double mean(const double *pd, size_t size);
|
||
|
||
Yapılardaki göstericilerin const olup olmaması önemli bir okunabilirlik sağlamaktadır. Eğer fonksiyonun yapı gösterici parametresi const ise
|
||
fonksiyon bizim verdiğimiz yapının içerisindeki kullanır ancak orada değişiklik yapmaz. Eğer fonksiyonun yapı gösterici parametresi const değilse
|
||
bu durumda fonksiyon bizden aldığı yapı nesnesinin içini dolduracaktır. Örneğin sistem tarihini alan ve değiştiren iki fonksiyon olsun.
|
||
Bu fonksiyonlar struct DATE isimli bir yapıyı kullanıyor olsunlar. Fonksiyonların prototipleri şöyle olmalıdır:
|
||
|
||
void getdate(struct DATE *pdate);
|
||
void setdate(const DATE *pdate);
|
||
|
||
Burada getdate fonksiyonuna biz bir strcut DATE nesnesi veririz. getdate fonksiyonu da bilgisayarın tarihine bakarak tarihi bizim verdiğimiz
|
||
yapı nesnesine yerleştirir. Görüldüğü gibi fonksiyon bizim adresini verdiğimiz yapı nesnesinde değişiklik yapmaktadır. Halbuki setdate fonksiyonu
|
||
adresiyle verdiğimiz yapı nesnesinin içindekileri kullanarak bilgisayarın tarihini değiştirmektedir. O halşde getdate fonksiyonun parametresi const gösterici
|
||
olmamalıdır, setdate fonksiyonun parametresi ise const gösterici olmalıdır.
|
||
|
||
Şimdi bir veritabanından isim ile arama yapan ve eğer kaydı bulursa onun bütün bilgilerini bizim verdiğimiz struct PERSON türünden bir
|
||
nesnenin içerisine yerleştiren findperson isimli fonksiyonun parametrelerini const'luk durumuna göre inceleyelim. Fopnksiyon aşağıdaki gibi
|
||
kullanılacaktır:
|
||
|
||
struct PERSON per;
|
||
...
|
||
|
||
if (!findperson("Kaan Aslan", &per))
|
||
fprintf(stderr, "cannot find person!..\n";)
|
||
else {
|
||
printf("person found...\n);
|
||
...
|
||
}
|
||
|
||
Burada fonksiyonun bizden aldığı isim üzerinde bir değişiklik yapma niyeti yoktur. Ancak fonksiyon bizden aldığı struct PERSON nesnesinin içini
|
||
doldurmak istemektedir. O halde fonksiyonun prototipi aşağıdaki gibi olabilir:
|
||
|
||
bool findperson(const char *name, struct PERSON *per);
|
||
|
||
get_sys_info isimli fonksiyon sistem hakkında çeşitli bilgileri (işlemci sayısı, RAM miktarı vs.) SYSINFO isimli bir yapıya yerleştiriyor olsun.
|
||
Bu durumda fonksiyonun parametresi const gösterici olamaz:
|
||
|
||
typedef struct tagSYSINFO {
|
||
....
|
||
} SYSINFO;
|
||
|
||
void get_sys_info(SYSINFO *si);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
const niteleyicisi typedef edilmiş bir adres ile kullanıldığında her zaman nesnenin kendisini nitelemktedir. Örneğin:
|
||
|
||
typedef struct tagPERSON {
|
||
...
|
||
} PERSON, *PPER;
|
||
|
||
const PPER per; /* özel durum, burada per gösterdiği yer değil, kendisi const olan const gösterici */
|
||
|
||
Burada per göstericinin gösterdiği yer const değildir. per göstericisinin kendisi const durumdadır. Bu özel ve istisna bir durumdur. Yani buradaki
|
||
bildirimin eşdeğeri şöyledir:
|
||
|
||
struct tagPERSON * const per;
|
||
|
||
Biz burada göstericinin gösteridği yeri const yapacaksak gösterici typedef ismini değil yapı typedef ismini kullanmalıyız. Örneğin:
|
||
|
||
const PERSON *per;
|
||
|
||
Burada artık per gösteridiği yer const olan const bir göstericidir. Başka bir deyişle const niteleyicisi başa getirilmişse her zaman dekleratörü const
|
||
yapmaktadır. Ancak programcılar const göstericiler için de typedef yapmayı tercih edebilmektedir. Örneğin:
|
||
|
||
typedef struct tagPERSON {
|
||
...
|
||
} PERSON, *PPERSON;
|
||
|
||
typedef const struct PERSON *PCPERSON;
|
||
|
||
Burada artık PCPERSON ismi "const struct PERSON *" anlamına gelmektedir. Örneğin:
|
||
|
||
void disp_person(PCPERSON per);
|
||
|
||
Burada per artık göstweridği yer const olan const bir göstericidir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
typedef int *PI;
|
||
|
||
int main()
|
||
{
|
||
int a = 10, b = 20;
|
||
const PI pi = &a;
|
||
|
||
pi = &b; /* geçersiz! pi'nin kendisi const */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
const bir adres için typedef işlemi uygulamak yaygın bir durumdur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
typedef const int *PCI;
|
||
|
||
int main()
|
||
{
|
||
int a = 10, b = 20;
|
||
PCI pi = &a;
|
||
|
||
pi = &b; /* geçerli, pi'nin gösterdiği yer const */
|
||
printf("%d\n", *pi);
|
||
*pi = 20; /* geçersiz! pi'nin gösterdiği yer const! */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Gösterici olmayan parametre değişkenlerinin const olmasının okunabilirlik bakımından hiçbir anlamı yoktur. Örneğin:
|
||
|
||
void foo(const int a);
|
||
|
||
Burada a'nın const olup olmaması fonksiyonu yazanı ilgilendiren kullanını ilgilendirmeyen bir durumdur. Dolayısıyla dış dünyaya faydalı hiçbir bilgi
|
||
vermemektedir. Parametre değişkenlerinin gösterici olmadıktan sonra const biçimde bildirilmesi iyi bir teknik değildir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
volatile tür niteleyicisi de kullanım bakımından const tür niteleyeicisine oldukça benzemektedir. volatile niteleyicisi yine türe ilişkindr. Örneğin:
|
||
|
||
volatile int x;
|
||
|
||
Burada x "volatile int" türündendir.
|
||
|
||
volatile const niteleyicisine göre oldukça seyrek kullanılan bir niteleyicidir. Bu niteleyicinin anlaşılması bazı başka temaları gerektirdiği için
|
||
biraz zor olabilmektedir.
|
||
|
||
volatile anahtar sözcüğünün işlevini anlayabilmek için derleyicilerin optimizasyonları hakkında bazı bilgilere sahip olunması gerekir.
|
||
Derleyiciler kodun anlamını değiştirmeden kodu daha hızlı çalışabilir ya da daha az yer kaplar hale getirebilmektedir. Optimizasyonun temel prensibi
|
||
"programcının niyet ettiği şeylerin asla bozulmamasıdır." Yani kodun optimize edilmiş haliyle edilmemiş hali tamamen aynı etkilere yol açmalıdır.
|
||
Programcı kodun optimize edilmiş olduğunu bilmek zorunda değildir. Örneğin:
|
||
|
||
x = a + 10;
|
||
y = a + 20;
|
||
|
||
Burda derleyici a değişkenini nellekten yazmaca çektikten sonra yeniden yazmaca çekmek istemeyebilir. Intel mimarisinde derleyici şöyle bir kod üretebilir:
|
||
|
||
mov reg1, a
|
||
mov reg2, 10
|
||
add reg2, reg1
|
||
mov x, reg2
|
||
add reg1, 20
|
||
mov y, reg1
|
||
|
||
Burada derleyici a'yı reg1 yazmacına çekmiştir. Sonra da a zaten reg1 yazmacında olduğu için onu yeniden yazmaca çekmeden reg1 yazmacındaki değeri
|
||
yeniden kullanmıştır. Şimdi başka bir akışın birinci deyim bitiğinde a'yı değiştirdiğini düşünelim. Yani a dışarıdaki bir el tarafından değiştirilmiş
|
||
olsun:
|
||
|
||
x = a + 10;
|
||
---> burada a'nın bilinmeyen bir biçimde programdan bağımsız olarak değiştirildiğini düşünelim
|
||
y = a + 20
|
||
|
||
Bu değişikliğin üretilen makine komutunda yapıldığı yer aşağıda belirtilmiştir:
|
||
|
||
mov reg1, a
|
||
mov reg2, 10
|
||
add reg2, reg1
|
||
mov x, reg2
|
||
----> a burada değiştirilmiş
|
||
add reg1, 20
|
||
mov y, reg1
|
||
|
||
İşte burada dışarıdan yapılan değişiklik derleyicinin ürettiği kod tarafından anlaşılamamktadır. Çünkü derleyici a'nın değerini daha önce reg1 yazmacına
|
||
yerleştirmiştir. Dılşarıdan başka bir elin a'yı değiştirebileceğini dikkat elmamıştır. Optimizasyon sırasında dışarıdan başka bir elin bu değişikliği
|
||
yapması derleyiciler tarafından dikkate alınmaz. Bizim burada isteğimiz dışarıdan a'da yapılan değişikliğin koda yansımasını sağlamak olsun. Eğer derleyici
|
||
a'nın reg1 yazmacındaki halini kullanmasaydı her a kullanıldığında onu bellekten yeniden yükleseydi program bu değişikliği görebilirdi. Örneğin:
|
||
|
||
mov reg1, a
|
||
mov reg2, 10
|
||
add reg2, reg1
|
||
mov x, reg2
|
||
----> a burada değiştirilmiş
|
||
mov reg1, a
|
||
add reg1, 20
|
||
mov y, reg1
|
||
|
||
Burada artık ilgili noktada a değiştirildiğinde derleyicinin ürettiği kod a'yı yeniden bellekten yazmaca yüklediğinden program içerisinde değişiklik
|
||
dikkate alınacaktır.
|
||
|
||
Burada anlatılmak istenen şey şudur: Derleyiciler nesneleri yazmaçlara çekip nasıl olsa onların değerleri yazmaçta diye o yazmaçtaki değeri
|
||
kullanabilmektedir. Bu durum programın daha hızlı çalışmasına yol açmaktadır. Ancak bilinmeyen bir el dışarıdan ilgili nesneyi değiştirdiğinde
|
||
derleyicinin üretmiş olduğu kod bu değişikliği dikkate alamamktadır. Bu değişikliğin kodda hemen dikkate alınabilmesi için derleyicinin ilgiyi nesneyi
|
||
yazmaçlarda uzun süre tutmaması ve nesne ne zaman kullanılırsa onu yeniden bellekten alması gerekir. Daha dramatik bir örnek şöyle verilebilir:
|
||
|
||
|
||
int flag;
|
||
...
|
||
flag = 0;
|
||
|
||
while (flag == 0) {
|
||
...
|
||
}
|
||
|
||
derleyici flag değişkenini bir yazmaca çekerek döngü içerisinde hep o yazmaçtaki değeri kullanıyor olabilir. Bu durumda dışarıdaki bir el
|
||
flag nesnesini 1'e çekse bile derleyicinin üretmiş olduğu bu kod döngüden çıkmayacaktır. Çünkü derleyici flag nesnesinin içeriği zaten yazmaçta
|
||
var diye onun yazmaçtaki halini kullanıyor olabilir. Oysa biz flag nesnesi dışarıdan başka bir el tarafından değiştirildiğinde döngüden çıkmak isteyebiliriz.
|
||
|
||
Burada kişilerin aklına iki soru gelmektedir.
|
||
|
||
1) Dışarıdaki bir el benim programındaki bir nesnenin değerini nasıl değiştirebilir? İşte bu başka el bazen bir thread olabileceği gibi bazen de bir
|
||
"kesme kodu (interrupt handler)" olabilmektedir.
|
||
|
||
2) Derleyicinin bu nesnenin dışarıdan değiştirilebileceğini dikkate alıp bu tür optimizasyonları yapmaması gerekmez mi? Derleyiciler bir nesnenin başka
|
||
bir el tarafından dışarıdan değiştirileceğini varsaymamaktadır. Bu çok özel bir durumdur. Eğer derleyiciler böyle bir varsayımda bulunup ona göre kod
|
||
üretselerdi üretilen kodun kalitesi hız bakımından düşük olurdu. Bu özel durumun programcı tarafından dikkate alınması daha uygun bir tasarımdır.
|
||
|
||
Şimdi volatile anahtar sözcüğünün neyi sağladığını açıklayalım. Biz bir nesneyi volatile anahtar sözcüğü ile tanımlarsak derleyiciye şunları
|
||
söylemiş olmaktayır: "Derleyici, bu nesnenin dışarıdan başka bir el tarafından değiştirilme gibi bir durumu var. Dolayısıyla ben bu nesne dışarıdan
|
||
değiştirildiğinde değişikliği hemen anlamak istiyorum. Ben ne zaman bu nesneyi kullansam sen onu yazmaca çekmiş olsan bile onun yazmaçtaki değerini
|
||
kullanma. Taze taze onu yeniden bellekten çek ve kullan". Örneğin:
|
||
|
||
volatile int a;
|
||
...
|
||
x = a + 10;
|
||
---> burada dışarıdan a değiştirilmiş olsun
|
||
y = a + 20;
|
||
|
||
Burada artık derleyici a volatile olduğu için a her kullanıldığında onu bellekten yeniden taze taze yükler. Böylece ok ile belirtilen noktada
|
||
a dışarıdan değiştirilmiş olsa bile bizim kodumuz bu değişikliği hemen görecektir. Örneğin:
|
||
|
||
volatile int flag;
|
||
...
|
||
flag = 0;
|
||
while (flag == 0) {
|
||
...
|
||
}
|
||
|
||
Burada flag nesnesi volatile olduğu için her flag kullanıldığında derleyici onu bellekten taze taze yeniden alacaktır. Böylece dışarıdaki bir el
|
||
flag değişkenini 1'e çektiğinde döngüden çıkılacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
64. Ders - 26/01/2023 Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
volatile niteleyicisi seyrek biçimde kullanılmaktadır. Eğer çok thread'li uygulamalar ya da birtakım kesme kodlarının oluşturulduğu uygulamalar söz konusu
|
||
değilse volatile niteleyicisine gereksinim duyulmaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
volatile niteleyicisi de göstericlerle kullanılabilmektedir. Tıpkı const niteleyicisinde olduğu gibi kendisi volatile olan, gösterdiği yer volatile olan,
|
||
hem kendisi hem gösterdiği yer volatile olan göstericiler söz konusu olabilmektedir. Öneğin:
|
||
|
||
volatile int *pi; /* kendisi değil, gösterdiği yer volatile olan gösterici */
|
||
int * volatile pi; /* gösterdiği yer değil kendisi volatile olan gösterici */
|
||
volatile int * volatile pi; /* hem kendisi hem de gösterdiği yer volatile olan gösterici */
|
||
|
||
Göstericinin gösterdiği yerin volatile olması "o gösterici ile * ya da [] oprratörü kullanılarak erişim yapıldığında erişilen yerin volatile olması"
|
||
anlamına gelmektedir. Yani bu durumda derleyici ne zaman göstsricinin gösterdiği yere erişilse onu yeniden taze taze bellekten yeniden alır. Örneğin:
|
||
|
||
int a = 10, b;
|
||
volatile int *pi = &a;
|
||
...
|
||
|
||
a = *pi + 10;
|
||
b = *pi + 20;
|
||
|
||
Derleyici bu durumda *pi bir yazmaçta olsa bile her defasında yeniden pi'nin gösterdiği yere erişecektir.
|
||
|
||
volatile anahtar sözcğü de türün bir parçasını oluşturmaktadır. Örneğin:
|
||
|
||
volatile int a;
|
||
|
||
Burada a "volatile int" türündendir. Tamamen const niteleyicisinde olan durumlar volatile niteleyicisinde de söz konusudur. Yani biz volatile bir
|
||
nesnenin adresini gösterdiği yer volatile olmayan bir göstericiye atayamayız. Ancak volatile olmayan bir bir nesnenin adresini gösterdiği yer volatile
|
||
olan bir göstericiye atayabiliriz. Başka bir deyişle volatile T * türünden T * türüne otomatik dönüştürme yoktur, ancak T * türünden volatile T *
|
||
türüne otomatik dönüştürme vardır.
|
||
|
||
Örneğin:
|
||
|
||
int a;
|
||
volatile int b;
|
||
int *pi1;
|
||
volatile int *pi2;
|
||
|
||
pi1 = &b; /* geçersiz, volatile int * türünden int * türüne otomatik dönüştürme yoktur. */
|
||
pi2 = &b; /* geçerli */
|
||
pi2 = &a; /* geçerli, int * türünden volatile int * türüne otomatik dönüştürme vardır */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
const ve volatile tür niteleyicileri beraber de kullanılabilmektedir. Bildirimde hangisinin önce yazılacağının bir önemi yoktur. Örneğin:
|
||
|
||
const volatile int *pi;
|
||
|
||
Burada pi'nin gösterdiği yer hem const hem de volatile durumdadır. Örneğin:
|
||
|
||
const volatile int x = 10;
|
||
|
||
Örneğin:
|
||
|
||
const volatile int *pi = &x;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
enum türleri ve sabitleri ilk kez C'de tasarlanmış ancak pek çok programlama diline benzer sentakslarla sokulmuştur. "enum" sözcüğü İngilizce
|
||
"enumarate" ve "enumaration" sözcüklerinden kısaltma yapılarak uydurulmuştur. "enumerate" tek tek saymak, numaralandırmak gibi anlamlara gelmektedir.
|
||
"enumeration" sözcüğü için Türkçe "sayımlama" sözcüğü de kullanılmaktadır. "enum" sözcüğü İngilizce "inam" ya da "inum" biçiminde okunmaktadır.
|
||
|
||
Bir enum bildiriminin genel biçimi şöyledir:
|
||
|
||
enum [isim] {
|
||
<enum sabit listesi>
|
||
};
|
||
|
||
Enum sabit listesi virgüllerle ayrılmış sabit belirten değişken isimlerinden oluşmaktadır. Örneğin:
|
||
|
||
enum COLOR {
|
||
Red, Green, Blue
|
||
};
|
||
|
||
Burada Red, Green ve Blue birer isimdir ancak sabit belirtmektedir. İlk enum sabitinin değeri 0'dır. Sonraki enum sabitleri öncekilerden bir fazla olarak devam eder.
|
||
Yani bu bildirimde Red 0 değerini, Green 1 değerini ve Blue 2 değerini belirtmektedir. Enum sabitlerine İngilizce "enumeration constans" denildiği gibi
|
||
kısaca "enumerator" da denilmektedir. Enum sabitleri isimsel olsa da aslında sabit bir sayı belirtmektedir. Dolayısıyla bunlara atama yapamayız. Örneğin:
|
||
|
||
Blue = 10; /* geçersiz bu işlem adeta 2 = 10 gibi bir işlemdir */
|
||
|
||
Enum sabitleri sabit ifadeleri gereken yerlerde kullanılabilmektedir. Örneğin:
|
||
|
||
switch (color) {
|
||
case Red:
|
||
...
|
||
case Green:
|
||
...
|
||
case Blue:
|
||
...
|
||
}
|
||
|
||
Buradaki Red, Green ve Blue sanki #define ile oluşturulmuş olan sembolik sabitler gibidir:
|
||
|
||
#define Red 0
|
||
#define Green 1
|
||
#define Blue 2
|
||
|
||
Her ne kadar enum sabitleri ile #define sabitleri semantik bakımdan birbirlerine benziyorsa da aralarında önemli bazı farklılıklar da vardır.
|
||
#define komutu önişlemci modülüne ilişkindir. Dolayısıyla bunların "faaliyet alanı (scope)" biçiminde bir kavramları yoktur. Oysa enum sabitleri
|
||
derleme modülüne ilişkindir. Bunların faaliyet alanları vardır. enum aynı zamanda bir tür de belirtmektedir. Enum sabitleri enum bildirimi hangi
|
||
faaliyet alanına yerleştirilmişse o faaliyet alaına sahip olur. Örneğin biz enum bildirimini global alana yerleştirirsek enum sabitleri de
|
||
global değişken gibi faaliyet gösterir.
|
||
|
||
Enum sabitleri gerçek sayı türlerine ilişkin olamazlar. Yalnızca tamsayı değerleri temsil ederler.
|
||
|
||
Bir enum bildirimi yapılırken enum ismi vermek zorunlu değildir. C++ programcıları genellikle enum bildiriminde tür ismi belirtmezler. Örneğin:
|
||
|
||
enum { sunday, monday, tuesday, wednesday, thursday, friday, saturday};
|
||
enum { January, February, March, April, May, June, July, August, September, October, November, December};
|
||
|
||
Bir enum sabitine '=' sentaksı ile bir sabit ifadesi kullanılarak değer verilebilir. Tabii bu işlem atama anlamına gelmez. Yalnızca "bu enum
|
||
sabitinin değeri bu olsun" anlamına gelir. Diğer enum sabitleri bu değeri izlemektedir.
|
||
|
||
enum COLOR {
|
||
Red = 10, Green, Blue
|
||
};
|
||
|
||
Burada Red = 10, Green = 11 ve Blue = 12 olur. Tabii istediğimiz birden fazla enum sabitine bu biçimde değerler verebiliriz. Örneğin:
|
||
|
||
enum { sunday, monday = 3, tuesday, wednesday, thursday = 7, friday, saturday};
|
||
|
||
Burada sunday = 0, monday = 3, tuesday = 4, wednesday = 5, thursday = 7, friday = 8, saturday = 9 olacaktır. Örneğin:
|
||
|
||
enum COMMAND {ADD = 1, DEL = 3, LIST = 5, QUIT = 7};
|
||
|
||
Birden fazla enuma sabitinin aynı değerde olmasının bir sakıncası yoktur. Örneğin:
|
||
|
||
enum KEY { Enter = 1, Return = 1, Insert, Home, PgDown = 12, PgUp = 20};
|
||
|
||
Bir enum sabiti gerçek sayı değerlerini alamaz. Enum sabitlerinin değerleri int türünün o sistemdeki sınırlarını aşamaz. Örneğin int türünün 4 byte yer
|
||
kapladığı bir sistemde söz konusu olsun:
|
||
|
||
enum COLOR {
|
||
Red = 2147483646, Green, Blue /* geçersiz! Blue int sınırlarını aşmış! */
|
||
};
|
||
|
||
Tabii enum sabitleri negatif değerler de alabilir. Örneğin:
|
||
|
||
enum TEST {AA = -1, BB, CC};
|
||
|
||
Burada AA = -1, BB = 0 ve CC = 1 değerlerini belirtecektir. Örneğin:
|
||
|
||
enum TEST = {A = 'a', B = 'b', C = 'd'}; /* geçerli */
|
||
|
||
Tek tırnak içerisine alınmış karakterlerin int türden sabit belirttiğini anımsayınız. Yukarıdaki enum bildirimi geçerlidir. Örneğin:
|
||
|
||
enum TEST = {A = "ankara", B = "izmir", C = "istanbul"}; /* geçersiz! */
|
||
|
||
Bir enum sabitine iki tırnak ile değer veremeyiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıda da belirttiğimiz gibi enum sabitleri enum bildirimi nerede yapılmışsa sanki orada bildirilmiş isimler gibi faaliyet alanına sahip olur.
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
enum COLOR {
|
||
Red, Green, Blue /* enum sabitleri her yerde kullanılabilir */
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
int a = Red; /* geçerli */
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo(void)
|
||
{
|
||
int x = Red; /* geçerli */
|
||
}
|
||
|
||
Burada enum bildirimi global düzeyde yapıldığı için enum sabitleri de global düzeyde her yerde kullanılabilir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
enum COLOR {
|
||
Red, Green, Blue /* enum sabitleri her yerde kullanılabilir */
|
||
};
|
||
|
||
int a;
|
||
|
||
a = Red; /* geçerli */
|
||
|
||
return 0;
|
||
}
|
||
|
||
void foo(void)
|
||
{
|
||
int x = Red; /* geçersiz! */
|
||
}
|
||
|
||
Burada enum bildirimi yerel düzeyde yapılmıştır. Bu durumda enum sabitleri de sanki yerel değişkenlermiş gibi yalnızca o blokta kullanılabilecektir.
|
||
Hemen her zaman enum sabitleri global düzeyde bildirilmektdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
enum sabitleri her zaman int türden sabitler olarak kabul edilmektedir. Örneğin:
|
||
|
||
enum {Red, Green, Blue};
|
||
|
||
Burada Red, Green ve Blue sabitleri int türdendir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
enum bildirimi aynı zamanda bir tür de oluşturmaktadır. Dolayısıyla enum türünden nesneler de tanılanabilmektedir. Tür ismi "enum" sözüğü ile enum isminden
|
||
oluşmaktadır. Örneğin:
|
||
|
||
enum COLOR {Red, Green, Blue};
|
||
|
||
Burada oluşturulan tür "enum COLOR" ismindedir. enum türündne nesneler de oluşturulabilmektedir. Örneğin:
|
||
|
||
enum COLOR c;
|
||
|
||
Pek çok programalama dilinde enum türünden bir nesneye yalnızca o enum türünün enum sabitleri atanabilmektedir. Ancak C'de durum böyle değildir.
|
||
C Standartlarına göre bir enum türü char türü ya da işaretli ya da işaretsiz tamsayı türlerinden biri ile "uyumlu (compatible)" olmak zorundadır. Yani enum
|
||
türleri derleyici için tamamen kendi seçtiği bir tamsayı türü ile özdeştir. Tabii derleyicinin o enum türü için seçtiği tamsayı türü o enum türünün
|
||
enum sabitlerinin değerlerini kapsayıcı olmalıdır. Buradan çıkan sonuç aslında enum türünden nesnelerin tamsayı türünden nesneler gibi ele alındığıdır.
|
||
Bu nedenle biz onlara tamsayı değerler atayabiliriz. Örneğin:
|
||
|
||
enum COLOR a = Red; /* geçerli */
|
||
enum COLOR b = 123; /* geçerli */
|
||
enum COLOR c = 12.3; /* geçerli, gerçek sayı değerleri tamsayı türünden nesnelere atanırsa noktadan sonraki kısım atılır */
|
||
|
||
Bir enum türünden nesnenin adresi doğrudan onunla uyumlu olan tamsayı türünden göstericiye de atanamaz. Örneğin:
|
||
|
||
enum COLOR c = 123;
|
||
int *pi;
|
||
|
||
pi = &c; /* enum COLOR int ile uyumlu olsa bile atama geçersizdir! */
|
||
|
||
Tabii enum türünden göstericiler de söz konusu olabilir. Örneğin:
|
||
|
||
enum COLOR *pc;
|
||
|
||
C'de enum türünden nesne tanımlamanaın pratik önemli bir faydası yoktur. Ancak C++ gibi, Java ve C# gibi dillerde en azından enum türü derleme zamanında
|
||
bir kontrol sunmaktadır. Bu dillerde enum türünden nesnelere biz yalnızca enum türünün sabitlerini atayabiliriz. Dolayısıyla o dillerde enum türünden nesneler
|
||
bu denetimden dolayı yaygın biçimde kullanılmaktadır. Örneğin aşağıdaki gibi bir C# kodu olsun:
|
||
|
||
enum Color {
|
||
Red, Green, Blue
|
||
};
|
||
Color c;
|
||
|
||
c = Color.Red; /* geçerli */
|
||
c = 0; /* geçersiz! */
|
||
|
||
Bu nedenlerden dolayı C'de programcılar enum türlerine genellikle isim de vermemektedir. Örneğin:
|
||
|
||
enum { Red, Green, Blue};
|
||
|
||
Tabii programcılar tarafından enum türleri okunabilirliği artırmak için de kullanılabilmektedir. Örneğin:
|
||
|
||
void foo(enum Color color)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Burada biz foo fonksiyonunu çağırırken argüman olarak enum Color türünde belirtilen enum sabitlerini kullanmazsak
|
||
herhangi bir sorun ortaya çıkmaz. Ancak kodu inceleyen kişiler fonksiyonun bir renk isteyen bir parametreye sahip
|
||
olduğunu anlar ve kodu daha iyi anlamlandırır. Tabii ilgili derleyicide enum Color eğer int türü anlamına geliyorsa
|
||
aslında bu fonksiyonun bildiriminin kullanım bakımından aşağıdakinden bir farkı yoktur:
|
||
|
||
void foo(int color)
|
||
{
|
||
/* ... */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
enum sabitlerinin faaliyet alanı enum bildiriminin yerleştirildiği yere ilişkin olduğu için ve aynı faaliyet alanında
|
||
aynı isimli tek bir değişken bulunabileceği için dikkatli olmak gerekir. Örneğin:
|
||
|
||
enum Fruit {
|
||
Apple, Orange, Banana
|
||
};
|
||
|
||
enum Company {
|
||
Apple, Microsoft, Oracle /* geçersiz! */
|
||
};
|
||
|
||
int Oracle; /* geçersiz! */
|
||
|
||
Burada aynı faaliyet alanı içerisinde iki defa aynı Apple ismi kullanılmıştır. Bu durum geçerli değildir. Benzer biçimde Oracle ismi de aynı faaliyet alanında
|
||
iki kez bildirilmiştir. Bu da geçerli değildir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir enum sabitine değer verilirken öncek enum sabitlerinden faydalanılabilir. Örneğin:
|
||
|
||
enum {Red, Green = Red + 2, Blue = Green + 2}; /* geçerli */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
enum türleri de typedef edilebilir. Örneğin:
|
||
|
||
enum Color {
|
||
Red, Green, Blue
|
||
};
|
||
|
||
typedef enum Color Cl;
|
||
|
||
Cl c; /* c enum Color türündendir */
|
||
|
||
Yapılarda olduğu gibi enum bildiriminde küme parantezinden sonra değişken listesi yazılırsa aynı zamanda o enum türünden nesneler de tanımlanmış olur. Örneğin:
|
||
|
||
enum Color {
|
||
Red, Green, Blue
|
||
} c, *pc;
|
||
|
||
Burada c enum Color türünden bir nesne ve pc de enum Color türünden bir göstericidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bu bölümde göstericilerle ilgili bazı karmaşık konular ele alınacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Göstericiler de birer nesne olduğuna göre göstericilerin de adresleri alınabilir. Örneğin:
|
||
|
||
int a = 10;
|
||
int *pi = &a;
|
||
|
||
Burada pi'nin içerisinde a'nın adresi vardır. Ancak pi'nin de adresi alınabilir. Daha öncedne de belirttiğimiz gibi göstericinin
|
||
türü sembolik olarak T * ile temsil edilmektedir. Örneğin:
|
||
|
||
int *pi;
|
||
|
||
Burada pi "int *" türündendir. Bir nesnenin adresini aldığımızda tür bileşeni o nesnenin türünden olan sayısal bileşeni o nesnenin
|
||
bellekteği başlangıç adresi olan bir adres bilgisini elde ederiz. Örneğin:
|
||
|
||
int a;
|
||
|
||
Burada biz &a işlemiyle "int *" türünden bir adres elde ederiz. Şimdi bir göstericinin adresini alalım:
|
||
|
||
int *pi;
|
||
|
||
Burada &pi ile pi göstericisinin adresini aldığımızda adresin tür bileşeni int * olacaktır. Pekiyi bu adresi hangi türden bir göstericiye yerleştirebiliriz?
|
||
Tabii ki tür bileşeni int * olan bir göstericiye yerleştrebiliriz. C'de iki yıldızlı göstericilere "göstericileri gösteren göstericiler (pointers to pointers)"
|
||
denilmektedir. Örneğin:
|
||
|
||
int **ppi;
|
||
|
||
Burada ppi nir göstericiyi gösteren gösterisidir. Buradaki göstericinin tür bileşeninin "int *" olduğuna dikkat ediniz:
|
||
|
||
int * *ppi;
|
||
|
||
Burada *ppi yani ppi'nin gösterdiği yerdeki nesnenin türü "int *" biçimindedir. Yani burada ppi bir göstericiyi göstermektedir. C'de "int *" türü
|
||
int nesneyi gösteren bir adres türü anlamına geldiğine göre "int **" türü de int * türündne nesneyi gösteren bir adres türü anlamına gelmektedir.
|
||
Örneğin:
|
||
|
||
int a = 10;
|
||
int *pi;
|
||
int **ppi;
|
||
|
||
pi = &a;
|
||
ppi = π
|
||
|
||
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 = π
|
||
|
||
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 = π /* 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 = π
|
||
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 = π /* 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 = π /* 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 = π /* 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 = π /* 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 = π
|
||
|
||
**ppi = 20; /* geçerli */
|
||
*ppi = π /* 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 = π /* 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 = π /* 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 = π /* 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 = π /* 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 = π /* 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 = π /* bu dönüştürme geçerli mi? */
|
||
|
||
Aslında burada tukarıda bahsettiğimiz kötüye kullanım söz konusu değildir. Ancak C standartları buradaki göstericilerin
|
||
türlerinin farklı olmasından dolayı otomatik dönüştürmeyi kabul etmemktedir. C++'ta bu atama geçerlidir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
66. Ders - 02/02/2023 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de çok boyutlu diziler de tanımlanabilmektedir. Çok boyutlu diziler aslında doğal türler değildir. Çünkü bilgisayarımızın belleği tek boyutludur,
|
||
çok boyutlu değildir. Ancak yine de doğaaki bazı olguları temsil edebilmek için çok boyutlu dizi kavramından faydalanılmaktadır. C'de çok boyutlu diziler
|
||
dekleratörde birden fazla köşeli parantez ile tanımlanmaktadır. Örneğin:
|
||
|
||
int a[3][2];
|
||
|
||
Burada 3 satır 2 sütuna sahip iki boyutlu bir dizi tanımlanmıştır. Örneğin:
|
||
|
||
int b[3][2][5];
|
||
|
||
Buada üç boyutlu bir dizi tanımlanmıştır. Her ne kadar boyut sayısı istenildeiğ kadar çok olabilirse de pratikte en çok iki boyutlu diziler kullanılmaktadır.
|
||
İki boyutlu dizilere "matris" de denilmektedir. Üç boyutlu dizilere çok seyrek gereknimin duyulmaktadır.
|
||
|
||
Çok boyutlu dizilerde elemana erişmek için bireden fazla köşeli parantez kullanılır. Örneğin:
|
||
|
||
int a[3][2];
|
||
|
||
a[0][0] = 10;
|
||
a[0][1] = 20;
|
||
a[1][0] = 30;
|
||
a[1][1] = 40;
|
||
a[2][0] = 50;
|
||
a[2][1] = 60;
|
||
|
||
Bütün boyutlar 0'ıncı indeksten başlatılarak indekslenmektedir.
|
||
|
||
Aşağıdaki örnekte iki boyutlu bir dizi matris görünümüyle iç içe iki döngü kullanılarak dolaşılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[3][2];
|
||
|
||
a[0][0] = 10;
|
||
a[0][1] = 20;
|
||
a[1][0] = 30;
|
||
a[1][1] = 40;
|
||
a[2][0] = 50;
|
||
a[2][1] = 60;
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
for (int k = 0; k < 2; ++k)
|
||
printf("%d ", a[i][k]);
|
||
printf("\n");
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında çok boyutlu diziler C'de "dizi dizisi" gibi ele alınmaktadır. Örneğin:
|
||
|
||
int a[3][4];
|
||
|
||
Bu tanımlamayı derleyici adeta "3 elemanlı, her elemanı 4 elemanlı int değerlerden oluşan bir dizi" tanımlaması olarak ele almaktadır. İlk köşeli
|
||
parantez asıl diziyi belirtir. Sonraki köşeli parantezler eleman olan dizileri belirtmektedir. Bir dizinin türünün sembolik olarak "T [N]" ile temsil
|
||
edildiğini belirtmiştik. Yukarıdaki a dizisi aslında her elemanı "int[4] (4 elemanlı int dizi)" türünden olan 3 elemanlı bir dizi dizisidir. Burada a'nın
|
||
türü "int[3][4]" biçiminde belirtilmektedir. Yani burada a "int[3][4]" türündendir. a dizisinin elemanları da "int[4]" türündendir. Dizi elemanları
|
||
ardışıl olduğuna göre yukarıdaki a dizisinin bellekteki organizasyonu şöyle olacaktır:
|
||
|
||
a[0][0]
|
||
a[0][1]
|
||
a[0][2]
|
||
a[0][3]
|
||
a[1][0]
|
||
a[1][1]
|
||
a[1][2]
|
||
a[1][3]
|
||
a[2][0]
|
||
a[2][1]
|
||
a[2][2]
|
||
a[2][3]
|
||
|
||
Burada a her elemanı 4 elemanlı bir int dizi olan dizidir. Dolayısıyla biz bu a dizisini şöyle temsil edebiliriz:
|
||
|
||
4 elemanlı dizi
|
||
4 elemanlı dizi
|
||
4 elemanlı dizi
|
||
|
||
Burada dikkat edilmesi gereken durum şudur: Bu tanımlamada a aslında 3 elemanlı bir dizidir. Bu 3 eleman ardışıl olmalıdır. Öte yandan a'nın her elemanı da
|
||
bir dizi olduğuna göre o dizinin elemanları da ardışıl olmak zorundadır. O halde organizasyon mecburen yukarıdaki gibi olacaktır. Yani buradaki tüm int
|
||
değerler aslında bellekte ardışıl bir biçimde bulunmaktadır. Örneğin:
|
||
|
||
int a[3][4][2];
|
||
|
||
Burada asıl dizi 3 elemanlıdır. Ancak bu 3 elemanlı dizinin her elemanı aslında bir matristir. a "int [3][4][2]" türündedir. a'nın elemanları ise
|
||
"int[4][2]" türündedir. Bu eleman olan dizinin elemanları ise "int[2]" türündendir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Çok boyutlu dizilere ilkdeğer verilirken onun aslında bir dizisi dizisi olduğu dikkate alınmalıdır. Verilen ilkdeğerler tek tek yukarıdan aşağıya doğru
|
||
dizi elemanlarına yerleştirilir. Örneğin:
|
||
|
||
int a[3][2] = {10, 20, 30, 40, 50, 60};
|
||
|
||
Bu dizinin bellekteki organizasyonu şöyledir:
|
||
|
||
a[0][0]
|
||
a[0][1]
|
||
a[1][0]
|
||
a[1][1]
|
||
a[2][0]
|
||
a[2][1]
|
||
|
||
İşte verilen ilkdeğerler de bellekteki organiasyona göre elemanlara yerleştirilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[3][2] = {10, 20, 30, 40, 50, 60};
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
for (int k = 0; k < 2; ++k)
|
||
printf("%d ", a[i][k]);
|
||
printf("\n");
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Ancak çok boyutlu dizilere ilkdeğer verilirken eleman olan her dizi ayrıca küme parantezine alınabilir. Bu iyi bir tekniktir. Örneğin:
|
||
|
||
int a[3][2] = {
|
||
{10, 20},
|
||
{30, 40},
|
||
{50, 60}
|
||
};
|
||
|
||
Örneğin:
|
||
|
||
int a[3][2][3] = {
|
||
{
|
||
{1, 2, 3},
|
||
{4, 5, 6}
|
||
},
|
||
{
|
||
{7, 8, 9},
|
||
{10, 11, 12}
|
||
},
|
||
{
|
||
{13, 14, 15},
|
||
{16, 17, 18}
|
||
}
|
||
};
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[3][2] = {
|
||
{10, 20},
|
||
{30, 40},
|
||
{50, 60}
|
||
};
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
for (int k = 0; k < 2; ++k)
|
||
printf("%d ", a[i][k]);
|
||
printf("\n");
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi uzunluk belirtmeden diziye ilkdeğer verebiliyorduk. Çok boyutlu dizilerde yalnızca ilk köşeli parantezin içi boş bırakılabilmektedir.
|
||
Örneğin:
|
||
|
||
int a[][2] = {{10, 20}, {30, 40}, {50, 60}, {70, 80}};
|
||
|
||
Aşağıdaki ilkdeğer verme geçersizdir:
|
||
|
||
int a[][] = {{10, 20}, {30, 40}, {50, 60}, {70, 80}};
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Mademki char türden bir diziye iki tırnak ifadesiyle ilkdeğer verilebilmektdir. O halde char türden bir dizi dizisinin
|
||
her elemanına da iki tırnak ile ilkdeğer verebiliriz. Örneğin:
|
||
|
||
char s[][10] = {"ali", "veli", "selami"};
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char s[][10] = {"ali", "veli", "selami"};
|
||
|
||
for (int i = 0; i < 3; ++i)
|
||
puts(s[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi C99 ile birlikte C'ye "designated initializer" ismi altında dizinin yalnızca istenilen elemanlarına ilkdeğer verilebiliyordu.
|
||
Çok boyutlu dizilerde iç bir elemana bu biçimde değer verebiliriz. Ya da dış elemana da küme parantezleri ile değer verebiliriz.
|
||
Her zaman designated initializer sonrasındaki eleman son verilen elemandan itibaren devam ettrilmektedir. Örneğin:
|
||
|
||
int a[4][3] = {[1][0] = 3, 5, [2] = {1, 2}, 4};
|
||
|
||
Burada 3 değeri dizinin [1][0] elemanına yerleştirilmiştir. Bu durumda 5 değeri [1][1] elemanına yerleştirilecektir.
|
||
{1, 2} ilkdeğeri dizinin 2'inci elemanı olan diziye yerleştirilmiştir. Sonraki eleman artık [3][0] elemanı olduğu için
|
||
4 değeri bu elamana yerleitirilmiştir. Dolayısıyla oluşturulan matris şöyledir:
|
||
|
||
0 0 0
|
||
3 5 0
|
||
1 2 0
|
||
4 0 0
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[4][3] = {[1][0] = 3, 5,[2] = {1, 2}, 4};
|
||
|
||
for (int i = 0; i < 4; ++i) {
|
||
for (int k = 0; k < 3; ++k)
|
||
printf("%d ", a[i][k]);
|
||
putchar('\n');
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıda belirttiğimiz gibi çok boyutlu diziler aslında dizi dizilerdir. Örneğin:
|
||
|
||
int a[4][3];
|
||
|
||
Burada her biri 3 elemandan oluşan 4 elemanlık bir dizi söz konusudur. a[i] ifadesi de aslında bu dizi dizisinin
|
||
i'inci elemanına ilişkin diziyi belirtmektedir. O halde a[i][k] ifadesi aslında a dizi dizisinin i'inci elemanın
|
||
belirttiği dizinin k'ıncı elemanıdır. Tabii burada yine a[i] ifadesi bir nesne belirtmez. Yani bu ifadeye atama
|
||
yapamayız. Bu ifade kod içerisinde kullanıldığında dizi dizsinin i'inci elemanına ilişkin dizinin başlangıç adresi
|
||
anlamına gelmektedir. İki boyutlu dizileri gösteri dizileri ile karıştırmayınız.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de "dizi göstericileri (pointer to array)" biçiminde ilginç bir gösterici türü vardır. Bir dizi göstericisi dizinin türü T ve uzunluğu N olmak üzere
|
||
aşağıdaki gibi tanımlanmaktadır:
|
||
|
||
T (*p)[N];
|
||
|
||
Buradaki normal parantezler p'nin bir dizi göstericisi olduğunu belirtmektedir. Eğer bu parantezler olmasaydı p "gösterici dizisi" belirtirdi:
|
||
|
||
T *p[N];
|
||
|
||
Bir dizi göstericisi * operatörüyle kullanıldığında tek bir eleman değil dizinin tamamı elde edilmektedir. Örneğin:
|
||
|
||
int (*pai)[5];
|
||
|
||
Burada *pai ifadesi bir nesne belirtmez. Sanki bir dizi ismi gibi ele alınmaktadır. *pai ifadesi adeta bir dizinin ismi gibi düşünülmelidir.
|
||
|
||
Anımsanacağı gibi diziler C'de bir nesne belirtirler fakat dizilerin isimleri ifade içerisinde kullanıldığında artık o ifade dizinin ilk
|
||
elemanının adresini belirtmektedir. C'de bir dizi nesnesinin adresi & operatörüyle alınabilir. Bu durumda elde ed,len adres bir dizi göstericisine
|
||
yerleştirilmektedir. Örneğin:
|
||
|
||
int a[5];
|
||
int (*pai)[5];
|
||
int *pi;
|
||
|
||
pai = &a; /* geçerli */
|
||
pi = a; /* geçerli */
|
||
pi = &a; /* geçersiz! */
|
||
|
||
C'de bir dizinin adresi programcı tarafından nadiren alınır. Ancak dizinin bu biçimde adresi alınmışsa onun bir dizi göstericisine yerleştirilmesi
|
||
gerekir. Bir dizinin adresi aynı uzunlukta bir göstericisine yerleştirilmelidir. Örneğin:
|
||
|
||
int a[5];
|
||
int (*pai)[6];
|
||
|
||
pai = &a; /* geçersiz! */
|
||
|
||
Dizi göstericileri tanımlarken köşeli parantezin için boş bırakılamaz. Örneğin:
|
||
|
||
int (*pai)[]; /* geçersiz! */
|
||
|
||
Bir dizinin adresi alındığında elde edilen tür sembolik olarak T (*)[N] ile gösterilmektedir. Örneğin:
|
||
|
||
int a[5];
|
||
|
||
Burada a dizisi int[5] türündendir. a bir ifade içerisinde kullanıldığında int * türüne dönüştürülür. a'nın adresi
|
||
alındığında (yani &a ifadesi) int (*)[5] türündendir.
|
||
|
||
Bir dizi göstericisi * operatörüyle kullanıldığında bu ifade bir nesne belirtmez. Bir dizi belirtir. Örneği:
|
||
|
||
int a[5];
|
||
int (*pai)[5];
|
||
|
||
pai = &a; /* geçerli */
|
||
*pai = 10; /* geçersiz *pai bir nesne belirtmiyor */
|
||
|
||
Biz *pai ifadesi ile dizinin tamamına erişmiş gibi oluruz. Onun da elemanlarına erişmemiz gerekir. Örneğin:
|
||
|
||
int a[5] = {1, 2, 3, 4, 5};
|
||
int (*pai)[5];
|
||
int *pi;
|
||
|
||
pai = &a;
|
||
|
||
(*pai)[1] = 10; /* dizinin 1'inci indisli elemanı güncelleniyor */
|
||
|
||
Burada (*pai)[1] ifadesinde *pai için parantez kullanıldığına dikkat ediniz. Bu parantezler olmasaydı önce [] operatörü yapılırdı bu da başka bir
|
||
anlama gelirdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizinin ismi dizinin ilk elemanın adresi anlamına geliyordu. Yani dizinin ismi dizi elemanı türünden bir adres belirtiyordu. örneğin:
|
||
|
||
int a[3][2];
|
||
|
||
Burada a dizisi aslında 2 elemanlı int dizilerin dizisidr. Bu durumda bu dizinin ismi ilk elemanın adresi olacağına göre ancak 2 elemanlı bir dizi göstericisine
|
||
atanabilir. Başka bir deyişle biz burada a ismini kifade içerisinde kullandığımızda aslında int[2] türünden bir dizinin adresini kullanmış gibi oluruz.
|
||
O halde biz çok boyutlu dizilerin isismlerini göstericiyi gösteren göstericilere değil, normal göstericilere de değil, ancak dizi göstericilerine
|
||
atayabiliriz. Örneğin:
|
||
|
||
int a[3][2];
|
||
int (*pai)[2];
|
||
|
||
pai = a; /* geçerli
|
||
|
||
Burada dizi göstericisinin matrisin sütun uzunluğuna ilişkin olduğuna dikkat ediniz.Pekiyi burada pai nereyi gösteriyor? Tabii ki dizi dizisinin
|
||
ilk dizisini gösteriyor. Bir dizi dizisi 1 artırılırsa adresin sayısal bileşeni dizinin uzunluğu kadar artacaktır. Bu durumda pai[0], pai[1] ve pai[2]
|
||
aslında bu dizi dizisinin elemanları olan dizileri belirtmektedir. Tabii bu ifadelerin hiçbiri nesne belirtmez. O halde biz çok boyutlu bir dizinin
|
||
ismini bir dizi göstericisine atayıp o dizi göstericisini iki köşeli parantez operatörü ile kullanabiliriz. Örneğin:
|
||
|
||
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
|
||
int (*pai)[2];
|
||
|
||
pai = a;
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
for (int k = 0; k < 2; ++k)
|
||
printf("%d ", pai[i][k]);
|
||
printf("\n");
|
||
}
|
||
|
||
Burada bir noktaya dikkatinizi çekmek istiyoruz. T a[N][K] gibi bir matrisin ismine biz ancak T (*pa)[K] biçiminde
|
||
bir dizi göstericisine atayabiliriz. Yani dizi göstericimizin bildiriminde belirtilen uzunluğun matrisin sütun uzunluğu
|
||
ile aynı olması gerekmektedir. Tüm matrislerin atanabileceği genel bir gösterici yoktur. Örneğin:
|
||
|
||
int a[3][2];
|
||
int b[5][2];
|
||
int c[5][3];
|
||
|
||
int (*pai)[2];
|
||
|
||
pai = a; /* geçerli */
|
||
pai = b; /* geçerli */
|
||
pai = c; /* geçersiz */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
|
||
int(*pai)[2];
|
||
|
||
pai = a;
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
for (int k = 0; k < 2; ++k)
|
||
printf("%d ", pai[i][k]);
|
||
printf("\n");
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir matrisi fonksiyona parametre yoluyla geçireseksek fonksiyonun parametre değişkeninin dizi göstericisi olması gerekir. Dizi göstericilerini tanımlarken
|
||
köşeli parantez içerisinde uzunluk belirtilmek zorunda olduğumuza göre böyle fonksiyonlar ancak sütun uzunluğu belirlenmiş olan matrisleri kabul edebilirler.
|
||
Örneğin:
|
||
|
||
void disp_matrix(const int (*pai)[2], int size)
|
||
{
|
||
...
|
||
}
|
||
|
||
Biz burada fonksiyona herhangi bir int türden matrisi geçiremeyiz. Sütun uzunluğu 2 olan int türden matrisleri geçirebiliriz.
|
||
|
||
Pekiyi aşağıdaki gibi üç boyutlu bir diziyi aktaracağımız fonksiyonun parametre değişkeni nasıl olmalıdır?
|
||
|
||
int a[3][4][2];
|
||
|
||
İşte çok boyutlu bir dizi göstericisi de söz konusu olabilir. Örneğin:
|
||
|
||
int (*pai)[4][2];
|
||
|
||
Bu dizi göstericisi her elemanı 4x2 olan bir matrisi gösteren göstericidir. O halde yukarıdaki üç boyutlu diziyi alacak olan fonksiyonun parametrik yapısı şöyle
|
||
olmalıdır:
|
||
|
||
void foo(int (*pai)[4][2] , int size)
|
||
{
|
||
}
|
||
...
|
||
foo(a, 3);
|
||
|
||
Özetle n boyutlu bir matrisin ismini atayabileceğimiz bir göstericinin ilk boyut uzunluğu dışındaki tüm boyut uzunluklarının
|
||
aynı olması gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void disp_matrix(const int(*pai)[2], int size)
|
||
{
|
||
for (size_t i = 0; i < size; ++i) {
|
||
for (int k = 0; k < 2; ++k)
|
||
printf("%d ", pai[i][k]);
|
||
printf("\n");
|
||
}
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
|
||
|
||
disp_matrix(a, 3);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıda belirttiğimiz gibi bir matrisi fonksiyona adres yoluyla aktaracaksak fonksiyonun parametresi matrisin sütun uzunluğu kadar olan
|
||
bir göstericisi olmalıdır. O halde biz her türlü matrisi aktaracağımız bir fonksiyonu dizi göstericisi yoluyla oluşturamayız. Örneğin:
|
||
|
||
void foo(int (*pa)[5])
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Biz bu fonksiyona sütun uzunşuğu 5 olan matrisleri aktarabiliriz. Sütun uzunluğu farklı olan matrisleri aktaramayız.
|
||
|
||
Pekiyi biz bu durumda satır ve sütun uzunluğu herhangi biçimde olan bir matrisi alan bir fonksiyonu nasıl yazabiliriz? Yani örneğin aşağıdaki gibi iki matrisi de
|
||
parametre olarak alabilen bir fonksiyon yazılabilir mi?
|
||
|
||
int a[3][2];
|
||
int b[5][4];
|
||
|
||
Maalesef C'de dizi göstericileriyle ya da başka yöntemlerle bunu yapmanın pratik bir yolu yoktur. Bunu sağlamanın tek yolu fonksiyonun parametresinin bir
|
||
void gösterici olması, sonra bu void göstericinin fonksiyon içerisinde ilgili türden bir göstericiye atanması ve sonra da bu gösterici yoluyla matrisin bellek
|
||
organizasyonu bilindiğine göre elemanlara erişilmesidir. Örneğin:
|
||
|
||
void foo(void *pmatrix, size_t rowsize, size_t colsize)
|
||
{
|
||
int *pi = (const int *)pmatrix;
|
||
|
||
/* ... */
|
||
}
|
||
|
||
Burada foo fonksiyonu içerisinde biz artık matrisin i'inci satır k'ıncı sütun elemanına pi[i * colsize + k] ifadesiyle erişlebiliriz.
|
||
Örneğin:
|
||
|
||
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
|
||
int b[4][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}};
|
||
|
||
foo(a, 3, 2);
|
||
foo(b, 4, 3);
|
||
|
||
Dizi göstericilerinde köşeli parantez içinin sabit ifadesi olması gerekir. Aşağıdaki gibi bir bildirim geçerli değildir:
|
||
|
||
void foo(size_t rowsize, size_t colsize, int (*pai)[colsize]); /* geçerli değil */
|
||
|
||
Satır ve sütun uzunluklarıyla bir matrisi alan fonksiyonun parametresi bir göstericiyi gösteren gösterici olamaz. Göstericiyi gösteren göstericinin
|
||
gösterdiği yerde bir göstericinin olması gerekir.
|
||
|
||
Bu biçimde genel bir fonksiyonu yazmanın aşağıdakinden daha pratik bir yolu yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void disp_matrix(const void *pmatrix, size_t rowsize, size_t colsize)
|
||
{
|
||
const int *pi = (const int *)pmatrix;
|
||
|
||
for (size_t i = 0; i < rowsize; ++i) {
|
||
for (size_t k = 0; k < colsize; ++k)
|
||
printf("%d ", pi[colsize * i + k]);
|
||
printf("\n");
|
||
}
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[3][2] = {{1, 2}, {3, 4}, {5, 6}};
|
||
int b[4][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}};
|
||
|
||
disp_matrix(a, 3, 2);
|
||
disp_matrix(b, 4, 3);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Tabii çok boyutlu dizilerin tüm elemanları bellekte ardışıl olduğuna göre çok boyutlu diziler için de dinamik tahsisat yapılabilir.
|
||
Örneğin biz 3x2 boyutlarında int türden bir matris için aşağıdaki gibi dinamik tahsisat yapabiliriz:
|
||
|
||
int (*pa)[2];
|
||
|
||
pa = (int (*)[2]) malloc(sizeof(int) * 3 * 2);
|
||
|
||
/* ... */
|
||
|
||
free(pa);
|
||
|
||
Artık biz bu pa gösterici yoluyla tahsis edilen alanı bir matris gibi kullanabiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
void disp_matrix(int **pmatrix, size_t rowsize, size_t colsize)
|
||
{
|
||
const int *pi = (const int *)pmatrix;
|
||
|
||
for (size_t i = 0; i < rowsize; ++i) {
|
||
for (size_t k = 0; k < colsize; ++k)
|
||
printf("%d ", pi[colsize * i + k]);
|
||
printf("\n");
|
||
}
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int (*pai)[2];
|
||
|
||
if ((pai = (int(*)[2]) malloc(sizeof(int) * 3 * 2)) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (int i = 0; i < 3; ++i)
|
||
for (int k = 0; k < 2; ++k)
|
||
pai[i][k] = i + k;
|
||
|
||
|
||
for (int i = 0; i < 3; ++i) {
|
||
for (int k = 0; k < 2; ++k)
|
||
printf("%d ", pai[i][k]);
|
||
printf("\n");
|
||
}
|
||
|
||
free(pai);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir matris etkisi göstericiyi gösteren göstericilerle de dolaylı bir biçimde sağlanabilir. Örneğin ppi N elemanlı int türden bir göstericisi dizisini
|
||
gösteriyor olabilir. Bu gösterici dizisinin her elemanı da birer int diziyi gösteriyor olabilir.
|
||
|
||
ppi ------->
|
||
ptr ----> dizi elemanları
|
||
ptr ----> dizi dizi elemanları
|
||
ptr ----> dizi dizi elemanları
|
||
...
|
||
ptr ----> dizi dizi elemanları
|
||
|
||
Bu durumda biz ppi[i][k] ifadesi ile aslında gösterici dizisinin i'inci elemanının gösteridği dizinin k'ıncı elemanına erişmiş oluruz.
|
||
Bu da bir çeşit matris erişimi gibi olur. Pekiyi bir matris göstericiyi gösteren gösterici yoluyla ve çok boyutlu dizi yoluyla oluşturulabiliyorsa
|
||
bunların arasındaki farklar nasıldır?
|
||
|
||
- Göstericiyi gösteren gösterici yolu ile matris tasarımında matris toplamda daha fazla yer kaplamaktadır . Çünkü bu tasarımda gösterici dizisinin kendisi de
|
||
yer kaplar.
|
||
|
||
- Çok boyutlu dizilerde her satırda eşit sayıda sütun bulunmak zorundadır. Oysa gösterici dizisi yönteminde her satırda eşit sayıda sütun elemanı bulunmak zorunda değildir.
|
||
|
||
- Gösterici dizilerinde elemana erişim matrislerdeki elemana erişimden (nano düzeyde) daha yavaş olmaktadır. Çünkü elemana erilişirken bir makine komutu daha
|
||
kullanılmak durumundadır.
|
||
|
||
- Göstericiyi gösteren gösterici yoluyla matris oluştururken matris elemanaları birbirinden uzak yerlerde olabilmektedir. Bir veri yapısında elemanların birbirlerinden
|
||
uzak ya da yakın olmasına İngilizce "locality of reference" denilmektedir. Elemanların birbiriden uzak olması genel olarak eleman erişimini yavaşlatabilmektedir.
|
||
|
||
- Göstericiyi gösteren göstericilerle matrisler oluşturulduğunda matrisin her satırı eşit sayıda eleman içermek zorunda değildir. Yani her satır
|
||
farklı uzunlukta elemanlar içerebilir. Halbuki çok boyutlu dizilerde bütün boyutların aynı uzunlukta olması gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
67. Ders - 16/02/2023 - Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Göstericiler konusunda nispeten karmaşık olan bir konu da "fonksiyon göstericileri (pointer to function)" konusudur. Bu bölümde fonksiyon göstericileri üzerinde
|
||
duracağız. Ancak bu konu daha derinlemesine ve uygulamalı olarak "Sistem Programlama ve İleri C Uygulamaları 1" kursunda ele alınmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyon makine kodlarından oluşmaktadır. Bu makine kodları da bellekte ardışıl bir biçimde bulunmaktadır. Yani bir fonksiyonun makine kodları
|
||
bellekte ardışıl byte topluluğu biçiminde bulunur. O halde fonksiyonların da başlangıç adreslerinden bahsedilebilir. Bir fonksiyonun başlangıç adresi
|
||
onun makine kodlarının bellekte bulunduğu yerin başlangıç adresidir. Fonksiyonları çağırmak için makine dilinde CALL isimli makine komutları bulunmaktadır.
|
||
Bu CALL komutları fonksiyonun başlangıç adresini alır ve işlemcinin o adresten itibaren komut çalıştırmasını sağlar. Örneğin:
|
||
|
||
CALL <adres>
|
||
|
||
Biz C'de adresleri "data" ve "kod" adresleri biçiminde ikiye ayırabiliriz. Şimdiye kadar gördüğümüz adreslerin hepsi "data" adresiydi. Data adresi
|
||
demekle fonksiyon olmayan nesnelerin adresleri kastedilmektedir.
|
||
|
||
C'de fonksiyonların başlangıç adreslerini gösteren göstericilere "fonksiyon göstericileri" denilmektedir. Bir fonksiyon göstericisi tanımlamanın genel
|
||
biçimi şöyledir:
|
||
|
||
<geri_dönüş_değerinin_türü> (* <gösterici_ismi>)([parametre bildirimi]);
|
||
|
||
Örneğin:
|
||
|
||
void (*pf1)(void);
|
||
int (*pf2)(int, int);
|
||
double (*pf3)(double, double);
|
||
|
||
Fonksiyon göstericileri her türlü fonksiyonları gösteremez. Geri dönüş değeri ve parametre türleri belli biçimde olan fonksiyonları gösterebilir.
|
||
Örneğin:
|
||
|
||
int (*pf)(int, int);
|
||
|
||
Burada pf fonksiyon göstericisi "geri dönüş değeri int olan, parametresi int, int olan" fonksiyonları gösterebilir. Yani biz bu fonksiyon göstericisinin
|
||
içerisine "geri dönüş değeri int olan, parametreleri int, int olan fonksiyonların" adreslerini yerleştirebilir.z Fonksiyon göstyericileri bildirilirken
|
||
paramtre değişkenleri için isimler de verilebilir. Bu isimler derleyici tarafından kullanılmaz. Yalnızca okunabilirliği artırmak için kullanılmaktadır. Örneğin:
|
||
|
||
int (*pf)(int a, int b);
|
||
|
||
Tabii buradaki parametre isimlerinin prototipteki ya da tanımlamadaki parametre isimleriyle uyuşması gerekmemektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında C standartlarına göre fonksiyonlar da bir çeşit nesne gibidir. Bir fonksiyonun ismi bir ifadede kullanıldığında artık bu isim o fonksiyonun başlangıç adresi
|
||
anlamına gelir. Buna standartlarda "function to pointer conversion" denilmektedir. O halde C'de bir fonksiyonun yalnızca ismi (parantezler olmadan) o fonksiyonun
|
||
bellekteki başlangıç adresini belirtmektedir. Aslında fonksiyonu çağırırken kullandığımız parantezler "falanca adresteki fonksiyonu çağır" anlamına gelmektedir.
|
||
Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
Burada biz yalnızca "foo" ifadesini kullandığımızda bu ifade "foo fonksiyonunun bellekteki başlangıç adresi" anlamına gelir. foo() ifadesi de "foo adresindeki
|
||
kodu çalıştır" anlamına gelmektedir. Fonksiyon çağırma operatörü "tek operandlı sonek (unary postfix)" bir operatördür. Bu operatörün operandının bir
|
||
fonksiyon adresi olması gerekir.
|
||
|
||
Bir fonksiyon göstericisine her fonksiyonun adresi yerleştirilemez. Geri dönüş değeri ve parametrik yapısı uyumlu olan fonksiyonların adresleri yerleştirilebilir.
|
||
Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
void bar(int a)
|
||
{
|
||
printf("bar\n");
|
||
}
|
||
|
||
int tar(int a, int b)
|
||
{
|
||
/* .... */
|
||
}
|
||
|
||
int (*pf)(int, int);
|
||
|
||
pf = foo; /* geçersiz! */
|
||
pf = bar; /* geçersiz */
|
||
pf = tar; /* geçerli */
|
||
|
||
Fonksiyon göstericisine atama yapılırken yanlışlıkla parantez kullanmayınız. Örneğin:
|
||
|
||
pf = tar(); /* geçersiz! */
|
||
|
||
Burada tar fonksiyonun adresi pf'ye atanmamaktadır, tar fonksiyonunun geri dönüş değeri pf'ye atanmaktadır. Bir fonksiyon göstericisine bir data adresi de
|
||
atanamaz. Ancak uyumlu bir fonksiyonun adresi atanabilir. Örneğin:
|
||
|
||
int *pi;
|
||
int (*pf)(int, int);
|
||
...
|
||
|
||
pf = pi; /* geçersiz! */
|
||
|
||
pf bir fonksiyon göstericisi olmak üzere bu göstericinin içerisindeki adreste bulunan fonksiyonu çağırabilmek için yine fonksiyon çağırma operatörü kullanılır.
|
||
Tabii fonksiyonun parametreleri varsa yine argümanlar parantez içerisine yazılmalıdır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <shellapi.h>
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
void (*pf)(void);
|
||
|
||
pf = foo;
|
||
|
||
pf(); /* pf göstericisinin içerisindeki adreste bulunan fonksiyonu çağır */
|
||
|
||
return 0;
|
||
}
|
||
|
||
C'de aslında resmi olarak pf bir fonksiyon göstericisi olmak üzere pf göstericisinin içerisindeki adreste bulunan fonksiyonu çağırmak için iki sentaktik
|
||
biçim vardır: pf(...) ve (*pf)(...). Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
...
|
||
void (*pf)(void);
|
||
|
||
pf = foo;
|
||
|
||
pf();
|
||
(*pf)();
|
||
|
||
Programcılar tarafından pf(..) biçimindeki doğal çağrım daha fazla tercih edilmektedir.
|
||
|
||
Tabii bir fonksiyon göstericisine ilkdeğer de verilebilir. Örneğin:
|
||
|
||
void (*pf)(void) = foo;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte fonksiyon göstericisine onunla uyumlu fonksiyonların adresleri atanarak fonksiyon göstericisi yoluyla bu fonksiyonlar çağrılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int add(int a, int b)
|
||
{
|
||
return a + b;
|
||
}
|
||
|
||
int sub(int a, int b)
|
||
{
|
||
return a - b;
|
||
}
|
||
|
||
int mul(int a, int b)
|
||
{
|
||
return a * b;
|
||
}
|
||
|
||
int div(int a, int b)
|
||
{
|
||
return a / b;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int (*pf)(int, int);
|
||
int result;
|
||
|
||
pf = add;
|
||
|
||
result = pf(20, 10);
|
||
printf("%d\n", result); /* 30 */
|
||
|
||
pf = sub;
|
||
|
||
result = pf(20, 10);
|
||
printf("%d\n", result); /* 10 */
|
||
|
||
pf = mul;
|
||
|
||
result = pf(20, 10); /* 2000 */
|
||
printf("%d\n", result);
|
||
|
||
pf = div;
|
||
|
||
result = pf(20, 10);
|
||
printf("%d\n", result); /* 2 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyon gösterici dizisi de söz konusu olabilir. Örneğin:
|
||
|
||
void (*pfs[5])(void);
|
||
|
||
Burada pfs her elemanı ""geri dönüş değeri void olan parametresi void"" olan bir fonksiyonu gösteren gösterici dizisidir.
|
||
|
||
Aşağıdaki örnekte bir fonksiyon gösterici dizisine çeşitli fonksiyonların adresleri yerleştirilmiş sonra döngü içerisinde bu fonksiyonlar
|
||
çağrılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int add(int a, int b)
|
||
{
|
||
return a + b;
|
||
}
|
||
|
||
int sub(int a, int b)
|
||
{
|
||
return a - b;
|
||
}
|
||
|
||
int mul(int a, int b)
|
||
{
|
||
return a * b;
|
||
}
|
||
|
||
int div(int a, int b)
|
||
{
|
||
return a / b;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int (*pfs[4])(int, int) = {add, sub, mul, div};
|
||
int result;
|
||
|
||
for (int i = 0; i < 4; ++i) {
|
||
result = pfs[i](20, 10);
|
||
printf("%d\n", result);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon adreslerinin sembolik tür isimleri oluşturulurken * atomu parantez içerisine alınmaktadır. Örneğin:
|
||
|
||
int (*pf1)(int, int);
|
||
void (*pf2)(void);
|
||
|
||
Burada pf1 "int (*)(int, int)" türündendir. pf2 ise "void (*)(void)" türündendir. Örneğin:
|
||
|
||
int square(int a)
|
||
{
|
||
return a * a;
|
||
}
|
||
|
||
Burada square ismi ifade içerisinde kullanıldığında int (*)(int) türündendir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon adres türleri de typedef edilebilmektedir. Örneğin:
|
||
|
||
void (*PF)(void);
|
||
|
||
Burada PF'nin türü "void (*) (void)" biçimindedir. Şimdi bildirimin başına typedef getirelim:
|
||
|
||
typedef void (*PF)(void);
|
||
|
||
Artık PF "void (*)(void)" türünü temsil etmektedir. Örneğin:
|
||
|
||
PF pf;
|
||
|
||
bu bildirim aşağıdaki ile eşdeğerdir:
|
||
|
||
void (*pf)(void);
|
||
|
||
Fonksiyon göstericilerine typedef işlemi yazımı kolaylaştırmaktadır. Örneğin:
|
||
|
||
PF pfs[3];
|
||
|
||
Burada 3 elemanlı, her elemanı void (*)(void) türünden bir fonksiyon adresi tutan dizi tanımlaması yapılmıştır. Yani
|
||
bu tanımlama aşağıdaki ile eşdeğerdir:
|
||
|
||
void (*pfs[3])(void);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int add(int a, int b)
|
||
{
|
||
return a + b;
|
||
}
|
||
|
||
int sub(int a, int b)
|
||
{
|
||
return a - b;
|
||
}
|
||
|
||
int mul(int a, int b)
|
||
{
|
||
return a * b;
|
||
}
|
||
|
||
int div(int a, int b)
|
||
{
|
||
return a / b;
|
||
}
|
||
|
||
typedef int (*PF)(int, int);
|
||
|
||
int main(void)
|
||
{
|
||
PF pfs[4] = {add, sub, mul, div};
|
||
int result;
|
||
|
||
for (int i = 0; i < 4; ++i) {
|
||
result = pfs[i](20, 10);
|
||
printf("%d\n", result);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonun parametresi bir fonksiyon göstericisi olabilir. Örneğin:
|
||
|
||
void foo(void (*pf)(void))
|
||
{
|
||
...
|
||
}
|
||
|
||
Burada foo fonksiyonun parametresi bir göstericidir. Ancak bu gösterici bir fonksiyon göstericisidir. foo fonksiyonu "geri dönüş değeri void
|
||
ve parametresi void" olan bir fonksiyonun adresiyle çağrılmalıdır. Örneğin:
|
||
|
||
void bar(void)
|
||
{
|
||
...
|
||
}
|
||
...
|
||
foo(bar);
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi fonksiyon göstericilerine neden gereksinim duyulmaktadır? İşet bunun temelde iki nedeni vardır:
|
||
|
||
1) Callback fonksiyon mekanizmasını oluşturabilmek.
|
||
2) Fonksiyonları işlevsel olarak genelleştirmek
|
||
|
||
Sistem programlamada "bir olay olduğunda çağrılması istenen fonksiyona callback fonksiyon" denilmektedir. Örneğin GUI uygulamalarında bir düğmeye
|
||
tıklandığında birşey yapılması istenebilir. Ya da örneğin bir dizin içerisinde her dosya bulunduğunda onun üzerinde bir şey yapılması istenebilir.
|
||
Ya da örneğin belli bir zaman dolduğunda bir fonksiyonun çağrılması istenebilir. Tüm bunları sağlayabilmek için "fonksiyon göstericisi" kavramının
|
||
bulunuyor olması gerekir.
|
||
|
||
Aşağıdaki örnekte set_alarm isimli fonksiyon belli bir zamanda verilen fonksiyonu çağırmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <time.h>
|
||
|
||
void set_alarm(time_t target, void (*pf)(void))
|
||
{
|
||
time_t t;
|
||
|
||
for (;;) {
|
||
t = time(NULL);
|
||
if (t == target) {
|
||
pf();
|
||
break;
|
||
}
|
||
}
|
||
}
|
||
|
||
void foo(void)
|
||
{
|
||
printf("Ok\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
time_t t;
|
||
|
||
t = time(NULL) + 10;
|
||
|
||
set_alarm(t, foo);
|
||
|
||
printf("continues..\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon göstericileri "genelleştirme" için de sıkça kullanılmaktadır. Aşağıdaki örnekte for_each isimli fonksiyon int türden bir dizinin adresini,
|
||
onun eleman sayısını ve bir de callback fonksiyon adresini almaktadır. for_each fonksiyonu dizinin her elemanının adresi ile callback fonksiyonu
|
||
çağırmaktadır. Callback fonksiyonun ikinci parametresi çağrımın sonuna gelinip gelinmediğini belirtmektedir. Bu parametre FOREACH_PROGRESS ya da
|
||
FOREACH_END değerlerini almaktadır. Callback fonksiyonun dizi elemanlarının adresleriyle çağrıldığına dikkat ediniz. Bu sayede callback fonksiyon
|
||
dizi elemanları üzerinde değişiklikler yapabilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
enum {
|
||
FOREACH_PROGRESS,
|
||
FOREACH_END
|
||
};
|
||
|
||
void for_each(int *pi, size_t size, void (*pf)(int *, int))
|
||
{
|
||
size_t i;
|
||
|
||
for (i = 0; i < size - 1; ++i)
|
||
pf(&pi[i], FOREACH_PROGRESS);
|
||
|
||
pf(&pi[i], FOREACH_END);
|
||
}
|
||
|
||
void disp(int *pi, int status)
|
||
{
|
||
printf("%d", *pi);
|
||
|
||
putchar(status == FOREACH_PROGRESS ? ' ' : '\n');
|
||
}
|
||
|
||
void square(int *pi)
|
||
{
|
||
*pi = *pi * *pi;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int a[5] = {1, 2, 3, 4, 5};
|
||
|
||
for_each(a, 5, disp);
|
||
for_each (a, 5, square);
|
||
for_each(a, 5, disp);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon göstericilerinin kullanımına diğer bir örnek de standart atexit fonksiyonudur. Bu fonksiyonun prototipi
|
||
<stdlib.h> dosyası içerisinde bulunmaktadır:
|
||
|
||
int atexit(void (*function)(void));
|
||
|
||
Fonksiyon parametre olarak geri dönüş değeri void ve parametresi void olan bir fonksiyonun adresini almaktadır. atexit fonksiyonu kendisine verilen
|
||
fonksiyonları saklar. Program sonlanırken bunları "ters sırada" çağırır. Fonksiyon başarı durumunda 0 değerine başarısızlık durumunda sıfır dışı
|
||
bir değere geri dönmektedir. Fonksiyonun başarısız olmasının tek nedeni fazla sayıda fonksiyonun register ettirilmesi olabilir.
|
||
|
||
atexit fonksiyonun amacı program sonlanırken belli boşaltım işlemlerinin otomatik çağrılan bir fonksiyona yaptırılmasıdır. atexit fonksiyonunun
|
||
kendisine verilen fonksiyonları ters sırada çağırdığına dikkat ediniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
void foo(void)
|
||
{
|
||
printf("foo\n");
|
||
}
|
||
|
||
void bar(void)
|
||
{
|
||
printf("bar\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
atexit(foo);
|
||
atexit(bar);
|
||
|
||
printf("main ends...\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
atexit fonksiyonu callback fonksiyon için parametre almadığından ancak bu fonksiyona verilen callback fonksiyonlar
|
||
global değişkenleri kullanabilir. Fonksiyonun bu bakımdan tasarımı biraz kusurlu gözükmektedir. Fonksiyon bizden
|
||
aynı zamanda callback fonksiyona geçirilecek değeri de parametrleri de alsaydı bu durumda daha faydalı kullanıma
|
||
yol açardı. Bu tür durumlarda callback fonksiyona geçirilecek değerin void gösterici olması anlamlıdır. Çünkü void
|
||
göstericiler genel bir bilgi tutucu olarak kullanılabilirler.
|
||
|
||
Burada belirttiğimiz ana fikri anlatan örnek kodu aşağıda veriyoruz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
#define MAX_ATEXIT 1024
|
||
|
||
void (*g_pfs[MAX_ATEXIT])(void *);
|
||
void *g_ptrs[MAX_ATEXIT];
|
||
int g_count;
|
||
|
||
void myatexit(void (*pf)(void *), void *ptr)
|
||
{
|
||
g_pfs[g_count] = pf;
|
||
g_ptrs[g_count] = ptr;
|
||
++g_count;
|
||
}
|
||
|
||
void myexit(int status)
|
||
{
|
||
for (int i = g_count - 1; i >= 0; --i)
|
||
g_pfs[i](g_ptrs[i]);
|
||
|
||
exit(status);
|
||
}
|
||
|
||
void free_mem(void *pv)
|
||
{
|
||
printf("memory frees...\n");
|
||
|
||
free(pv);
|
||
}
|
||
|
||
void free_some_resource(void *pv)
|
||
{
|
||
int handle = (int)pv;
|
||
|
||
printf("resource frees with %d handle...\n", handle);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int *pi1;
|
||
int *pi2;
|
||
|
||
if ((pi1 = (int *)malloc(sizeof(int))) == NULL)
|
||
myexit(EXIT_FAILURE);
|
||
|
||
myatexit(free_mem, pi1);
|
||
|
||
if ((pi2 = (int *)malloc(sizeof(int))) == NULL)
|
||
myexit(EXIT_FAILURE);
|
||
|
||
myatexit(free_mem, pi2);
|
||
|
||
myatexit(free_some_resource, (void *)123);
|
||
|
||
myexit(EXIT_SUCCESS);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
68. Ders - 21/02/2023 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon adresleri tür dönüştürme operatörüyle bile data adreslerine, data adresleri de fonksiyon adreslerine dönüşürülememktedir. Örneğin:
|
||
|
||
int *pi;
|
||
void (*pf)(void);
|
||
...
|
||
|
||
pf = (void (*)(void))pi; /* geçersiz! */
|
||
|
||
Benzer biçimde örneğin:
|
||
|
||
int *pi;
|
||
void (*pf)(void);
|
||
...
|
||
|
||
pi = (int *)pf; /* geçersiz! */
|
||
|
||
C'de void göstericiler data göstericileridir. Yani biz C'de void göstericiye herhangi türden bir data adresini atayabiliriz. Ancak fonksiyon adresini atayamayız.
|
||
Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
...
|
||
}
|
||
|
||
void *pv;
|
||
|
||
pv = foo; /* geçersiz! */
|
||
|
||
Benzer biçimde void bir adres de herhangi türden bir göstericiye tür dönüştürme operatörü uygulansa bile atanamaz. Zira C'de data adreslerinden fonksiyon adreslerine,
|
||
fonksiyon adreslerinden data adreslerine tür dönüştürmesi yoktur. C'de her türlü fonksiyonun adresini atayabileceğimiz void bir fonksion göstericisi de yoktur.
|
||
Ancak tür dönüştürmesi yoluyla bir fonksiyon adresi başka bir fonksiyon adresine dönüştürülebilmektedir. Örneğin:
|
||
|
||
int add(int a, int b)
|
||
{
|
||
return a + b;
|
||
}
|
||
|
||
void (*pf)(void);
|
||
|
||
pf = add; /* geçersiz! */
|
||
|
||
pf = (void (*)(void))add; /* geçerli */
|
||
|
||
Tabii bu tür durumlarda yazımı kolaylaştırmak için fonksiyon adresleri typedef edilebilir. Örneğin:
|
||
|
||
typedef void (*PF)(void);
|
||
|
||
int add(int a, int b)
|
||
{
|
||
return a + b;
|
||
}
|
||
|
||
PF pf;
|
||
|
||
pf = (PF)add; /* geçerli */
|
||
|
||
C'de bir fonksiyonun adresini almakla doğrudan fonksiyonun ismini kullanmak arasında standartlara göre hiçbir farklılık yoktur.
|
||
Örneğin foo bir fonksiyon ismi olmak üzere foo ifadesi ile &foo ifadesi tamamen eşdeğerdir.
|
||
|
||
Bir fonksiyon adresinin bir data adresine bir adata adresinin fonksiyon adresine dönüştürülmesinin doğrudan bir yolu yoktur.
|
||
Ancak derleyicilerin önemli bir bölümü bu tür dönüştürmelere standartlarca yasak olmasına karşın ses çıkartmamaktadır.
|
||
Çok seyrek olarak bu biçimde işlemlerin yapılması gerektiğinde derleyicilerin bunu kabul ettiğini göreceksiniz.
|
||
Ancak C'de aşağıdaki gibi dolambaçlı bir arkdadan dolaşma yöntemiyle standartlara uygun bir biçimde bir fonksiyon
|
||
göstericisine bir data adresei atanabilmektedir:
|
||
|
||
int a;
|
||
void (*pf)(void);
|
||
|
||
*(void **)&pf = &a;
|
||
|
||
Burada pf bir fonksiyon göstericisidir ve türü int (*)(void) biçimdedir. Ancak biz buun adresini aldığımızda artık
|
||
elde ettiğimiz adres bir fonksiyon değil data adresi olur. Dolayısıyla iki data adresini tür dönüştürme operatörü ile
|
||
dönüştürebiliriz. *(void **)&pf ifadesi void bir gösterici olduğuna göre oraya herhangi bir data nesnesinin adresini
|
||
atayabiliriz. Bu öntem standratlarda geçerli olsa da aslında bir arkadan dolaşma işlemidir. Tabii bunun tersinini de
|
||
yapabiliriz. Örneğin biz void göstericiye doğrudan bir fonksiyonun adresini atayamayız ancak bunu aşağıdaki dolayı
|
||
vir biçimde yapabiliriz:
|
||
|
||
void foo(void);
|
||
void *pv;
|
||
|
||
*(void (**)(void))&pv = foo;
|
||
|
||
Örneğin POSIX sistemlerinde dinamik kütüphanelerin içerisindeki data nesnelerinin ve fonksiyonların adreslerini veren
|
||
dlsym isimli bir fonksiyon vardır. Bu fonksiyonun prototipi şöyledir:
|
||
|
||
void *dlsym(void *handle, const char *name);
|
||
|
||
Fonksiyon bize aslında data adresi de fonksiyon adresi de verebilmektedir. Eğer biz bu fonksiyonla bir fonksiyon adresi elde
|
||
etmek istesek fonksiyon bize fonksiyon adresini bir data adresi gibi vermektedir. gcc ve clang derleyicileri aşağıdaki
|
||
dönüştürmeye kızmıyor olsalar da aslında bu dönüştürme standartlarca geçerli değildir:
|
||
|
||
int (*add)(int, int);
|
||
|
||
add = (int (*)(int, int))dlsym(handle, "add"); /* C'de aslında geçersiz! */
|
||
|
||
İşte biz bu tür durumlarda yukarıda bahsettiğimiz arakadan dolaşmayı uygulayabiliriz:
|
||
|
||
int (*add)(int, int);
|
||
|
||
*(void **)&add = dlsym(handle, "add");
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon göstericileri ile ilgili C'de şöyle bir ayrıntı vardır: Bir fonksiyon göstericisinde parametre parantezlerinin içinin boş bırakılmasıyla oraya
|
||
void yazılması farklı anlamlara gelmektedir. Parametre parantezinin içi boş bırakılırsa bu biçimde tanımlanmış olan fonksiyon göstericilerine
|
||
geri dönüş değeri uygun olmak koşulu ile herhangi bir parametrik yapıya sahip fonksiyonların adresleri atanabilmektedir. Örneğin:
|
||
|
||
void (*pf)();
|
||
|
||
Buradaki pf göstericisine "geri dönüş değeri void olmak üzere herhangi bir parametrik yapıya sahip fonksiyonun adresi" atanabilir.
|
||
Oysa parametre parantezinin içine void yazılırsa bu dururmda göstericiye ancak parametresi olmayan fonksiyonların adresleri atanabilir.
|
||
Örneğin:
|
||
|
||
void (*pf)(void);
|
||
|
||
Burada pf göstericisine "geri dönüş değeri void olan, parametresi olmayan" fonksiyonların adresleri atanabilir. Örneğin:
|
||
|
||
void foo(void)
|
||
{
|
||
...
|
||
}
|
||
|
||
void bar(int a)
|
||
{
|
||
...
|
||
}
|
||
|
||
int tar(int a)
|
||
{
|
||
...
|
||
}
|
||
|
||
void (*pf)(void);
|
||
|
||
pf = foo; /* geçerli */
|
||
pf = bar; /* geçersiz! */
|
||
pf = tar; /* geçersiz! */
|
||
|
||
Ancak örneğin:
|
||
|
||
void (*pf)();
|
||
|
||
pf = foo; /* geçerli */
|
||
pf = bar; /* geçerli */
|
||
pf = tar; /* geçersiz! */
|
||
|
||
Ancak C++'ta parametre parantezinin için boş bırakılmasıyla void yazılması arasında bir farklılık yoktur. İkisi de
|
||
"parametresi olmayan fonksiyonun" adresinin atanabileceği anlamına gelir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Fonksiyon göstericilerinin kullanımına tipik bir örnek qsort isimli standart C fonksiyonudur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir byte'tan uzun olan tamsayı türünden nesnelerin bellekteki yerleşimleri işlemciden işlemciye değişebilmektedir. Buna işlemcinin "endian'lığı (endianness)"
|
||
denilmektedir. Intel işlemcilerinde sayının düşük anlamlı byte'ı düşük adreste olacak biçimde yerleşim uygulanmaktadır. Power PC, SPARC, Alpha
|
||
gibi işlemcilerde sayının yüksek anlamlı byte değeri düşük adreste tutulur. Örneğin:
|
||
|
||
int a = 0x12345678;
|
||
|
||
Intel işlemcilerinde bu int nesne içerisindeki byte'lar şöyle tutulacaktır:
|
||
|
||
78
|
||
56
|
||
34
|
||
12
|
||
|
||
Halbuki PowerPC işlemcilerinde tutulma şöyle olacaktır:
|
||
|
||
12
|
||
34
|
||
56
|
||
78
|
||
|
||
Sayının düşük anlamlı byte değerinin düşük adreste tutulmasına "little endian", sayının yüksek anlamlı byte değerinin düşük adreste tutulmasına ise
|
||
"big endian" yerleşim denilmektedir. Burada "endian" sözcüğü alakasız bir biçimde uydurulmuştur.
|
||
|
||
Bazı işlemciler her iki endian'lığa göre de çalışabilecek biçimde tasarlanmış durumdadır. Örneğin ARM işlemcileri default durumda "little endian"
|
||
çalışmaktadır. Ancak işlemcinin modunu değiştirerek onu "big endian" çalışacak biçime getirebiliriz. Little edian ve big endian tasarım arasında
|
||
bir performans farklılığı yoktur.
|
||
|
||
Endianlık'ta byte bitleri arasında da bir farklılık yoktur. Byte'ların birbirlerine göre dizilimleri arasında farklılık söz konusudur. Biz örneğin bir int nesnenin
|
||
adresini aldığımızda her zaman en düşük adresi elde ederiz. O adresteki byte'a bakarak işlemcimizin litlle endian mı yoksa big endian mı kullandığını anlayabiliriz. Örneğin:
|
||
|
||
int a = 0x12345678;
|
||
unsigned char *pc;
|
||
|
||
pc = (unsigned char *)&a;
|
||
printf("%02X\n", *pc); /* Little endian ise 78, big endian ise 12 görürüz */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 0x12345678;
|
||
unsigned char *pc;
|
||
|
||
pc = (unsigned char *) &a;
|
||
printf("%02X\n", *pc); /* little endian ise 78, big endian ise 12 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Elimizde unsigned short türden ps isimli bir gösterici olsun. Bu gösterici aşağıdaki byte yığınının başlangıç adresini gösteriyor olsun:
|
||
|
||
13
|
||
62
|
||
e3
|
||
4a
|
||
|
||
Burada biz *ps dediğimizde ne elde ederiz? Eğer Little Endian bir sistemse bizim 0x6213 elde etmemiz gerekir. Çünkü bu sistemlerde erişim sırasında da
|
||
düşük adreste düşük anlamlı byte değerinin olduğu varsayılmaktadır. Ancak sistem big endian ise 0x1362 elde ederiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 0x12345678;
|
||
unsigned short *ps;
|
||
|
||
ps = (unsigned short *) &a;
|
||
printf("%04X\n", *ps); /* 0x5678 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte ekrana 78, 56, 34, 12 değerleri çıkacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 0x12345678;
|
||
unsigned char *pc;
|
||
|
||
pc = (unsigned char *) &a;
|
||
|
||
for (int i = 0; i < 4; ++i)
|
||
printf("%02X\n", pc[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Birlikler (unions) yapılara benzer ancak onlardan organizasyon bakımından farklı olan bir veri yapılarıdır. Bir birlik union anatar sözcüğü kullanılarak
|
||
aşağıdaki genel biçim ile bildirilir:
|
||
|
||
union [isim] {
|
||
<birlik eleman bildirimi>
|
||
};
|
||
|
||
Bildirim konusunda birlik ile yapı arasındaki tek fark struct anahtar sözcüğü yerine union anahtar sözcüğünün kullanılmasıdır.
|
||
|
||
nion türünden nesnelerin tanımlanması da benzer biçimde yapılmaktadır. Birlik türleri "union" anahtar sözcüğü ile birlik isminden oluşur.
|
||
Örneğin:
|
||
|
||
union SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
union SAMPLE s;
|
||
|
||
Burada s "union SAMPLE" türündendir. Bir birlğin elemanlarına yine nokta operatörü ile erişilir. Örneğin s.a, s.b ve s.c gibi. Birlikler türünden
|
||
göstericiler olabilir. Yine gösterici ile birlik elemanlarına -> operatörü ile erişilebilir. Örneğin:
|
||
|
||
union SAMPLE s;
|
||
union SAMPLE *ps;
|
||
|
||
ps = &s;
|
||
|
||
union türleri de benzer biçimde typedef edilebilir. Örneğin:
|
||
|
||
typedef union tagSAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
} SAMPLE;
|
||
|
||
Aynı isimli birlik ve yapı aynı faaliyet alanında bildirilemez. Örneğin:
|
||
|
||
union SAMPLE {
|
||
/* ... */
|
||
};
|
||
|
||
struct SAMPLE { /* geçersiz! */
|
||
/* ... */
|
||
};
|
||
|
||
Bir birlik nesnesine ilkdeğer verilebilir. Ancak birliğin yalnızca ilk elemanına değer verilebilir. Örneğin:
|
||
|
||
union SAMPLE {
|
||
int a;
|
||
long b;
|
||
double c;
|
||
};
|
||
|
||
union SAMPLE s = {1, 2, 3}; /* geçersiz! */
|
||
union SAMPLE k = {1}; /* geçerli, değer a elemanına verilmiş */
|
||
|
||
C99 ile eklenen "designator inializer" sentaksı ile birliğin herhangi bir elemanına da ilkdeğer verilebilir. Ancak birden fazla elemana yine ilk değer verilemez.
|
||
Örneğin:
|
||
|
||
union SAMPLE s = {.c = 1}; /* geçerli! */
|
||
union SAMPLE k = {.b = 1, 2}; /* geçersiz! */
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Birlikler C'de çok seyrek kullanılmaktadır. Birliklerle yapılar arasındaki tek önemli fark elemanların yerleştirilmesi ile ilgilidir.
|
||
Bir birlik elemanları çakışık yerleştirilmektedir. Yani birliğin her elemanı aynı adresten itibaren çakışık bir biçimde yerleştirilmektedir. Bu nedenle
|
||
bir birlik nesnesi için birliğin en büyük elemanı kadar yer ayrılır. Örneğin:
|
||
|
||
union SAMPLE {
|
||
char a;
|
||
short b;
|
||
int c;
|
||
double d;
|
||
};
|
||
|
||
union SAMPLE s;
|
||
|
||
Burada s'in elemanları için ayrı yerler ayrılmamaktadır. Bu birliğin en büyük elemanı double türdendir. O da 8 byte'tır. O halde bu birlik nesnesi için
|
||
8 byte yer ayrılacaktır. Bu 8 byte'ın ilk byte'ı a elemanı, ilk iki byte'ı b elemanı, ilk dört byte'ı c elemanı ve ilk 8 byte'ı d elemanı olacaktır.
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
union SAMPLE {
|
||
char a;
|
||
short b;
|
||
int c;
|
||
double d;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
union SAMPLE s;
|
||
|
||
printf("%zu\n", sizeof(s)); /* 8 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
Şüphesiz bir birliğin bir elemanına bir değer atadığımızda diğer elemanların değerlerini de değiştirmiş oluruz. Bu durumda birliğin yalnızca belli bir elemanı
|
||
belli bir süreçte kullanılmalıdır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Birlikler iki nedenle kullanılmaktadır:
|
||
|
||
1) Bir grup bilgiden yalnızca birinin kullanıldığı durumlarda yer kazancı sağlamak için.
|
||
2) Parçalardan bütünü oluşturmak ve bütünü parçalarına ayırmak için.
|
||
|
||
Bir grup bilgiden yalnızca birine gereksinim duyulduğu durumlar seyrektir. Örneğin bir kişinin iletişim bilgisi "telefon numarası" ya da "e-posta adresi"
|
||
ya da "TC kimlik numarası" olabilir. Bu bilgilerin hepsinin bulunmasına gerek olmayabilir. Bunlardan yalnızca birinin bulunması yeterli olabilir. Örneğin:
|
||
|
||
struct PERSON {
|
||
char name[64];
|
||
int no;
|
||
union {
|
||
char telno[11];
|
||
char tcid[12];
|
||
char email[64];
|
||
} cinfo;
|
||
};
|
||
|
||
struct PERSON per;
|
||
|
||
Burada per nesnesinin cinfo elemanı bir birliktir. Bu birliğin elemanları çakışık yerleştirilecektir. Çünkü buradaki bilgilerden yalnızca birinin
|
||
bulunması yeterlidir. Pekiyi biz per nesnesi içerisindeki cinfo nesnesinin hangi elemanın set edilmiş olduğunu nasıl anlayabiliriz? Bir birliğin hangi
|
||
elemanının kullanılıyor olduğunu anlamanın pratik bir yolu yoktur. O zaman bu bilgiyi de yapının içerisinde tutmak gerekir:
|
||
|
||
enum { CINFO_TELNO, CINFO_TCID, CINFO_EMAIL };
|
||
|
||
struct PERSON {
|
||
char name[64];
|
||
int no;
|
||
int cinfo_flag;
|
||
union {
|
||
char telno[11];
|
||
char tcid[12];
|
||
char email[64];
|
||
} cinfo;
|
||
};
|
||
|
||
void disp_person(struct PERSON *per)
|
||
{
|
||
printf("Adi Soyadı: %s\n", per->name);
|
||
printf("No: %d\n", per->no);
|
||
|
||
switch (per->cinfo_flag) {
|
||
case CINFO_TELNO:
|
||
printf("Tel No: %s\n", per->cinfo.telno);
|
||
break;
|
||
case CINFO_TCID:
|
||
printf("TC Kimlik: %s\n", per->cinfo.tcid);
|
||
break;
|
||
case CINFO_EMAIL:
|
||
printf("E-Posta: %s\n", per->cinfo.email);
|
||
break;
|
||
}
|
||
}
|
||
...
|
||
struct PERSON per = { "Ali Serce", 123, CINFO_EMAIL, {.email = "aslank@csystem.org"} };
|
||
|
||
disp_person(&per);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
|
||
enum { CINFO_TELNO, CINFO_TCID, CINFO_EMAIL };
|
||
|
||
struct PERSON {
|
||
char name[64];
|
||
int no;
|
||
int cinfo_flag;
|
||
union {
|
||
char telno[11];
|
||
char tcid[12];
|
||
char email[64];
|
||
} cinfo;
|
||
};
|
||
|
||
void disp_person(struct PERSON *per)
|
||
{
|
||
printf("Adi Soyadı: %s\n", per->name);
|
||
printf("No: %d\n", per->no);
|
||
|
||
switch (per->cinfo_flag) {
|
||
case CINFO_TELNO:
|
||
printf("Tel No: %s\n", per->cinfo.telno);
|
||
break;
|
||
case CINFO_TCID:
|
||
printf("TC Kimlik: %s\n", per->cinfo.tcid);
|
||
break;
|
||
case CINFO_EMAIL:
|
||
printf("E-Posta: %s\n", per->cinfo.email);
|
||
break;
|
||
}
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct PERSON per = { "Ali Serce", 123, CINFO_EMAIL, {.email = "aslank@csystem.org"} };
|
||
|
||
disp_person(&per);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Parçalardan bütünü oluşturmak ve bütünü parçalara ayrımak için de birlikler kullanılabilmektedir. Aşağıdaki birliğe dikkat ediniz:
|
||
|
||
#include <stdint.h>
|
||
|
||
union DWORD {
|
||
uint32_t dword;
|
||
uint8_t bytes[4];
|
||
};
|
||
|
||
union DWORD x;
|
||
|
||
Burada DWORD isimli birliğin iki elemanı vardır: dword ve bytes. Bunlar çakışık bir biçimde yerleştirilecektir. O halde biz x.dword elemanına 4 byte
|
||
yerleştirdiğimizde bunların parçalarını x.bytes[0], x.bytes[1], x.bytes[2] ve x.bytes[3] elemanlarından alabiliriz. Tabii işlemcinin little endian
|
||
ya da big endian olup olmadığına göre parçaların sırası da değişebilecektir.
|
||
|
||
Aşağıdaki örnekte biz birliğin dword elemanına 4 byte'lık bir bilgi yerleştirip onları tek tek birliğin bytes elemanından elde ettik. Sonra da bu işlemin
|
||
tersini yaptık.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
|
||
union DWORD {
|
||
uint32_t dword;
|
||
uint8_t bytes[4];
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
union DWORD x;
|
||
|
||
x.dword = 0x12345678;
|
||
|
||
printf("%02X %02X %02X %02X\n", x.bytes[0], x.bytes[1], x.bytes[2], x.bytes[3]); /* 78 56 34 12 */
|
||
|
||
for (int i = 0; i < 4; ++i)
|
||
printf("%02X ", x.bytes[i]);
|
||
printf("\n");
|
||
|
||
x.bytes[0] = 0x10;
|
||
x.bytes[1] = 0x20;
|
||
x.bytes[2] = 0x30;
|
||
x.bytes[3] = 0x40;
|
||
|
||
printf("%08lX\n", (unsigned long)x.dword); /* 40302010 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aynı şeyi birliğin bir elemanını yapı yaparak da sağlayabiliriz. Örneğin:
|
||
|
||
struct BYTES {
|
||
uint8_t b0;
|
||
uint8_t b1;
|
||
uint8_t b2;
|
||
uint8_t b3;
|
||
};
|
||
|
||
union DWORD {
|
||
uint32_t dword;
|
||
struct BYTES bytes;
|
||
};
|
||
|
||
union DWORD x;
|
||
|
||
Burada DWORD x nesnesinin dword ve bytes elemanları yine çakışık yerleştirilecektir. Yapı elemanlarının ilk yazılan eleman düşük adreste olacak biçimde
|
||
ardışıl yerleştirildiğini biliyoruz. Bu durumda biz x.dword elemanına değer atadığımızda onun parçalarını x.bytes.b0, x.bytes.b1, x.bytes.b2 ve
|
||
x.bytes.b3 ifadeleriyle elde edebiliriz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
|
||
struct BYTES {
|
||
uint8_t b0;
|
||
uint8_t b1;
|
||
uint8_t b2;
|
||
uint8_t b3;
|
||
};
|
||
|
||
union DWORD {
|
||
uint32_t dword;
|
||
struct BYTES bytes;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
union DWORD x;
|
||
|
||
x.dword = 0x12345678;
|
||
|
||
printf("%02x %02X %02X %02X\n", x.bytes.b0, x.bytes.b1, x.bytes.b2, x.bytes.b3); /* 78 56 34 12 */
|
||
|
||
x.bytes.b0 = 0x10;
|
||
x.bytes.b1 = 0x20;
|
||
x.bytes.b2 = 0x30;
|
||
x.bytes.b3 = 0x40;
|
||
|
||
printf("%08lx\n", (unsigned long)x.dword); /* 40302010 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
69. Ders - 23/02/2023 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bileşik sabitler (compound literals) konusu C'ye C99 ile birlikte eklenmiştir. Ancak C++ bu özelliği benimsememiştir. Bileşik sabitler
|
||
yapılar ve dizilerin ifade içerisinde oluşturulmasına olanak sağlamaktadır. Örneğin ekrandaki iki nokta arasında doğru çizen
|
||
aşağıdaki gibi bir fonksiyon olsun:
|
||
|
||
struct POINT {
|
||
int x;
|
||
int y;
|
||
};
|
||
|
||
void draw_line(struct POINT pt1, struct POINT pt2)
|
||
{
|
||
/* ... */
|
||
}
|
||
|
||
Normal olarak biz bu fonksiyonu struct POINT türünden iki nesne oluşturarak çağırırız. Örneğin:
|
||
|
||
struct POINT pt1 = {3, 4}, pt2 = {13, 21};
|
||
|
||
draw_line(pt1, pt2);
|
||
|
||
Böylesi çağırmalarda işin başında nesnelerin tanımlanma zorunluğu kodu kalabalık hale getirmektedir. Benzer biçimde bir yapı nesnesinin elemanlarına değer atamanın da
|
||
C90'da ilkdeğer vermenin dışında pratik bir yolu yoktur. Örneğin:
|
||
|
||
struct DATE{
|
||
int day, month, year;
|
||
};
|
||
struct DATE date;
|
||
...
|
||
|
||
date.day = 10;
|
||
date.month = 12;
|
||
date.year = 2009;
|
||
|
||
Halbuki bu işlemin tek satırda yapılması yalın bir görünüm oluşturacaktır. İşte bileşik sabitler bu tür durumlar için yazım kolaylığı sağlamak amacıyla
|
||
dile eklenmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Her ne kadar burada açıklayacağımız konuya C standartlarında "bileşik sabitlşer (compound literals)" denildiyse de aslında semantik olarak
|
||
burada ele alacağımız özelliğin sabitle bir ilgisi yoktur. Zaten standartlarda da konu "Expressions" bölümünde ele alınmıştır. Bileşik sabit
|
||
oluşturmanın genel biçimi şöyledir:
|
||
|
||
(<tür>) { <eleman_listesi>}
|
||
|
||
Aslında bu işlem bir tür dönüştürme işlemi gibidir. Ancak bu işlemin daha önce gördüğümüz tür dönüştürme operatöründen farkı
|
||
yanında küme parantezlerinin olmasıdır. Biz bir dizi türüne ya da yapı türüne tür dönüştürmesi yapamayız. Ancak bileşik sabitlerle bu durum
|
||
mümkün hale getirilmiştir. Örneğin:
|
||
|
||
struct POINT {
|
||
int x;
|
||
int y;
|
||
};
|
||
|
||
void draw_line(struct POINT pt1, struct POINT pt2)
|
||
{
|
||
/* ... */
|
||
}
|
||
...
|
||
|
||
draw_line((struct POINT){3, 4}, (struct POINT){13, 21});
|
||
|
||
Burada biz adeta sabit formunda yapı nesneleri oluşturmuş olduk. Yani o anda bir yapı nesnesi yaratıp onu fonksiyona yolladık. Örneğin:
|
||
|
||
struct POINT pt;
|
||
...
|
||
pt = (struct POINT) {10, 20};
|
||
|
||
Burada da yapı elemanlarına tek satırda atama yapmış olduk.
|
||
|
||
Bileşik sabitler her ne kadar isminde sabit olsa da aslında nesne belirtmektedir. Biz bir bileşik sabitin adresini de alabiliriz. Örneğin:
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
void disp_date(struct DATE *pdate)
|
||
{
|
||
printf("%02d/%02d/%04d\n", pdate->day, pdate->month, pdate->year);
|
||
}
|
||
...
|
||
int main(void)
|
||
{
|
||
disp_date(&(struct DATE) { 10, 12, 2020 });
|
||
|
||
return 0;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
struct DATE {
|
||
int day, month, year;
|
||
};
|
||
|
||
void disp_date(struct DATE *pdate)
|
||
{
|
||
printf("%02d/%02d/%04d\n", pdate->day, pdate->month, pdate->year);
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
struct DATE d;
|
||
|
||
disp_date(&(struct DATE) { 10, 12, 2020 });
|
||
|
||
d = (struct DATE){21, 12, 2008};
|
||
disp_date(&d);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dizi türü için de bileşik sabit oluşturabilmektedir. Bu işlemin genel biçimi de şöyledir:
|
||
|
||
(tür [uzunluk]) {değer_listesi}
|
||
|
||
Eğer dizi uzunluğu belirtiliyorsa sabit ifadesi biçiminde olması gerekir. Tabii küme parantezleri içerisindeki değer listesinde
|
||
sabit ifadesi kullanmak zorunlu değildir.
|
||
|
||
Örneğin:
|
||
|
||
disp((int[5]) { 10, 20, 30, 40, 50 }, 5);
|
||
|
||
Burada 5 elemanlı int bir dizi bileşik sabit yöntemiyle yaratılmış ve başlangıç adresi disp fonksiyonuna geçilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
void disp(const int *pi, size_t size)
|
||
{
|
||
for (size_t i = 0; i < size; ++i)
|
||
printf("%d ", pi[i]);
|
||
printf("\n");
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
disp((int[5]) { 10, 20, 30, 40, 50 }, 5);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bileşik sabit yöntemiyle dizi nesnesi yarattığımızda o bileşik sabit artık aynı zamanda o yaratılan dizinin başlangıç adresi anlamına gelmektedir.
|
||
Yani biz bu biçimde yarattığımız dizileri aynı türden göstericilere atayabiliriz. Örneğin:
|
||
|
||
int *pi;
|
||
|
||
pi = (int[5]) {10, 20, 30, 40, 50};
|
||
|
||
Burada pi yine yaratılmış olan 5 elemanlı int diziyi gösterecektir. Tabii aslında bileşik sabit yolu ile dizi oluştururken dizi uzunluğunun
|
||
belirtilmesine gerek yoktur. Örneğin:
|
||
|
||
pi = (int[]) {10, 20, 30, 40, 50};
|
||
|
||
Derleyici bu durumda küme parantezleri içerisindeki eleman sayısı uzunluğunda diziyi yaratacaktır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bileşik sabitler sol taraf değeri oluşturmaktadır. Yani onların değerleri değiştirilebilir. Örneğin:
|
||
|
||
int *pi = (int[]){10, 20, 30};
|
||
|
||
*pi = 100; /* geçerli */
|
||
|
||
Bileşik sabitler yerel bir blokta oluşturulmuşlarsa tıpkı yerel nesneler gibi, global biçimde oluşturulmuşsa global nesneler
|
||
gibi ömre sahip olurlar. Yani bileşik sabitler eğer global düzeyde oluşturulmuşsa statik ömürlü olurlar. Program çalışma başladığında
|
||
yaratılırlar program sonlanana kadar yaşamaya devam ederler. Bileşik sabitler eğer bir bloğun içerisinde oluşturulmuşlarsa ömürleri
|
||
oluşturuldukları yerden blok sonuna kadarki akış içerisinde devam eder.
|
||
|
||
Bu durumda bileşik sabitler sabitin kullanıldığı yerde isimsiz bir nesne oluşturulmasına yol açmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int *pi;
|
||
|
||
pi = (int[]){10, 20, 30};
|
||
|
||
*pi = 100; /* geçerli */
|
||
|
||
for (int i = 0; i < 3; ++i)
|
||
printf("%d\n", pi[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonun yerel düzeyde yaratılmış bir bileşik sabitin adresiyle geri dönmesi tanımsız davrana yol açacaktır.
|
||
Örneğin:
|
||
|
||
int *foo(void)
|
||
{
|
||
return (int[]) { 1, 2, 3, 4, 5 }; /* tanımsız davranış */
|
||
}
|
||
|
||
Tabii derleyiciler bileşik sabit gördüklerinde optimizasyon yaparak gereksiz işlemleri elimine edebilmektedir. Örneğin:
|
||
|
||
struct POINT pt;
|
||
...
|
||
|
||
pt = (struct POINT) {10, 20};
|
||
|
||
Burada aslında aynı türden iki yapı nesnesi birbirine atanıştır. Ancak derleyici bileşik sabit biçiminde oluşturulmuş olan
|
||
nesneyi hiç yaratmadan 10 ve 20 değerlerini doğrudan pt nesnesinin elemanlarına yerleştirebilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bugün kullandığımız pek çok işlemcide yerel değişkenler iç blokta tanımlanmış olsa bile sanki fonksiyonun ana bloğunun başında tanımanmış gibi
|
||
derleyiciler kod üretmektedir. Ancak C standartlarında böyle bir zorunluluk yoktur. Örneğin aşağıdaki kodda aslında döngünün her yinelenmesinde
|
||
iç bloktaki a nesnesinin yeniden yaratılıp yok edilmesi beklenir. Ancak yukarıda da belirttiğimiz gibi bugünkü mimarilerde genellikle
|
||
(her zaman değil) derleyiciler aslında bu nesneyi ana blokta yaratıp fonksiyon sonlanana kadar muhafaza ederler. Dolayısıyla aşağıdaki kodda
|
||
ekrana hep aynı adresin yazıldığına şaşırmayınız:
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
int a = 10;
|
||
|
||
printf("%d, %p\n", %d, &a);
|
||
}
|
||
|
||
Her iç blokta nesneyi yeniden yaratıp yok etmenin maliyeti daha yüksektir. Derleyiciler optimizasyon gereği onu ana bloğun başında yaratmayı tercih
|
||
etmektedir. Tabii ne olursa olsun biz bu örnekteki a değişkenini tanımlandığı bloğun dışnda yine kullanamayız. Ancak yukarıdaki örnekte döngünün
|
||
hger yinelenmesinde a nesnesine atama işlemi yine yapılmaktadır.
|
||
|
||
Bileşik sabitlerde de aynı durum söz konusudur. C'deki resmi anlatıma göre bileşik sabitler programın akışı sabitin yaratıldığı noktaya geldiğinde yaralıp
|
||
blok sonlanınca yok edilmektedir. Ancak derleyiciler yine eğer gözlemlenebilecek bir değişiklik oluşmnayacaksa bu yaratımı blok başlarında birt kez
|
||
yapabilmektedir. Örneğin:
|
||
|
||
{
|
||
int *pi;
|
||
|
||
REPEAT:
|
||
pi = (int[]) {10, 20, 30};
|
||
...
|
||
goto REPEAT;
|
||
}
|
||
|
||
Burada aynı faaliyet alanı içerisinde goto deyimi ile yukarıya atlanmıştır. Ancak derleyiciler optimizasyon uygulayarak yaratımı
|
||
fonksiyon başlarında yapabilirler. Fakat her yinelemede yine diziye bu elemanlar atanacaktır. Aynı durum döngü içerisinde bileşik sabit
|
||
oluştururken de karşımıza çıkabilir. Örneğin:
|
||
|
||
int *pi;
|
||
...
|
||
for (int i = 0; i < 10; ++i) {
|
||
pi = (int[]) {10, 20, 30};
|
||
...
|
||
}
|
||
|
||
Tabii burada yine derleyicileerin optimizasyon mekanizması devreye girebilecektir. Eğer bu döngü içerisinde söz konusu dizide hiçbir değişiklik yapılmıyorsa
|
||
derleyici de her defasında bu atamayı yapmayabilir.
|
||
|
||
Tabii bileşik sabitler const biçimde oluşturulursa bu durumda derleyici de optimizasyon yaparak onları ilkdeğerlerini her defasında vermeyebilir. Örneğin:
|
||
|
||
const int *pi;
|
||
...
|
||
for (int i = 0; i < 10; ++i) {
|
||
pi = (const int[]) {10, 20, 30};
|
||
...
|
||
}
|
||
|
||
Burada derleyicinin döngünün her yinelenmesinde dizi elemanlarına bu atamayı yapmayabilir. Nasıl olsa const bir dizinin elemanları değiştirilemez ve
|
||
onların const olmayan göstericiler yoluyla değiştirilmesi de "tanımsız davranışa (undefined behavior)" yol açmaktadır. Dolayısıyla yukarıdaki kodu derleyici
|
||
aşağıdaki eşdeğerlikle derleyebilir:
|
||
|
||
const int *pi;
|
||
const int temp[] = {1, 2, 3}
|
||
...
|
||
for (int i = 0; i < 10; ++i) {
|
||
pi = temp;
|
||
...
|
||
}
|
||
|
||
Örneğin benzer biçimde:
|
||
|
||
for (int i = 0; i < 10; ++i) {
|
||
foo((const int[]) {1, 2, 3});
|
||
...
|
||
}
|
||
|
||
Burada derleyici muhtemelen döngünün her yinelenmesinde yeniden diziyi yaratıp dizi elemanlarına atama yapmayacaktır.
|
||
|
||
Aşağıdaki kodu çalıtırıp test ediniz:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < 3; ++i) {
|
||
int *pi = (int[]){1, 2, 3};
|
||
|
||
for (int k = 0; k < 3; ++k)
|
||
printf("%d ", pi[k]);
|
||
printf("\n");
|
||
|
||
pi[0] = 10;
|
||
pi[1] = 20;
|
||
pi[2] = 30;
|
||
|
||
for (int k = 0; k < 3; ++k)
|
||
printf("%d ", pi[k]);
|
||
printf("\n");
|
||
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
Kodu çalıştırdığımızda aşağıdaki gibi bir çıktı elde edilmiştir:
|
||
|
||
1 2 3
|
||
10 20 30
|
||
1 2 3
|
||
10 20 30
|
||
1 2 3
|
||
10 20 30
|
||
|
||
Özetle aşağıdaki gibi bir kod söz konusu olsun:
|
||
|
||
pi = (int[]){1, 2, 3};
|
||
|
||
Aslında bu işlemin eşdeğeri şyledir:
|
||
|
||
int temp[] = {1, 2, 3};
|
||
pi = temp;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında bileşik sabitler dizi ve yapıların dışında normal türler için de oluşturulabilmektedir. Örneğin:
|
||
|
||
int *pi;
|
||
...
|
||
pi = &(int){10}; /* geçerli */
|
||
|
||
Biz burada int türden bir nesne yaratıp onun adresini pi'ye atamış olduk. Bileşik sabitlerin aslında sabit oluşturmadığına bir nesne oluşturduna
|
||
dikkat ediniz. Yukarıdaki işlemin eşdeğeri aşağıdaki gibi düşünülebilir:
|
||
|
||
int *pi;
|
||
...
|
||
int temp = 10;
|
||
pi = &temp;
|
||
|
||
C'de normal türlere de küme parantezleri ile ilkdeğer verilebildiğini anımsayınız:
|
||
|
||
int a = {10}; /* geçerli */
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bileşik sabitlerde yine C99 ile birlikte gelen "designated initializer" sentaksı kullanılabilmektedir. Örneğin:
|
||
|
||
pi = (int[10]){[5] = 100,[7] = 200, 300};
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int *pi;
|
||
|
||
pi = (int[10]){[5] = 100,[7] = 200, 300};
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d \n", pi[i]);
|
||
printf("\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de ismine "bit operatörleri (bitwise operators)" denilen bir grup operatör vardır. Bu operatörler sayıları bütünsel olarak değil bitsel olarak
|
||
işleme sokmaktadır. Başka bir deyişle bit operatörleri sayıların karşılıklı bitlerini işleme sokarlar. Bu tür bitsel işlemler sistem programlamada
|
||
yoğun olarak kullanılmaktadır. C'nin bit operatörleri şunlardır:
|
||
|
||
~ (Bit NOT)
|
||
& (bit AND)
|
||
^ (Bit EXOR)
|
||
| (bit OR)
|
||
<< (Sola öteleme)
|
||
>> (Sağa öteleme)
|
||
|
||
Bit operatörlerinin öncelik tablosundaki konumları şöyledir:
|
||
|
||
() [] . -> Soldan-Sağa
|
||
+ - ++ -- ! & * (tür) sizeof ~ Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
<< >> Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
& Soldan-Sağa
|
||
^ Soldan-Sağa
|
||
| Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
=, +=, /=, *=,... Sağdan-Sola
|
||
, Soldan-Sağa
|
||
|
||
Burada sola ve sağa öteleme operatörlerinin aritmetik operatörlerle karşılaştırma operatörlerinin arasında bulunduğuna &, ^ ve | operatörlerinin
|
||
ise karşılaştırma operatörlerinden daha düşük öncelikte bulunduğuna dikkat ediniz. Bu durum C'de eleştirilmektedir. Örneğin Python'da &, | ^ operatörleri
|
||
karşılaştırma operatörlerinden daha önceliklidir. ~ operatörü tek operandlı bir operatör olduğu için öncelik tablosunun ikinci düzeyinde bulunmaktadır.
|
||
|
||
Bit operatörlerinin hepsini operand'ları tamsayı türlerine ilişkin olmak zorundadır. Yani bu operatörlerin operandları float ve double türden, bir adres
|
||
türünden olamaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir sayının en düşük anlamlı biti 0 olmak üzere her bitinin bir pozisyon numarası vardır. Örneğin:
|
||
|
||
0101 1000
|
||
|
||
Bu ikilik sistemdeki sayının bit pozisyon numaralerı şöyledir:
|
||
|
||
7654 3210
|
||
0101 1000
|
||
|
||
Biz aşağıdaki örnekllerde "sayının n numaralı biti" dediğimizde numaralandırmanın böyle yapılmış olduğunuı varsayınız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
& operatörü iki operand'lı araek bit operatörüdür. Sayıların karşılıklı bitlerini AND işlmine sokar. Yine işlem öncesi otomatik tür dönüştürmesi
|
||
(int türüne yükseltme kuralı da dahil olmak üzere) uygulanmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0x3F; /* 0011 1111 */
|
||
unsigned char b = 0x77; /* 0111 0111 */
|
||
unsigned char c;
|
||
|
||
c = a & b; /* 0011 0111 = 0x37 */
|
||
printf("%02X\n", c); /* 37 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bit düzeyindeki AND işleminde 1'in etkisiz eleman olduğuna dikkat ediniz. 1 ile 0 AND yapılırsa 0, 1 ile 1 AND yapılırsa
|
||
1 elde edilmektedir. Benzer biçimde bit AND işleminde 0 yutn elemandır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Elimizde bir sayı olsun ve biz bu sayının n'inci bitinin durumunu belirlemeye çalışalım. Klasik yöntem şudur: Sayı tüm bitleri 0 olan n'inci biti
|
||
1 olan bir sayıyla bit AND işlemine sokulur. Bunun sonucunda 0 değeri elde edilirse n'inci bit 0, sıfır dışı bi değer elde edilirse n'inci bit 1'dir.
|
||
Tüm bitleri 0 olan yalnızca tek biti 1 olan bu tür değerlere halk arasında "bit mask değerler" ya da "one hot" değerler denilmektedir.
|
||
|
||
Aşağıdaki örnekte sayının 5'inci bitinin durumu elde edilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0x3F; /* 0011 1111 */
|
||
|
||
if (a & 0x20)
|
||
printf("5. Bit 1\n");
|
||
else
|
||
printf("5. Bit 0\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir sayının diğer bitlerine dokunmadan n'inci bitini nasıl 0 yapabiliriz? Bunun klasik yolu şöyledir: Sayı tüm bitleri 1 olan n'inci biti 0 olan
|
||
bir sayıyla bit AND işlemine sokulur. 0 değeri AND işleminde yutan elemandır, 1 ise etkisiz elemandır.
|
||
|
||
Aşağıdaki örnekte a içerisindeki sayının diğer bitlerine dokunmadan 4'üncü biti 0 yapılmak istenmiştir. Burada a değeri 0xEF ile bit AND işlemine
|
||
sokulmuştur. 0xEF değeri 1110 1111 biçimindedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0x3F; /* 0011 1111 */
|
||
|
||
a = a & 0xEF; /* 0xEF => 1110 1111 */
|
||
printf("%02X\n", a); /* 0010 1111 = 0x2F */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
70. Ders - 28/02/2023 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
& ve | operatörlerinin "kısa devre (short circuit)" özelliği yoktur. Örneğin:
|
||
|
||
a = foo() | bar() & tar();
|
||
|
||
Burada her zaman bu üç fonksiyon da çağrılacaktır. (Halbuki mantıksal AND ve OR operatörlerinde bunun bir garantisi yoktur.)
|
||
Burada önce her zaman & operatörü yapılır. Sonra buradan elde edilen değer | operatörğne sokulur. Yani işlemler şu sırada
|
||
yapılacaktır:
|
||
|
||
İ1: foo()
|
||
İ2: bar()
|
||
İ3: tar()
|
||
İ4: İ2 & İ3
|
||
İ5: İ1 | İ4
|
||
İ6: İ5
|
||
|
||
Aşağıda bit AND operatörü ile mantıksal AND operatörünün kısa devre davranışına ilişkin bir örnek verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int foo(void)
|
||
{
|
||
printf("foo\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
int bar(void)
|
||
{
|
||
printf("bar\n");
|
||
|
||
return 0x3F;
|
||
}
|
||
|
||
int tar(void)
|
||
{
|
||
printf("tar\n");
|
||
|
||
return 0x7A;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int result;
|
||
|
||
result = foo() & bar() & tar();
|
||
|
||
printf("%d\n", result);
|
||
|
||
result = foo() && bar() && tar();
|
||
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir tamsayının tek mi çift mi olduğunu anlamak için genellikle o sayının 2'ye bölümünden elde edilen kalana bakılmaktadır. Aslında işlemcilerin çoğunda
|
||
çarpma, bölme ve bölümden elde edilen kalan işlemleri nano düzeyde bit işlemlerine göre daha fazla zaman almaktadır. Bir sayının tek ya da
|
||
çift olduğunu anlamanın diğer bir yolu da sayıyı 1 ile (1 sayısının tüm bitleri 0, en düşük anlamlı biti 1'dir) bit AND işlemine sokmaktır. Bunun sonucu
|
||
ya 0 ya da 1 olarak elde edilir. Tabii bit operatörlerinin yalnızca tamsayı türleriyle kullanılabildiğine dikkat ediniz. Eğer örneğin biz bir double sayı
|
||
için kontrol yapmak isteseydik ikiye bölümünden elde edilen kalan yöntemine başvururduk.
|
||
|
||
Aşağıda bu yöntemin kullanımına bir örnek verilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("Bir sayi giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
printf(a & 1 ? "tek\n" : "cift\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bit OR işleminde 0 etkisiz elemandır. 1 ise yutan elemandır. Yani örneğin 0 biti ile 0 biti bir OR işlemine sokulursa 0 biti,
|
||
0 bti ile 1 biti bit OR işlemine sokulursa 1 biti elde edilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bit OR işlemi için | operatörü kullanılmaktadır. Bu operatör sayının karşılıklı bitlerini OR işlemine sokmaktadır. Örneğin:
|
||
|
||
unsigned char a = 0xA2; /* 1010 0010 */
|
||
unsigned char b = 0x34; /* 0011 0100 */
|
||
unsigned char c;
|
||
|
||
c = a | b;
|
||
|
||
Bit OR işleminin de kısa devre özelliği yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0xA2; /* 1010 0010 */
|
||
unsigned char b = 0x34; /* 0011 0100 */
|
||
unsigned char c;
|
||
|
||
c = a | b;
|
||
|
||
printf("%02X\n", c); /* 1011 0110 = 0xB6 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir sayının diğer bitlerine dokunmadan belli bir pozisyondaki bitini 1 nasıl yapabiliriz? Sayı bütün bitleri 0, ilgili biti 1 olan bir sayı ile
|
||
bit düzeyinde OR işlemine sokulur.
|
||
|
||
Aşağıdaki örnekte a içerisindeki sayının diğer bitlerine dokunmadan 4'üncü biti 1 yapılmak istenmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0xA7; /* 1010 0111 */
|
||
|
||
a = a | 0x10; /* 0001 0000 */
|
||
printf("%02X\n", a); /* 1011 0111 = 0xB7 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıda da belirttiğimiz gibi bit AND operatörü bit OR peratöründen dagh yüksek önceliklidir. Örneğin:
|
||
|
||
a = foo() | bar() & tar();
|
||
|
||
Burada önce bit AND operatörü yapılır daha sonra bit OR operatörü yapılır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de karşılaştırma operatörlerinin bit AND, bit OR ve bit EXOR operatörlerinden daha öncelikli olduğuna dikkat ediniz. Bu durum maalesef programcılar tarafından
|
||
yanlış yorumlanıp hatalı kod oluşumuna yol açabilmektedir. Örneğin:
|
||
|
||
if (a & 1 == 0) /* dikkat! */
|
||
printf("cift\n");
|
||
else
|
||
printf("tek\n");
|
||
|
||
Burada a & 1 işleminin sonucu 0 iler karşılaştırılmaz. Önce 1 ile 0 karşılaştırılıp bunun sonucu a ile AND işlemine sokulmaktadır. Halbuki programcı büyük olasılıkla
|
||
a ile 1 değerini bi AND işlemine sokup sonucun 0 olup olmadığını kontrol etmek istemiştir. Burada paraztez kullanmak gerekir:
|
||
|
||
if ((a & 1) == 0)
|
||
printf("cift\n");
|
||
else
|
||
printf("tek\n");
|
||
|
||
Pek çok C derleyicisi bu tür durumlarda bir mesajla programcıyı uyarmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a;
|
||
|
||
printf("Bir sayi giriniz:");
|
||
scanf("%d", &a);
|
||
|
||
if ((a & 1) == 0)
|
||
printf("cift\n");
|
||
else
|
||
printf("tek\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
EXOR işlemi operand'lar aynı ise 0 değerini, farklı ise 1 değerini veren işlemdir. (Exor sözcüğü "exclusive or" sözcüklerinden kısaltılarak uydurulmuşur.
|
||
"exclusive" dışlayan anlamına gelmektedir.) C'de EXOR işlemi ^ operatöryle temsil edilmektedir. Öneğin:
|
||
|
||
0101 1011
|
||
0111 1101
|
||
|
||
Bu iki sayıyı EXOR işlemine sokarsak şu binary dizilimi elde ederiz:
|
||
|
||
0010 0110
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0x5B; /* 0101 1011 */
|
||
unsigned char b = 0x7D; /* 0111 1101 */
|
||
unsigned char c;
|
||
|
||
c = a ^ b; /* 0010 0110 */
|
||
|
||
printf("%02X\n", c); /* 26 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
0 EXOR işleminde etkisiz elemandır. Ancak 1 karşı tarafın tersini verir. Örneğin 0 biti ile 0 biti EXOR işlemine sokulursa
|
||
0 biti, 0 biti ile 1 biti EXOR işlemine sokulursa 1 biti elde edilir. 1 biti ile 0 biti EXOR işlemine sokulursa 1 biti
|
||
1 biti ile 1 biti EXOR işlemine sokulursa 0 biti elde edilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi bir sayının diğer bitlerine dokunmadan onun belli bir bitini tersi ile nasıl yer değiştirirsiniz? İşte bu sayı
|
||
tüm bitleri 0 olan ilgili biti 1 olan bir sayı ile EXOR işlemine sokulur. Örneğin biz 0110 1101 sayısını diğer bitlerine
|
||
dokunmadan 4'üncü bitini tersi ile yer değiştirmek isteyelim. Bu sayıyı 0001 0000 sayısı ile EXOR işlemine sokarız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0x6B; /* 0110 1101 */
|
||
|
||
a = a ^ 0x10;
|
||
|
||
printf("%02X\n", a); /* 0111 1011 = 0x7B */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
EXOR işlemi geri dönüşümlü bir işlemdir. Yani a ^ b => c ise, c ^ a => b ve c ^ b => a olmaktadır. Örneğin (sembolik olarak):
|
||
|
||
a = 0110 1101
|
||
b = 1011 0010
|
||
|
||
a ^ b = 1101 1111 => c
|
||
c ^ a = 1011 0010 => b
|
||
c ^ b = 0110 1101 => a
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0x6D; /* 0110 1101 */
|
||
unsigned char b = 0xB2; /* 0110 1111 */
|
||
unsigned char c, d, e;
|
||
|
||
c = a ^ b;
|
||
printf("%02X\n", c); /* 1011 1111 = 0xDF */
|
||
|
||
d = c ^ a;
|
||
printf("%02X\n", d); /* 0110 1111 = 0xB2 */
|
||
|
||
e = c ^ b;
|
||
printf("%02X\n", e); /* 0110 1101 = 0x6D */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
EXOR işleminin geri dönüşümlü olması şifreleme gibi bazı alanlarda yaygın kullanılmasına yol açmıştır. Örneğin biz bir dosya içerisindeki byte'ları
|
||
bir anahtarla EXOR işlemine sokup bozalım. Sonra aynı anahtarla yeniden bozulmuş veriyi EXOR işlemine sokalım. Bu işlemden sonra orijinal içeriği elde ederiz.
|
||
Yani bir şeyi bozup düzeltmek için EXOR çok güzel bir işlemdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de iki öteleme (shift) operatörü vardır: Sola öteleme (<< operatörü) ve sağa öteleme (>> operatörü). Her iki operatör de iki operand'lı arek operatörlerdir.
|
||
Öteleme operatörleri öncelik tablosunda aritmetik operatörler ile karşılaştırma operatörlerinin arasında bulunmaktadır. Öteleme operatörlerinde
|
||
soldaki operand ötelenecek değeri, sağdaki operand ötelenecek miktarı belirtmektedir.
|
||
|
||
Sola bir kez öteleme sırasında her bit bir sola kaydırılır, sayı en sağdan 0 ile beslenir. En soldaki bit yok olur. Örneğin:
|
||
|
||
0101 0111
|
||
|
||
Bu sayıyı bir kez sola öteleyelim:
|
||
|
||
1010 1110
|
||
|
||
Tabii biz ilk sayıyı iki kez de sola öteleyebiliriz. Bu durumda aynı işlem bir kez daha yapılacaktır:
|
||
|
||
0101 1100
|
||
|
||
Sağa öteleme bunun tersi bir işlemdir. Yani tüm bitler bir sağa kaydırılır, sayı en soldan 0 ile (bu konuda bazı ayrıntılardan bahsedeceğiz) beslenir,
|
||
en sağdaki bit kaybolur. Örneğin:
|
||
|
||
0101 0111
|
||
|
||
Bu sayıyı 1 kez sağa öteleyelim:
|
||
|
||
0010 1011
|
||
|
||
elde edilir.
|
||
|
||
Öteleme operatörlerinde önce her iki operand üzerinde de "int türüne yükseltme kuralı" uygulanır. İşlemin sonucu, sol taraftaki operand'ın int türüne yükseltme
|
||
kuralı sonucunda elde edilmiş olan türdendir. Yani örneğin biz short bir değeri sola ya da sağa ötelersek sonuç short türden çıkmaz. int türden çıkar.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
unsigned char a = 0x6D; /* 0110 1101 = 0x6D */
|
||
unsigned char b;
|
||
|
||
b = a << 1; /* 1101 1010 = 0xDA */
|
||
printf("%02X\n", b); /* 6A */
|
||
|
||
b = a >> 1; /* 0011 0110 = 0x36 */
|
||
printf("%02X\n", b); /* 39 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Sayıyı bir kez sola ötelemek onu 2 ile çarpmak anlamına gelir. Örneğin:
|
||
|
||
0000 0011
|
||
|
||
Bu sayı 3'tür. Şimdi onu 1 kez sola öteleyelim:
|
||
|
||
0000 0110
|
||
|
||
Bu sayı 6'dır. Şimdi 3 sayısını 2 kez sola öteleyelim:
|
||
|
||
0000 1100
|
||
|
||
Bu sayı 12'dir. O halde genelliştirirsek bir sayıyı n kez sola ötelediğimizde o sayıyı 2 üzeri n ile çarpmış oluruz.
|
||
|
||
Pekiyi sola öteleme sırasında bilgi kaybı söz konusu olabilir mi? Örneğin:
|
||
|
||
int a = 0x7FFFFFFF;
|
||
|
||
Burada a'nın içerisine en büyük pozitif sayı yerleştirilmiştir. Bu sayıyı a << 1 işlemi ile sola bir kez öteleyelim. Sola öteleme ikili çarpma anlamına geldiğine
|
||
göre sayıda bilgi kaybı oluşur. Burada bir taşma (overflow) söz konusu olduğu için işlem zaten tanımsız davranışa yol açacaktır.
|
||
|
||
Sayıyı bir kez sağa ötelemek onu 2'ye tam bölmek anlamına gelir. Örneğin:
|
||
|
||
0000 0110
|
||
|
||
Bu sayı 6'dır. Şimdi bu sayıyı bir kez sağa öteleyelim:
|
||
|
||
0000 0011
|
||
|
||
Bu sayı 3'tür. Görüldüğü gibi sayı 2'ye bölünmüştür. Örneğin:
|
||
|
||
0000 0101
|
||
|
||
Bu sayı 5'tir. Sayıyı bir kez sağa öteleyelim:
|
||
|
||
0000 0010
|
||
|
||
Bu sayı 2'dir. Bu biçimde 2'ye bölmede noktadan sonraki kısmın atıldığına dikkat ediniz. Sayıyı 2 kere sağa öteledeiğimizde sayıyı 2 ile değil 4 ile bölmüş
|
||
oluruz. O halde genel olarak sayıyı n defa sağa öteldiğimizde sayıyı 2 üzeri n'e bölmüş oluruz. Pekiyi sayıyı sağa ötelediğimizde işaret biti değişirse ne olur?
|
||
Örneğin:
|
||
|
||
int a = 0xFFFFFFFF;
|
||
|
||
Burada a'da -1 vardır. Biz şimdi a'yı sağa a >> 1 biçiminde bir kez ötelersek 0x7FFFFFFF değerini elde ederiz. Bu da en büyük pozitif değerdir.
|
||
İşte C standartlarında buradaki durumlar için şu kurallar oluşturulmuştur:
|
||
|
||
1) Eğer sol taraftaki operand işaretsiz bir tamsayı türündense sola öteleme sırasında işlemler yukarıda belirtildiği gibi yapılır. Yani tüm bitler bir sola kaydırılır,
|
||
en soldaki bit yok edilir. Sayıya en sağdan 0 ile besleme yapılır. Sağa öteleme sırasında da tüm bitler bir sağa kaydırılır, en sağdaki bit yok edilir.
|
||
Sayı en soldan 0 ile beslenir.
|
||
|
||
2) Sola öteleme işleminde sol taraftaki operand işaretli bir tamsayı türündense ve pozitifse, sayı 2 ile çarpıldığında çarpımın sonucu hala bu işaretli tamsayı
|
||
türünün sınırları içerisinde kalıyorsa işlem sonucunda bu iki ile çarpım değeri elde edilir. Eğer işaretli tamsayı negatifse ya da pozitif olduğu halde
|
||
sayının iki ile çarpılması sonucunda elde edilen değer o işaretli tamsayı türünün sınırları içerisine girmiyorsa "tanımsız davranış (undefined behavior)" söz
|
||
konusudur. Buradan çıkan sonuçlar şunlardır:
|
||
|
||
a) Negatif bir tamsayıyı sola ötelemek tanımsız davranışa yol açar.
|
||
b) Sayı pozitifse ancak sola öteleme sonucunda sayı soldaki operand'ın yükseltilmiş türünün sınırları içerisinde kalmıyorsa (yani overflow oluşuyorsa)
|
||
yine tanımsız davranış söz konusudur.
|
||
c) İşaretsiz tamsayı türlerininin sola ötelenmesinde hiçbir sakınca yoktur.
|
||
|
||
3) Sağa öteleme işleminde sol taraftaki operand işaertli bir tamsayı türündense ve pozitif ise işlem normal olarak her bitin sağa kaydırılması biçiminde
|
||
yani 2'ye bölme biçiminde yapılır. Ancak sol taraftaki operand işaretli tamsayı türünden ve negatif ise bu durumda işaret bitinin korunup korunmayacağı
|
||
(yani en soldan 0'la mı 1'le mi besleneceği) derleyicileri yazanların isteğine bırakılmıştır. İşaretsiz bir sayının sağa ötelenmesi tamamen yukarıda anlattığımız
|
||
bir kaydırma ile yapılmaktadır. Buradan çıkan sonuçlar şunlardır:
|
||
|
||
a) İşaretli pozitif bir sayının sağa ötelenmesinde hiçbir sakınca yoktur.
|
||
b) İşaretli begatif bir sayının sağa ötelenmesinde en soldan besleme 1 ya da 0 ile yapılabilir. Beslemenin 1 ile yapılması aslında negatif bir sayının işareti korunarak
|
||
2'ye bölündüğü anlamına gelir. Beslemenin 0 ile yapılması sayıyı büyük bir pozitif sayı haline getirir. Bu durun derleyicileri yazanların isteğine bırakılmıştır.
|
||
c) İşaretsiz tamsayı türlerinin sağa ötelenmesinde hiçbir sakınca yoktur.
|
||
|
||
Sonuç olarak işaretsiz tamsayılar üzerinde öteleme işlemleri sorunsuz olarak yukarıda anlatıldığı gibi yapılmaktadır. Ancak işaretli sayılar üzerinde öteleme
|
||
işlemleri yapılırken dikkat edilmelidir. Programcılar genellikle öteleme işlemlerini şşaretsiz tamsayılar üzerinde yaparlar.
|
||
|
||
Yine standartlara göre sola öteleme ya da sağa öteleme işleminde sağ taraftaki operand negatif ise ya da soldaki operand'ın yükseltilmiş türünün
|
||
bit uzunluğunu aşıyorsa tanımsız davranış söz konusudur. Yani bir sayıyı -1 defa ötelemek istersek derleme aşamsından geçilir. Ancak programın çalışması
|
||
sırasında ne olacağı belli değildir. Benzer biçimde biz int türünün 32 bit olduğu bir sistemde unsigned int bir değeri örneğin 40 kere sola ya da sağa ötelersek
|
||
bu durum da tanımsız davranış oluşturur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
71. Ders - 02/03/2023 - Perşembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Daha önce biz bir sayının n'inci bitinin 1 mi 0 mı olduğunu anlamak için bütün bitleri 0 olan ancak n'inci 1 olan bir sayı ile bit AND işlemi
|
||
uygulamıştık. Buna alternatif olarak aynı işlemi şöyle de yapabiliriz: Sayıyı n defa sağa öteleyip 1 ile bit AND işlemine sokarız. Sonuç ya 0 çıkar ya
|
||
da 1 çıkar. Örneğin:
|
||
|
||
if (val >> n & 1) {
|
||
/* n'inci bit 1 */
|
||
}
|
||
else {
|
||
/* n'inci bit 0 */
|
||
}
|
||
|
||
Aşağıdaki örnekte bir sayını n'inci bitinin durumu bu yöntemler elde edilmiştir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
|
||
int main(void)
|
||
{
|
||
uint32_t a = 0x12345678;
|
||
int n, result;
|
||
|
||
printf("Bit no:");
|
||
scanf("%d", &n);
|
||
|
||
result = a >> n & 1;
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bazen sayının yan yana k bitinin durumu elde edilmek istenebilir. Bunun için o k bitin düşük anlamlı biti n defa sağa ötelenerek en sağ tarafa getirilir.
|
||
Sonra da k tane biti 1 olan bir sayıyla bit AND işlemi uygulanır. Örneğin a sayısının 7 ve 8 numaralı bitlerinin durumunu elde etmek isteyelim:
|
||
|
||
result = a >> 7 & 3;
|
||
|
||
Burada 3 değerinin bit olarak 000...011 biçiminde olduğuna dikkat ediniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir sayının n'inci bitini sayının diğer bitlerine dokunmadan 1 yapmak isteyelim. Bu işlem de alternatif olarak şöyle gerçekleştirilebilir:
|
||
|
||
a = a | 1u << n;
|
||
|
||
Burada 1 sabitini 1u biçiminde ifade ettik. Böylece sola ötelemede n sayısı yüksek olduğunda "tanımsız davranıştan" sakınmak istedik.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Elimizde bir byte'lık işaretsiz bir tamsayı değer olsun. Bu değeri a ile temsil edelim. Biz bunun düşük anlamlı 4 bitini a & 0xF gibi bir işlemle
|
||
elde edebiliriz. Yüksek anlamlı 4 bitini ise a >> 4 işlemiyle elde ederiz. Bu iki 4 biti (nibble) yer değiştirmek için ise şu işlemi yaparız:
|
||
|
||
result = a << 4 | a >> 4;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
|
||
int main(void)
|
||
{
|
||
uint8_t a = 0xAF;
|
||
uint8_t result;
|
||
|
||
result = a >> 4 | a << 4;
|
||
printf("%02X\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Elimizde iki byte'lık (word) işaretsiz bir tamsayı olsun Bunun düşük anlamlı ve yüksek byte'larını şöyle elde edebiliriz:
|
||
|
||
#define LOBYTE(w) ((w) & 0xFF)
|
||
#define HIBYTE(w) ((w) >> 8 & 0xFF)
|
||
|
||
HIBYTE makrosunda 0xFF ile bit AND işlemi gereksiz gibidir. Ancak ne olursa olsun kişi yanlışlıkla 2 byte'tan daha büyük bir bilgiyi bu makroya verirse
|
||
yüksek byte'ların maskelenmesi daha güvenli bir durum oluşturur.
|
||
|
||
Benzer biçimde 32 bitlik işaretsiz bir tamsayı için de onları word word ayrıştıran makroları şöyle yazabiliriz:
|
||
|
||
#define LOWORD(dw) ((dw) & 0xFFFF)
|
||
#define HIWORD(dw) ((dw) >> 16 & 0xFFFF)
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
|
||
#define LOBYTE(w) ((w) & 0xFF)
|
||
#define HIBYTE(w) ((w) >> 8 & 0xFF)
|
||
|
||
int main(void)
|
||
{
|
||
uint16_t w = 0x1234;
|
||
uint8_t high, low;
|
||
|
||
high = HIBYTE(w);
|
||
low = LOBYTE(w);
|
||
|
||
printf("%02X\n", high);
|
||
printf("%02X\n", low);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bit NOT operatörü ~ ile temsil edilmektedir ve tek operand'lı önek bir operatördür. Bu operatör sayı içerisindeki 1'leri 0, 0'ları 1 yapar.
|
||
Bit NOT operatöründe operand üzerinde yine int türüne yükseltme kuralı uygulanır. Operatörün ürettiği değer bu yükseltilmiş türdendir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = 0;
|
||
|
||
a = ~a;
|
||
|
||
printf("%d\n", a); /* -1 */
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Klavyeden (stdin dosyasından) bir n değerinin okunduğunu kabul edelim. Düşük anlamlı n tane biti 1 olan geri kalan tüm bitleri 0 olan 32 bitlik
|
||
işaretsiz bir tamsayı değerini nasıl elde edebiliriz? Örneğin n değer 5 olsun. Elde edilecek sayı şyle olmalıdır:
|
||
|
||
0000 0000 0000 0000 0000 0000 0001 1111
|
||
|
||
Aşağıdaki gibi bir işlemle bu yapılabilir:
|
||
|
||
result = ~(~0u << n)
|
||
|
||
Burada 0 sabitinin 0u biçiminde yazıldığına dikkat ediniz. Eğer biz bu sabiti ~0 biçiminde yazsaydık bu durumda ~0 ifadesinden elde edilecek değer int türden olurdu.
|
||
Onun da sola ötelenmesi tanımsız davranış oluştururdu. Ancak ~0u ifadesinde 0u unsigned int türünden olduğu için artık ~ operatöründen elde edilen değer
|
||
unsigned int türünden olacaktır. Tabii aynı işlem şöyle yapılabilirdi:
|
||
|
||
result = ~0u >> (32 - n);
|
||
|
||
Burada da eğer ~0u yerine ~0 yazılırsa tüm bitleri 1 olan signed int bir değer elde edilir. İşaretli tamsayıların sağa ötelenmesinde
|
||
işaret bitinin korunup korunmayacağının derleyicilere bağlı bir davrış olduğunu anımsayınız.
|
||
|
||
2 üzeri n değerinden 1 çıkartmak da bir alternatif olabilir. Ancak kuvvet almak bit işlemlerine göre daha yavaş olacaktır:
|
||
|
||
result = (uint32_t)pow(2, n) - 1;
|
||
|
||
Tabii 2'nin n'inci kuvveti aslında 1U << n olduğu için aynı işlem şöyle yapılabilirdi:
|
||
|
||
result = (1U << n) - 1;
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
|
||
void disp_bits32(uint32_t val)
|
||
{
|
||
for (int i = 31; i >= 0; --i)
|
||
putchar((val >> i & 1) + '0');
|
||
putchar('\n');
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
uint32_t result;
|
||
int n;
|
||
|
||
printf("n: ");
|
||
scanf("%d", &n);
|
||
|
||
result = ~(~0u << n);
|
||
disp_bits32(result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıdaki sorununun benzeri şöyle olabilir: Yüksek anlamlı n biti 1 olan geri kalan bitleri 0 olan bir sayı nasıl elde edilir? Bu işlem de benzer
|
||
biçimde şöyle yapıabilir:
|
||
|
||
result = ~(~0u >> n);
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdint.h>
|
||
|
||
void disp_bits32(uint32_t val)
|
||
{
|
||
for (int i = 31; i >= 0; --i)
|
||
putchar((val >> i & 1) + '0');
|
||
putchar('\n');
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
uint32_t result;
|
||
int n;
|
||
|
||
printf("n: ");
|
||
scanf("%d", &n);
|
||
|
||
result = ~(~0u >> n);
|
||
disp_bits32(result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir sayının diğer bitlerine dokunmadan k'ıncı bitinden k + m'inci bitine kadar m tane bitini nasıl 0'layabiliriz? Örneğin daha somut olarak
|
||
32 bitlik bir sayıda 17, 18 ve 19'uncu bitleri diğer bitlere dokunmadan nasıl sıfırlayabiliriz? Buradaki m değerinin ve k değerinin klavyeden girildiğini
|
||
varsayalım ve sayının a olduğunu kabul edelim:
|
||
|
||
result = a & (~((~0u << m) << k))
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Anımsanacağı gibi C'de atama operatörüyle sağdan sola eşit öncelikli bir grup işlemli atama operatörü vardı. Örneğin a += b işlemi tamamen a = a + b
|
||
ile aynı anlama geliyordu. İşte bit operatörlerinin işlemli atama verisyonları da vardır. Örneğin biz a = a & b yerine a &= b ifadesini kullanabiliriz.
|
||
İşlemli bit atama operatörlerinin listesi şöyledir:
|
||
|
||
&=
|
||
|=
|
||
^=
|
||
<<=
|
||
>>=
|
||
|
||
Burada bir noktaya dikkatinizi çekmek istiyoruz: C'de mantıksal && ve || operatörlerinin işlemli atama karşılığı yoktur. Genel olarak tek operand'lı operatörerin de
|
||
işlemli karşılıkları yoktur. C'nin bütün işlemli atama operatörleri şunlardır:
|
||
|
||
= *= /= %= += -= <<= >>= &= ^= |=
|
||
|
||
O halde öncelik tablomuzun nihai durumu da aşağıdaki gibidir:
|
||
|
||
|
||
() [] . -> Soldan-Sağa
|
||
+ - ++ -- ! & * (tür) sizeof ~ Sağdan-Sola
|
||
* / % Soldan-Sağa
|
||
+ - Soldan-Sağa
|
||
<< >> Soldan-Sağa
|
||
< > <= >= Soldan-Sağa
|
||
!= == Soldan-Sağa
|
||
& Soldan-Sağa
|
||
^ Soldan-Sağa
|
||
| Soldan-Sağa
|
||
&& Soldan-Sağa
|
||
|| Soldan-Sağa
|
||
?: Sağdan-Sola
|
||
= *= /= %= += -= <<= >>= &= ^= |= Sağdan-Sola
|
||
,
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Son olarak yine bit operatörlerinin operand'larının tamsayı türlerine ilişkin olmak zorunda olduğunu anımsatmak istiyoruz. Örneğin elimizde bir adres
|
||
bilgisi olsun biz de bu adres bilgisinin düşük anlamlı 4 bitini 0 yapmak isteyelim. Adres bilgilerini doğrudan bit operatörlerine sokamayız. O zaman
|
||
adres bilgilerini önce tamsayı türlerine dönüştürüp sonra bit işlemini yapıp yeniden adres türüne dönüştürmek uygun olur. Örneğin a nesnesinin adresini alıp
|
||
onu sola 4 kez öteleyip bir göstericiye atamak isteyelim:
|
||
|
||
int a;
|
||
int *pi;
|
||
|
||
pi = (int *)((uintptr_t)&a << 4);
|
||
|
||
Burada biz önce &a ifadesini tamsayı türüne dönüştürdük sonra bu tamsayı değeri 4 kere sola öteledik. uintptr_t türünün <stdint.h> içerisinde o sistemdeki
|
||
adresi alacak genişlikte typedef edildiğini anımsayınız. Benzer biçimde biz ilgili sistemdeki adresin düşük anlamlı 4 bitini sıfırlamak isteyelim.
|
||
Bunu da taşınabilir bir biçimde şöyle yapabiliriz:
|
||
|
||
pi = (int *)((uintptr_t)&a & (~(uintptr_t)0 << 4))
|
||
|
||
Pekiyi bir double sayı üzerinde bit işlemi yapmanın bir anlamı olabilir mi? Aslında adresler üzerinde bit işlemleri bazı özel durumlarda gerekebilmektedir.
|
||
Ancak gerçek sayılar üzerinde genellikle böyle bir gereksinim ortaya çıkmaz. Gerçek sayılar ğzerinde bir işlemi yapmak için onu tamsayı türüe dönüştüremeyiz.
|
||
Çünkü o zaman saynın formatı değişir. O zaman mecburen gösterici kullanırız. Örneğin:
|
||
|
||
double d = 12.3;
|
||
|
||
result = *(uint64_t *)&d << 4;
|
||
|
||
Burada biz double sayıyı formatını bozmadan aynı bit dizilimiyle sanki bir tamsayı gibi ifade ettik. Sonra onun üzerinde bit işlemi yaptık.
|
||
Tabii bu işlem sonucunda bir tamsayı elde etmiş oluruz. Bu tamsayı da aynı yöntemle yeniden double sayıya dönüştürülebilir. Ancak yukarıda da belirttiğimiz
|
||
gibi genellikle böylesi işlemlere gereksinim duyulmamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bit işelmleri işlemcilerde genel olarak diğer işlemlere göre daha hızlı olma eğilimindedir. Özellikle çarpma ve bölme işlemleri pek çok işlemcide
|
||
yavaş işlemler grubundadır. Bu nendenle sistem programcıları 2'nin kuvvetleriyle çarpma ya da bölöe yapmak yerine doğrudan öteleme işlemlerini
|
||
tercih edebilirler. Gerçi günümüzde derleyicilerin kod optimizasyonları bu tür optimizasyonları da zaten yapabilmektedir. Yani biz pek çok derleyici 2'nin
|
||
kuvvetleri söz konusu olduğnda çarpma ve bölme işlemi yerine öteleme işlemlerini kullanabilmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de bir yapının elemanı n bitlik nesnelerden oluşturulabilmektedir. Yapıların bu biçimdeki elemanlarına "bit alanı (bit field)" elemanları denilmektedir.
|
||
Yapının bit alanı elemanları dekleratörde ':' atomu ve bit uzunluğunu belirtien bir sabit ifadesi ile oluşturulmaktadır. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
double a;
|
||
int b;
|
||
int c: 3; /* bit alanı elemanı */
|
||
int d: 5; /* bit alanı elemanı */
|
||
};
|
||
|
||
Burada yapının a ve b elemanları sırasıyla double ve int türdendir. Ancak c elemanı 3 bitlik bir int nesneyi, d elemanı ise 5 bitlik bir int nesneyi belirtmektedir.
|
||
n bitlik nesneler ancak yapı elemanları olarak oluşturulabilmektedir. Yapının dışında böyle bir bildirim yapılamaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
72. Ders - 07/03/2023 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Standartalara göre yapıların bit alanı elemanları int, unsigned int ya da _Bool türden olabilir. Ancak diğer türlerin bit alanı elemanı olarak
|
||
kullanılıp kullanılmayacağı derleyicileri yazanların isteğine bırakılmıştır. Bit alanı elemanı int (signed int) türdense yine ikiye tümleyen aritmetiği
|
||
kullanılır. Örneğin şöyle bir bit alanı elemanı olsun:
|
||
|
||
int a: 3;
|
||
|
||
Burada a elemanı işaret biti dahil olmak üzere 3 bittir. 3 bit ile yazılabilecek işaretli tamsayı sınırı şöyledir:
|
||
|
||
100 -4
|
||
011 +3
|
||
|
||
Bunun genel formününün bit sayısı n olmak üzere şöyle olduğunu biliyorsunuz:
|
||
|
||
[-2^(n - 1), + 2^(n - 1) - 1]
|
||
|
||
Bit alanı elemanı unsigned int türünden olsaydı bu durumda işaret biti söz konusu olmayacaktı. Örneğin:
|
||
|
||
unisgned a: 3;
|
||
|
||
Burada a bit alanı elemanının sayı sınırı şöyle olacaktır:
|
||
|
||
000 0
|
||
111 +7
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İşlemcilerde adresleme byte düzeyinde yapılmaktadır. Dolayısıyla bit alanı işlemleri işlemciler tarafından bitlere ilişkin makine komutlarıyla
|
||
gerçekleştirilmektedir. Şüphesiz aslında programcı da bit alanı işlemlerini kendisi bit operatörleriyle gerçekleştirebilir. Ancak bit alanları bunu
|
||
biraz daha yüksek seviyeli bir biçimde sunmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Mademki en az byte katlarındaki nesnelerin adresleri alınabilmektedir o halde bit alanı elemanlarının adreslerinin alınması anlamsızdır. Dolayısıyla
|
||
C standartlarına göre bir alanı elemanlarının adresleri & operatörü ile alınmaz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bit alanı elemanlarının yapı içerisindeki organizasyonları C standartlarında ana hatlarıyla belirtilmiştir. Organizasyon büyük ölçüde derleyicileri
|
||
yazanların isteğine bırakılmıştır. Standartlar bu konuda şunları söylemektedir:
|
||
|
||
- Bir bit alanı elemanı yapı nesnesinin içerisndeki bir byte içerisinde konumlandırıldığında eğer onu izleyen yapının bir bit alanı elemanı varsa
|
||
izleyen bit alanı elemanı eğer o byte'ın içerisine sığacak durumdaysa kesinlikle o byte'ın içerisinde konumlandırılır. Ancak izleyen bit alanı elemanı
|
||
o byte'ın içerisine sığmıyorsa bu durumda o byte ile sonraki byte içerisine konumlandırılabilir ya da sonraki byte'tan başlayarak konumlandırılabilir.
|
||
Bu konuda davranış derleyicileri yazanların isteğine bırakılmıştır. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b: 3;
|
||
int c: 5;
|
||
int d;
|
||
};
|
||
|
||
Burada yapıu nesnesinin b ve c bit alanı elemanları aynı byte içerisinde konumlandırılır. Çünkü b elemanı 3 bit olduğundan aynı byte'ta 5 bitlik
|
||
daha yer vardır ve c elemanı bu 5 bitlik yere sığmaktadır. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b: 3;
|
||
int c: 7;
|
||
int d;
|
||
};
|
||
|
||
Burada b bit alanı elemanı 3 bite yerleştirildikten sonra aynı byte içerisinde 5 bit kaldığından dolayı c elemanı için yeterli yer yoktur.
|
||
Bu durumda c elemanı bir kısmı o byte bir kısmı diğer byte'ta (overlapped) bir biçimde yerleştirilebileceği gibi tamamen sonraki byte'tan
|
||
itibaren de yerleştirilebilir. Bu durum derleyicileri yazanların isteğine bırakılmıştır. Yani yukarıdaki yerleşim bir derleyicide aşağıdaki gibi olabilir:
|
||
|
||
cccc cbbb
|
||
???? ??cc
|
||
|
||
Ya da örneğin şöyle olabilir:
|
||
|
||
???? ?bbb
|
||
?ccc cccc
|
||
|
||
Buradaki ?'leri derleyicinin yerleştirme yapmadığı boş bitleri temsil etmektedir.
|
||
|
||
Ayrıca standartlar byte içerisindeki bit alanı elemanlarının yerleşim bitlerinin yüksek anlamlı bitlerden düşük anlamlı bitlere doğru mu yoksa
|
||
düşük anlamlı bitlerden yüksek anlamlı bitlere doğru mu yapılacağını yine derleyicileri yazanların isteğine bırakmıştır. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a;
|
||
int b: 3;
|
||
int c: 5;
|
||
int d;
|
||
};
|
||
|
||
Burada b elemanının düşük anlamlı bitlerde, a elemanının yüksek anlamlı bitlerde olup olmayacağı derleyicileri yazanların isteğine bırakılmıştır. Yani dizilim
|
||
aşağıdaki iki biçimdeki gibi de olabilir:
|
||
|
||
cccc cbbb
|
||
bbbc cccc
|
||
|
||
Görüldüğü gibi bit alanı elemanlarının yapı nesnesi içerisindeki yerleşimleri konusunda derleyiciden derleyiciye değişebilecek
|
||
büyük bir belirsizlik vardır. Programcıların belli bir derleyicinin uyguladığı yerleşime güvenerek çıkarım yapmaması gerekir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bit alanı elemanları aslında yer kazancı sağlamak için kullanılmaktadır. Örneğin biz bir zaman bilgisini bir yapı içerisinde şöyle tutmak isteyelim:
|
||
|
||
struct TIME {
|
||
int hour;
|
||
int minute;
|
||
int second;
|
||
};
|
||
|
||
Bu yapı muhtemelen Windows ve UNIX/Linux sistemlerinde 12 byte yer kaplayacaktır. Halbuki aslında zamanın bu bileşenlerinin dörder byte yer kaplamasına
|
||
gerek yoktur. Saat bilgisi 5 bit ile, dakika ve saniye bilgileri 6 bit ile temsil edilebilir:
|
||
|
||
struct TIME {
|
||
unsigned hour: 5;
|
||
unsigned minute: 6;
|
||
unsigned second: 6;
|
||
};
|
||
|
||
Pekiyi bu yapı kaç byte yer kaplayacaktır? Bitleri saydığımızda 17 olduğunu görüyoruz. Ancak 17 bit biçiminde bir nesne oluşturulamaz. Üstelik derleyicinin
|
||
uyguladığı bir hizalama da vardır. Anımsanacağı gibi derleyiciler tüm yapı nesnesi için belli değerin katlarında yer ayırmaktadır. O zaman bu yapı muhtemelen bellekte
|
||
4 byte yer kaplayacaktır (hizalama uygulanacağına dikkat ediniz). Bu yapyı aşağıdaki gibi pragma direktifleriyle BYTE hizalamasına getirsek bile derleyicilerin
|
||
çoğu bit alanı elemanları içeren yapı nesneleri için yine 4'ün katları kadar yer ayırmaktadır. Yani aşağıdaki gibi bir pragma direktifi muhtemelen derleyicinizde
|
||
bu yapının daha az yer kaplaması için yeterli olmayacaktır:
|
||
|
||
#pragma pack(1)
|
||
|
||
struct TIME {
|
||
unsigned hour: 5;
|
||
unsigned minute: 6;
|
||
unsigned second: 6;
|
||
};
|
||
|
||
Bu konu sonraki paragrafta ele alınacaktır. Örneğin bir test sınavında bir sorunun yanıtı A, B, C, D, E şıklarından biri olabilir. Bu yanıtı biz 1 byte
|
||
içerisinde değil 3 bit içerisinde saklayabiliriz.
|
||
|
||
Örneğin bir satranç oyununda hamleler kaydedilebilmektedir. Kayıt için kullanılan en yaygın format bir taşın başlangıç ve bitiş karesinin yazılmasıdır.
|
||
Örneğin "e2-e4" hamlesi demek e2 karesindeki taşın e4 karesine oynanmış olması demektir. Satranç 8x8'lik bir tahtada oynandığına göre aslında harfler ve sayılar
|
||
üçer bit ile temsil edilebilir. Böylece bir hamle 12 bit ile kaydedilebilir.
|
||
|
||
Bit alanı elemanları yer kazancı sağlamak için düşünülmüştür. Ancak sıradan nesneler için bu kazanaç genellikle önemli olmaz. Yani örneğin bir zaman bilgisinin
|
||
int elemanlarla 12 byte ile kodlanması programlamada aslında hiç önemli değildir. Ancak milyonlarca zaman bilgisinin saklanacağı bir durumda bu önemli olabilmektedir.
|
||
Yani özetle tekil nesneler için bit alanı kullanarak yer kazancı sağlamaya çalışmak iyi bir teknik değildir. Ancak çok sayıda nesne söz konusu ise
|
||
bit alanı elemanlarıyla yer kazancı sağlama iyi bir fikir haline gelmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıda da belirtildiği gibi bit alanları her ne kadar yer kazancı sağlamak için kullanılıyor olsa da bunun gerçekleşmesi bazı koşullara bağlıdır.
|
||
Örneğin bizim yapımız toplam 17 bit yer kaplıyorsa derleyiciler 17 bitlik bir nesne oluşturamadıklarından dolayı mecburen byte'ın katları kadar nesne
|
||
oluştururlar. Bu da en az 3 byte olur. Öte yandan derleyiciler hizalama da uyguladıklarından tüm nesne için belli sayının katları kadar yer ayırabilmektedir.
|
||
Örneğin:
|
||
|
||
struct TEST_ANSWER {
|
||
unsigned answer: 3;
|
||
usnsigned result: 1;
|
||
};
|
||
|
||
Biz burada 1000000 TEST_ANSWER değerini bir yapı dizisi biçiminde tutmaya çalışalım:
|
||
|
||
struct TEST_ANSWRE answers[1000000];
|
||
|
||
Burada bir tane struct TEST_ANSWER nesnesi hizalama yüzünden 4 byte yer kaplayabilecektir. Bu durumda da bizim bit alanı elemanlarını kullanmamıza bir gerek
|
||
kalmayacaktır. Tabii derleyicilerde daha önceden de belirttiğimiz gibi hizalama kontrol altına alınabilmektedir. Fakat bu taşınabilir bir durum değildir.
|
||
|
||
O halde yer kazancı sağlanmak isteniyorsa hizalama da dikkate alınarak yapının birden fazla aynı türden olguyu depolayacak biçimde oluşturulması uygun olur.
|
||
Örneğin:
|
||
|
||
struct TEST_ANSWER {
|
||
unsigned answer1: 3;
|
||
usnsigned result1: 1;
|
||
unsigned answer2: 3;
|
||
usnsigned result2: 1;
|
||
unsigned answer3: 3;
|
||
usnsigned result3: 1;
|
||
unsigned answer4: 3;
|
||
usnsigned result4: 1;
|
||
};
|
||
|
||
Burada bu yapı hizalama yüzünden yine 4 byte yer kaplayacaktır. Ancak bir tane struct TEST_ANSWER nesnesi aslında idört farklı test yanıtını ve sonucunu tutmaktadır.
|
||
|
||
Bit alanı elemanları kullanırken programcının yapının o derleyicideki sizeof değerine dikkat etmesi tavsiye edilir. Aksi takdirde bit alanı
|
||
elemanlarından bir yer kazancı sağlanmayacağı gibi hız bakımından bir dezavantaja da yol açılmış olabilir. Çünkü bit alanı elemanları aslında derleyici tarafından
|
||
bitsel makine komutlarıyla yapay bir biçimde oluşturulmaktadır.
|
||
|
||
Bizim bu konudaki önerimiz şudur: Bit alanı elemanlarını çok önemli bir yer problemi olmadıkça kullanmayınız. Eğer kullanacaksanız bundan bir
|
||
kazanç sağlayıp sağlamayacağınızı test ederek görünüz. Ancak bu konudaki davranışın da derleyiciden derleyiciye değişebileceğini aklınızda bulundurunuz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bit alanlarıyla ilgili son bir özellik daha vardır. Bir bit alanı elemanı için isim belirtilmeyebilir. Bu durumda bu eleman yine yer kaplar. Ancak
|
||
o elemanın bir ismi olmadığı için kullanılamaz. Bu özelllik "padding" yapmak için düşünülmüştür. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a: 4;
|
||
int : 4;
|
||
int b: 5;
|
||
int : 3;
|
||
};
|
||
|
||
Burada a elemanından sonra ve b elemanından sonra padding amaçlı boşluklar bırakılmıştır. Buradaki "padding" terimi kullanılmayan ama yer kaplayan
|
||
dolayısıyla sonraki elemanın diğer byte'tan başlamasını sağlayan boşlukları anlatmaktadır. Genellikle böyle padding gereksinimi seyrek bir biçimde
|
||
ortaya çıkmaktadır.
|
||
|
||
Eğer bit alanı elemanının uzunluğu 0 verilirse bu da özel bir anlama gelir. Bu durumda sonraki bit alanı elemanı aynı byte'ın içerisine yerleştirilmez,
|
||
Sonraki byte'ın içine yerleştirilir. Örneğin:
|
||
|
||
struct SAMPLE {
|
||
int a: 5;
|
||
int :0 ;
|
||
int b: 7;
|
||
/* ... */
|
||
};
|
||
|
||
Burada programcı a elemanından sonraki b elemanının aynı byte içerisinden başlanarak yerleştirilmesini istememiştir. Artık b elemanı kesinlikli sonraki byte'tan
|
||
itibaren yerleştirilecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bütün anlattığımız durumlar eşliğinde bir alanları elemanları için şu tavsiyelerde bulunabiliriz: Bir alanı elemanlarının yerleşimi, bundan elde edilecek
|
||
yer kazancı derleyiciden derleyiciye değişebilmektedir. Bu konuda standartlar bir belirlemede bulunmamıştır. Eğer birtakım olguları az bit ile
|
||
standart bir biçimde ifade edecekseniz char gibi int gibi türleri kullanıp bit işlemleriyle bitleri bu nesnelerin içerisine yerleştirmelisiniz.
|
||
Zaten derleyiciler de bit alanı elemanlarını bit operatörleriyle işleme sokmaktadır.Bütün bunların sonucu olarak C'de b,t alanı elemanlarının
|
||
kullanımı çok sınırlıdır. Bu nedenle programlarda bunlarla çok seyrek karşılaşırsınız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İçerisinde byte'ların bulunduğu ikincil bellekleredeki bölgere "dosya (file)" denilmektedir. Kullanıcılar dosyalara bir isim ile erişirler.
|
||
Dosyaların bütün organizasyonu işletim sistemi tarafından sağlanmaktadır. Bir dosyanın ismi verildiğinde işletim sistemi onun ikincil bellekteki
|
||
yerini belirleyebilmektedir.
|
||
|
||
Modern işletim sistemlerinde dosyalar "dizin (directory)" denilen kapların içerisinde bulunmaktadır. Bir dizinde hem dosyalar hem de başka dizinler
|
||
bulunabilir. Böylece dizinler bir ağaç oluştururlar. Bu ağacın en dışındaki dizine "kök dizin (root directory)" denilmektedir. Genel olarak bir dizin içerisinde
|
||
aynı isimli bi,rden fazla dosya ya da dizin bulunamaz. Ancak farklı dizilerde aynı isimli dizin ya da dosyalar bulunabilir. O halde biz "test.txt" dosya
|
||
ismini kullandığımızda pek çok dizin içerisinde bu isimli bir dosya var olabilir. İşte bir dosyanın hangi dizinin içerisinde olduğunu anlatan
|
||
yazısal ifadelere "yol ifadeleri (paths)" denilmektedir. Kullanıcı işletim sistemine yalnızca dosyanın ismini değil dizin bilgisini de içeren
|
||
yol ifadesini verir.
|
||
|
||
Windows işletim sisteminde her disk biriminin ayrı bir kökü vardır. Örneğin bu sistemlerde biz bir çıkartılabilir flash disk disk taktığımızda sistem
|
||
onu ayrı birim olarak görür. O birimin ayrı bir kökü vardır. Bu birimlere Windows sistemlerinde "sürücü (drive)" denilmektedir. UNIX/Linux sistemleri
|
||
ve macOS sistemleri sürücü kavramını kullanmamaktadır. Bu sistemlerde tek bir ağaç vardır.
|
||
|
||
UNIX(Linux ve macOS sistemlerinde bir bellek birimi sisteme iliştirildiğinde sistem onu ağaç içerisinde bir dizinin altına monte etmektedir.
|
||
Bu işleme "mount" işlemi denilmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
73. Ders - 09/03/2023 - Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yol ifadelerinde dizin geçişlerinde UNIX/Linux sistemleri ve macOS sistemleri "/" karakterini, Windows sistemleri ise "\" karakterini kullanmaktadır.
|
||
Ancak Windows sistemleri dizin geçişlerinde UNIX/Linux uyumunu korumak için "/" karakterini de desteklemektedir. Ancak UNIX/Linux sistemleri genel olarak
|
||
"\" karakterini desteklememektedir. Örneğin:
|
||
|
||
/a/b/c.text UNIX/Linux ve macOS sistemlerinde (ancak Windows sistemleri de destekliyor)
|
||
\a\b\c.text Windows sistemlerinde, ancak UNIX/Linux ve macOS sistemleri desteklemiyor
|
||
|
||
Yol ifadeleri "mutlak (absolute)" ve "göreli (relative)" olmak üzere ikiye ayrılmaktadır. Yol ifadesinin ilk karakteri UNIX/Linux sistemlerinde "/",
|
||
Windows sistemlerinde "\" ise böyle yol ifadelerine mutlak yol ifadeleri denilmektedir. Örneğin:
|
||
|
||
|
||
/a.txt
|
||
/a/b/c.txt
|
||
\windows\notepad.exe
|
||
|
||
Mutlak yol ifadeleri her zaman kök dizinden (root) yer belirtmektedir. Örneğin:
|
||
|
||
/usr/include/stdio.h
|
||
|
||
Burada kökün altında "usr" dizini, "usr" dizininin altında "include" dizini vardır ve bu "include" dizininin altındaki "stdio.h" dosyası belirtilmektedir.
|
||
|
||
Eğer yol ifadelerinin ilk karakterleri UNIX/Linux ve macOS sistemlerinde "/" değilse, Windows sistemlerinde de "\" değilse böyle yol ifadeleri
|
||
göreli yol ifadeleridir. Örneğin:
|
||
|
||
a/b/c.txt
|
||
temp\test.txt
|
||
test.txt
|
||
|
||
Pekiyi göreli yol ifadeleri nereden itibaren yer belirtmektedir? İşte işletim sistemlerinde çalışmakta olan programlara "proses (process)" denilmektedir.
|
||
Her prosesin bir "çalışma dizini (current working directory)" vardır. Prosesin çalışma dizini proses yaratıldığında (yani program çalışmaya başladığında)
|
||
belli bir dizindir. Ancak programın çalışması sırasında programcı tarafından değiştirilebilmektedir. Göreli yol ifadeleri prosesin çalışma dizininden
|
||
itibaren yer belirtir. Yani göreli yol ifadeleri için orijin noktası prosesin çalışma dizinidir. Örneğin programımızın çalışma dizini "c:\temp" olsun.
|
||
Biz de "a/b/c.txt" biçiminde göreli bir yol ifadesi belirtmiş olalım. İşte aslında biz bu göreli yol ifadesi ile "c:\temp\a\b\c.txt" yol ifadesini
|
||
belirtmiş olmaktayız. Aynı durumda "test.txt" yol ifadesini belirtirsek bu sefer aslında biz "c:\temp\test.txt" yol ifadesini belirtmiş oluruz.
|
||
|
||
İşletim sistemlerinde o anda çalışmakta olan her programın (yani proseslerin) çalışma dizinleri birbirinden farklı olabilmektedir. Pekiyi programın
|
||
çalışma dizini için başında (yani akış main fonksiyonuna girdiğinde) neresidir? İşte işletim sistemlerinde bir program başka bir program tarafından
|
||
çalıştırılmaktadır. Bir programın çalıştırdığı programa işletim sistemleri terminolojisinde "alt proses (child prosess)", çalıştıran programa
|
||
da "üst proses (parent prosess)" denilmektedir. Genel olarak işletim sistemlerinin büyük bölümünde üst prosesin çalışma dizini alt prosese aktarılmaktadır.
|
||
Bu durumda özetle şunlar söylenebilir:
|
||
|
||
- Eğer programı komut satırından çalıştırıyorsanız komut satırı programının çalışma dizini çalıştırdığınız programın çalışma dizini olacaktır.
|
||
|
||
- Eğer programı IDE'den çalıştırıyorsanız. IDE'ler genellikle çalıştırdıkları programın çalışma dizinini proje dizini olarak ayarlamaktadır.
|
||
Ancak bunun için IDE'nin dokümanlarına başvurabilirsiniz. Pek çok IDE'de zaten bu dizin ayarlanabilmektedir.
|
||
|
||
Prosesin çalışma dizinini alan ve onu set eden standart C fonksiyonları yoktur. Bu işlemler işletim sistemine bağlı olarak o işletim sistemine özgü
|
||
fonksiyonlarla yapılabilmektedir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Windows sistemlerinde "sürücü (drive)" kavramı da olduğuna göre bu sistemlerdeki mutlak yol ifadeleri hangi sürücünün kökünü referans almaktadır?
|
||
Windows sistemlerinde mutlak yol ifadelerine sürüvü bilgisi de eklenebilmektedir. Örneğin:
|
||
|
||
d:\temp\test.txt
|
||
|
||
Burada artık sürücü de belirtildiği için kök dizinin D sürücüsünün kök dizini olduğu anlaşılmaktadır. Sürücü de içeren yol ifadelerine Windows
|
||
sistemlerinde "tam yol ifadeleri (full path names)" denilmektedir. Pekiyi aşağıdaki mutlak yol ifadesi hangü sürücünün kökündne itibaren yer belirtir?
|
||
|
||
\a\b\c.txt
|
||
|
||
İşte Windows sistemlerinde proses çalışma dizini sürücü bilgisi de içermektedir. Prosesin çalışma dizini hangi sürücüye ilişkinse sürücü belirtilmemiş
|
||
olan mutlak yol ifadeleri o sürücüsünün kökünden itibaren yer belirtmektedir. Örneğin Windows'ta prosesimiizn çalışma dizini "d:\ali" alsun.
|
||
Biz de "\a\b\c.txt" biçiminde sürücü içermeyen bir mutlak yol ifadesi belirtmiş olalım. Buradaki kök D sürücüsünün köküdür.
|
||
|
||
Windows sistemlerinde sürücü içeren göreli yol ifadeleri de oluşturulabilmektedir. Örneğin:
|
||
|
||
C:a\b\c.txt
|
||
|
||
Burada yol ifadesi hem görelidir hem de bir sürücü belirtilmiştir. İşte Windows sistemlerinde bu tür durumlarda prosesin bazı çevre değişkenlerine
|
||
bakılmaktadır. Bu çevre değişkenleri set edilmediyse yol ifadesi sanki mutlak yok ifadesi gibi ele alınmaktadır. Böylesi yol ifadelerini tercih
|
||
etmeyiniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dosya ve dizin isimlendirmesinde kullanılabilecek karakterler Windows sistemlerinde ve UNIX/Linux ve macOS sistemlerinde farklılıklar içerebilmektedir.
|
||
Bu konu kullanılan kullanılan "dosya sistemi (file system)" ile ilgilidir. Ancak Windows dünyası ile diğer dünya arasında en önemli farklılıklardan biri
|
||
Windows dünyasında dosya ve dizin isimlerinin büyük harf küçük harf duyarlılığının olmaması ancak UNIX/Linux ve macOS sistemlerinde olmasıdır.
|
||
Örneğin Windows sistemlerinde aşağıdaki iki yol ifadesi arasında hiçbir farklılık yoktur. Ancak UNIX/Linux ve macOS sistemlerinde farklılık vardır:
|
||
|
||
test.text
|
||
Test.txt
|
||
|
||
Dosya uzunatılarının (file extensions) işletim sistemi için bir önemi yoktur. Dosya uzantıları aslında insanların dosyanın neden oluşturulduğunu
|
||
anlamaları için kullanılmaktadır. Bir dosya ya da dizin ismi uzantıya sahip olmak zorunds değildir. Dizinler genellikle uzantıya sahip olmazlar.
|
||
Ancak istenirse dizinlere uzantı verilebilir. Bir dosya isminde genel olarak birden fazla "." karakteri kullanılabilemktedir. Windows sistemlerinde
|
||
Unicode karakterler dosya ismi olarak kullanılabilmektedir. Ancak UNIX/Linux sistemlerinde genel olarak kullanılamamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'de dosya işlemleri prototipleri <stdio.h> içerisinde olan standart C fonksiyonlarıyla yapılmaktadır. Bu dosya fonksiyonlarının hepsi "f" harfi ile
|
||
başlatılarak isimlendirilmiştir. Örneğin:
|
||
|
||
fopen, fclose, fread, fwrite, fgetc, ...
|
||
|
||
Aslında bütün dosya işlemleri işletim sisteminin kontrolü altındadır. Yani tüm dosya işelmleri aslında işletim sistemlerinin sistem fonksiyonları
|
||
tarafından yapılmaktadır. Programlama dili ne olursa olsun dosya işlemleri eninde sonunda işletim sisteminin içerisindeki fonksiyonların
|
||
çağrılmasıyla gerçekleştirilmektedir. Yani dosya işlemleri aslında srandart C fonksiyonlarıyla değil bu fonksiyonların çağırdığı sistem fonksiyonlarıyla
|
||
yapılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dosya üzerinde işlem yapabilmek için önce dosyanın açılması gerekir. Dosyanın açılması sırasında işletim sistemi birtakım hazırlık işlemleri
|
||
yapmaktadır. Dosyanın açılması için fopen isimli standart C fonksiyonu kullanılmaktadır. fopen fonksiyonunun prototipi şöyledir:
|
||
|
||
FILE *fopen(const char *path, const char *mode);
|
||
|
||
Fonksiyonun birinci parametresi açılacak dosyanın yol ifadesini belirtmektedir. Bu yol ifadesi mutlak ya da göreli olabilmektedir. İkinci parametre "açış
|
||
modunu" belirtir. Dosya açış modları aşağıdakilerden biri olabilir:
|
||
|
||
Açış Modu Anlamı
|
||
--------- ------
|
||
"r" Olan dosyayı açar, yalnızca okuma yapılabilir.
|
||
"r+" Olan dosyayı açar, hem okuma hem de yazma yapılabilir.
|
||
"w" Dosya yoksa yaratır ve açar, dosya varsa içini sıfırlar (truncate eder) ve açar, yalnızca yazma yapılabilir.
|
||
"w+" Dosya yoksa yaratır ve açar, dosya varsa içini sıfırlar (truncate eder) ve açar, hem okuma hem de yazma yapılabilir.
|
||
"a" Dosya varsa olanı açar, yoksa yaratır ve açar, yalnızca yazma yapılabilir. Her yazılan sona eklenir.
|
||
(Dosyanın sonunun dışında başka bir yerine yazma yapılamaz)
|
||
"a+" Dosya varsa olanı açar, yoksa yaratır ve açar, hem okuma hem yazma yapılabilir. Her yazılan sona eklenir.
|
||
Ancak herhangi bir yerden okuma yapılabilir.
|
||
|
||
Bir dosya çeşitli nedenlerden dolayı açılamayabilir. Bu durumda fopen fonksiyonu başarısız olur. Örneğin dosyayı "r" modunda açmak istediğimizde
|
||
dosya mevcut değilse açık işlemi başarısız olur. İşletim sistemlerinde her prosesin açabileceği maksimum bir dosya sayısı vardır. Dosya ismi
|
||
geçersizse dosya açılamaz. Açış modu yukarıda belirtilenlerin dışındaysa yine fopen başarısz olur. Ayrıca işletim sistemleri dosyalar üzerinde
|
||
bazı erişim kontrolleri de uygulayabilemktedir. Yani biz her istediğimiz dosyayı her istediğimiz modda açamayabiliriz. Bu durumlarda da yine fopen
|
||
fonksiyonu başarısız olur.
|
||
|
||
fopen fonksiyonu başarılı olduğunda FILE isimli bir yapı nesnesinin adresiyle geri döner. fopen fonksiyonun geri döndürdüğü nesne güvenli bir biçimde
|
||
tahsis edilmiştir. FILE bir typedef ismidir. <stdio.h> içerisinde aşağıdaki biçimde typedef edilmiştir:
|
||
|
||
typedef struct {
|
||
....
|
||
} FILE;
|
||
|
||
Ancak C standartları bu FILE yapısının içeriği konusunda bir açıklama yapmamıştır. Dolayısıyla programcı bu yapının elemanlarıyla ilgilenmemelidir.
|
||
fopen fonksiyonun geri döndürdüğü FILE nesnesinin adresi diğer dosya fonksiyonlarına argüman olarak geçirilmektedir. Programcı bu FILE yapısının içeriği
|
||
ile ilgili bilgi sahibi olmak zorunda değildir. fopen başarısız olursa NULL adrese geri döner. Programcı mutlaka fopen fonksiyonun başarısını
|
||
kontrol etmelidir.
|
||
|
||
fopen fonksiyonun geri döndürdüğü FILE adresine İngilizce "stream" denilmektedir. Biz bu adrese Türkçe "dosya bilgi göstericisi" diyeceğiz.
|
||
|
||
O halde bir dosya tipik olarak şöyle açılır:
|
||
|
||
FILE *f;
|
||
|
||
if ((f = fopen("test.txt", "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXI T_FAILURE);
|
||
}
|
||
|
||
Aşağıdaki örnekte var olan bir dosya "r" modunda açılmak istenmiştir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
|
||
if ((f = fopen("test.txt", "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
printf("Ok\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Programcı bir dosyayla işlemlerini bitirince dosyayı kapatmalıdır. Dosyanın kapatılması ile açılması sırasında yapılan birtakım işlemler geri
|
||
alınmaktadır. Aslında dosya kapartılmasa bile en kötü olasılıkla program biterken exit işlemşyle birlikte zaten tüm açık dosyalar otomatik kapatılmaktadır.
|
||
Yani aslında dosyaların kapatılması mutlak anlamda gerekli değildir. Ancak artık gereksinim duyulmayan kaynakların geri bırakılması iyi bir tekniktir.
|
||
Dolayısıyla programcının programın bitmesini beklemeden artık kullanmayacağı dosyaları kapatması iyi bir tekniktir.
|
||
|
||
Dosyayı kapatmak için fclose fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
|
||
|
||
int fclose(FILE *f);
|
||
|
||
Fonksiyon fopen fonksiyuonundan elde edilen dosya bilgi göstericisini (stream) parametre olarak alıp dosyayı kapatmaktadır. Dosya başarılı bir biçimde
|
||
kapatılmışsa fclose 0 değerine geri döner. Eğer dosya başarılı bir biçimde kapatılamadıysa fclose <stdio.h> içerisinde define edilmişl olan EOF değerine geri
|
||
dönmektedir. C standartları EOF sembolik sabitinin kaç oalrak define edileceği konusunda bir belirlemede bulunmamıştır. Ancak negatif int bir sabit ifadesi
|
||
olarak define edilmesi gerektiği standartlarda belirtilmiştir. Tipik olarak derleyicilerin hemen hepsi EOF sembolik sabitini -1 olarak define etmektedir:
|
||
|
||
#define EOF (-1)
|
||
|
||
fclose fonksiyonun gheri dönüş değerinin kontrol edilmesine gerek yoktur. Programcı her şeyi doğru yaptıysa bu fonksiyon başarısız olamaz.
|
||
Ancak örneğin programcı fonksiyona yanlış bir adres geçtiyse ya da örneğin zaten kapatılmış bir dosyayı yeniden kapatmaya çalışmışsa
|
||
fonksiyon başarısız olabilir. Biz de örneklerimizde fonksiyonun başarısını kontrol etmeyeceğiz.
|
||
|
||
Aşağıdaki örnekte bir dosya açıldıktan sonra kapatılmıştır. Dosyanın programın sonunda kapatılmasının bir önemi yoktur. Çünkü nasıl olsa birazdan
|
||
program bittiğinde dosya da fclose işlemi ile otomatik biçimde kapatılacaktır. Ancak ne olursa olsun programcıların çoğu okunabilirliği
|
||
artırmak için program sonunda bile olsa dosyaları fclose ile kapatmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
|
||
if ((f = fopen("test.txt", "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
printf("Ok\n");
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
74. Ders - 14/03/2023 - Salı
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İşletim sistemleri bir dosya açıldığında dosya içerisindeki tüm byte'lara ilk byte 0 olmak üzere bir offset numarası karşı düşürmektedir.
|
||
Dosyanın ilk byte'ı 0'ıncı offset'tedir. Sonraki byte 1'incş offset'te olacak biçimde her byte'ın bir offset numarası vardır.
|
||
İşte işletim sistemi açık her dosya için "o andaki işlem pozisyonunu" belirten bir offset de tutmaktadır. Bu offset'e "dosya göstericisi (file pointer)"
|
||
denilmektedir. Dosya göstericisi o anda o dosya üzerinde yapılacak okuma yazma işlemlerinin dosyanın neresinden itibaren yapılacağını belirtmektedir.
|
||
Dosya açıldığında dosya göstericisi 0'ıncı offset'tedir. Okuma ve yazma sırasında her zaman işlem dosya göstwricisinin gösterdiği offset'ten itibaren
|
||
yapılmaktadır ve dosya göstericisi okunan ya da yazılan miktar kadar otomatik olarak ilerletilmektedir. Örneğin:
|
||
,
|
||
01234
|
||
xxxxx
|
||
|
||
Burada x'ler dosyadaki byte'ları gösteriyor olsun. Dosya göstericisi de 2'inci offset'te olsun. Biz bu offset'ten iki byte okursa 2'inci ve 3'üncü
|
||
offset'teki byte'ları okumuş oluruz. İki byte okuduğumuz için dosya göstericisi de 2 ilerletilecek ve artık 4'üncü offset'i gösterir duruma gelecektir.
|
||
Dosya göstericisi adeta "kalemin ucu" gibi bir imleç belirtmektedir.
|
||
|
||
Dosya göstericisinin dosyanın en son byte'ından onrakş olmayan byte'ı göstermesi durumuna EOF (End of File) durumu denilmektedir. Örneğin:
|
||
|
||
012345
|
||
xxxxx
|
||
|
||
Burada dosya gösteicisi 4'üncü offset'te olsun. Biz de 1 byte okumuş olalım. Artık dosya göstericisi 5'inci offset'i gösterir durumdadır.
|
||
Ancak 5'inci offset'te herhangi bir byte yoktur. Dosya göstericisi artık EOF durumundadır. EOF durumundan okuma yapılamaz. Ancak yazma (eğer
|
||
açış modu da müsaitse) yapılabilir. EOF durumunda dosyaya yazma yapıldığında dosyaya byte eklenmiş olur. Dosya göstericisi EOF'ta değilse
|
||
yazım sırasında dosya büyütülmez. Dosya göstericisinin gösterdiği yerdeki byte'lar ezilerek yazım yapılır. Bir dosyanın uzunluğunu artırmanın tek yolu
|
||
dosya göstericisini EOF'a çekip yazma yapmaktır. Tabii dosya göstericisi dosyanın sonund aolmasa bile biz dosyaya dosya göstericisinin gösterdiği yerden
|
||
itibaren dosyadaki byte sayısından daha fazla byte yazarsak dosya yine büyülür dosya göstericisi de EOF durumuna çekilir. Örn3ğin:
|
||
|
||
012345
|
||
xxxxx
|
||
|
||
Burada dosya göstericisi 2'inci offset'te bulunuyor olsun. Biz burada dosyaya 5 byte yazarsak dosya 2 byte büyütülür:
|
||
|
||
01234567
|
||
xxyyyyy
|
||
|
||
Dosya göstericisi artık 7'inci offset'te olacaktır. Bu da yine EOF durumudur.
|
||
|
||
Dosya göstericisi EOF'ta olsun ve bir dosyaya 1 byte yazalım. Şimdi biz dosyaya 1 byte eklemiş oluruz. Ancak yine göstericisi EOF durumundadır.
|
||
Dolayısıyla artık yeni yazılanlar da yine dosyanın sonuna eklenecektir.
|
||
|
||
Dosya göstericisi dosya başına bir tane değil her açık dosya için bir tanedir. Yani örneğin biz aynı dosyayı fopen ile iki kez açmışsak
|
||
her iki açımın dosya göstericisi farklı olabilir:
|
||
|
||
FILE *f1, *f2;
|
||
|
||
if ((f1 = fopen("test.txt", "r") == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
if ((f2 = fopen("test.txt", "r") == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
Burada biz f1 dosya bilgi göstericisini kullanıp okuma yaparsak f2 dosya bilgi göstericisinin dosya göstericisi değişmez, f1 dosya
|
||
bilgi göstericisinin dosya göstericisi değişir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
fgetc dosya göstericisinin gösterdiği yerdne itibaren 1 byte okuyup bize okuduğu byte'ı vermektedir. Fonksiyonun ptototipi şöyledir:
|
||
|
||
int fgetc(FILE *f);
|
||
|
||
Fonksiyonun geri dönüş değerinin int türdne olması kişilere tuhaf gelebilmektedir. Ancqak fgetc başarısız olabilirve başarısızlık durumunda
|
||
EOF değerine(tipik olarak -1) geri dönmektedir. Fonksiuonun geri dönüş değeri char ya da unsigned char olsaydı fonksiyonun başarısız mı olduğu
|
||
yoksa FF nmaralı byte'ı mı okuduğu anlaşılamazdı. Halbuki fonksiyonun geri dönüş değeri int olduğu için bu durum anlaşılabilmektedir. Fonksiyon
|
||
FF nmaralı byte'ı okursa 255 değerine (000000FF değerine) başarısız olursa -1 değerine (FFFFFFFF değerine) geri dönmektedir.
|
||
|
||
fgetc temelde iki nedenden dolayı başarısız olabilir:
|
||
|
||
1) Dosya göstericisi EOF durumundadır ve bu nedenle okuma yapılamamıştır.
|
||
2) Disk ilgili ciddi ve patolojik bir problem oluşmuştur.
|
||
|
||
Diskle ilgili bu tür ciddi hatalara "IO hataları (IO error)" denilmektedir. IO hataları çok seyrek oluşabilecek hatalardır. EOF durumundna dolayı
|
||
okuma işleminin başarısız olması ise patolojik bir durum değildir. Bu durum normal bir başarısızlık durumudur.
|
||
|
||
Aşağıda dosya sonuna kadar dosyadan byte byte okuma yapıp bunu yazdıran bir program örneği verilmiştir. Programın dönüsü şöyledir:
|
||
|
||
while ((ch = fgetc(f)) != EOF)
|
||
putchar(ch);
|
||
|
||
Burada her byte okundukça dosya göstericisi bir ilerletilecek ve en sonunda dosya göstericisi EOF durumuna gelecektir. Böylece döngüden
|
||
çıkılacaktır. Tabii IO hatası oluşursa da fgetc EOF değeri ile geri dönmektedir. Bu da döngünün sonlanmasına yol açacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
int ch;
|
||
|
||
if ((f = fopen("test.txt", "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
while ((ch = fgetc(f)) != EOF)
|
||
putchar(ch);
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Son yapılan işlemin nedne başarısız olduğunu veren iki standart C fonksiyonu bulundurulmuştur: ferror ve feof. Fonksiyonların prototipleri
|
||
şöyledir:
|
||
|
||
int ferror(FILE *f);
|
||
int feof(FILE *f);
|
||
|
||
Fonksiyonlar fopen fonksiyonundna alınan dosya bilgi göstericisini parametre olarak almaktadır.
|
||
|
||
ferror fonksiyonu son yapılan işlem IO hatasından dolayı başarısz olmuşsa sıfır dışı bir değere, IO hatasındna dolayı başarısz olmamışsa 0 değerine
|
||
geri dönmektedir. feof fonksiyonu ise son yapılan işlem EOF durumundan dolayı başarısz olmuşsa sıfır dışı bir değere, EOF durumundan dolayı başarısız
|
||
olmamışsa 0 değerine geri dönmektedir. Bu iki fonksiyon tipik olarak bir başarısızlık sonrasında başarısızlığın nedenini sorgulamak için kullanılmaktadır.
|
||
Bir başarısızlık söz konusu olmadığı durumda bu fonksiyonlar 0 değerine geri döner. Özellikle feof fonksiyonunun işlevi yanlış anlaşılmaktadır. Örneğin:
|
||
|
||
012345
|
||
xxxxx
|
||
|
||
Burada dosya gösteicisi 4'üncü offset'te olsun. Biz de fgetc fonksiyonu ile dosyadan 1 byte okumuş olalım. Burada biz feof fonksiyonu ile durumu sorgulamaya
|
||
çalışsak feof 0 değerini verecektir. Halbuki dosya göstericisi EOF durumundadır. Çünkü feof ve ferror "son yapılan işlemin başarısızlığının nedenini"
|
||
tespit etmekte kullanılır. Son yapılan işlem başarısız değilse bu fonksiyonlar 0 ile geri döner. feof fonksiyonundaki yanlış anlaşılma şudur:
|
||
Yeni öğrenen kişiler sanki feof fonksiyonunun o andaki dosya göstericisinin konumuna göre durum tespiti yaptığını sanmaktadır. Halbuki feof
|
||
"son yapılan işlemin başarısızlığının nedeninin EOF nedeni ile mi olduğuna" bakmaktadır. Başka bir deyişle ferror ve feof ancak başarısızlık durumunda
|
||
anlamlı bir biçimde kullanılabilmektedir.
|
||
|
||
Pekiyi bu fonksiyonlar neden bu biçimde çalışmaktadır? İşte bu tasarım stream sisteminin tasarımı ile ilgilidir. FILE yapısı içerisinde iki
|
||
eleman vardır: feof bayrağı ve ferror bayrağı. Bu bayraklar başarısızlık durumunda başarısızlığa göre set edilmektedir. feof ve ferror fonksiyonları
|
||
aslında yalnızca bu bayrakların durumuna bakmaktadır. feof fonksiyonu dosya göstericisinin durumuna bakmaz. FILE yapısı içerisindeki feof bayrağının durumuna
|
||
bakar. Bu bayrağı da okuma fonksiyonları EOF durumundan dolayı başarısızlık durumunda set etmektedir.
|
||
|
||
Yeni öğrenenlerin sık yaptığı bir hata şudur:
|
||
|
||
while (!feof(f)) {
|
||
ch = fgetc(f);
|
||
putchar(ch);
|
||
}
|
||
|
||
Burada programcı EOF'a gelinmediği sürece işlem yapmak istemiştir. Ancak bu kod parçası kusurludur. Dosyanın son byte'ının fgetc tarafından
|
||
okunduğunu varsayalım. Bu durumda putchar son byte'ı yazdıracaktır. Ancak feof hale sıfır değerini verecektir (çünkü son işlem başarısız olmamıştır).
|
||
Bu durumda döngü yinelenecek fgetc bu sefer EOF'tan dolayı başarısız olacak putchar EOF değerini (-1 değeirni) yazdırmaya çalışacaktır.
|
||
Halbuki döngünün şöyle oluşturulması gerekir:
|
||
|
||
while ((ch = fgetc(f)) != EOF)
|
||
puthcar(ch);
|
||
|
||
Tabii aşağıdaki gibi bir döngü de oluşturulabilir:
|
||
|
||
for (;;) {
|
||
ch = fgetc(f);
|
||
if (feof(f))
|
||
break;
|
||
putchar(ch);
|
||
}
|
||
|
||
Tabii bu döngü güzel gözükmemektedir. Aşağıdaki döngüyü yeniden inceleyelim:
|
||
|
||
while ((ch = fgetc(f)) != EOF)
|
||
puthcar(ch);
|
||
|
||
Bu döngüden patolojik bir IO hatası ile de çıkmış olabiliriz. Bu tür durumlarda programcının bu IO hatasını kullanıcıya bildirmesi
|
||
iyi bir tekniktir. Bunun bir yolu şudur:
|
||
|
||
while ((ch = fgetc(f)) != EOF)
|
||
puthcar(ch);
|
||
|
||
if (ferror(f)) {
|
||
fprintf(stderr, "Unexpected IO error!..n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
Tabii aynı kontrol aşağıdaki gibi de yapılabilrdi. Ancak bunu tavsiye etmiyoruz:
|
||
|
||
while ((ch = fgetc(f)) != EOF)
|
||
puthcar(ch);
|
||
|
||
if (!feof(f)) {
|
||
fprintf(stderr, "Unexpected IO error!..n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
fputc isimli standart C fonksiyonu dosya göstericisinin gösterdiği yere bir byte yazmak için kullanılmaktadır. Fonksiyonun prototipi şöyledir:
|
||
|
||
int fputc(int ch, FILE *f);
|
||
|
||
Fonksiyonun birinci parametresi yazılacak byte'ı belirtir. Bu parametre int türden olmasına karşın buradaki sayının en düşün anlamlı byte değeri
|
||
dosyaya yazılmaktadıır. Fonksiyonun ikinci parametresi yazılacak dosyaya ilişkin dosya bilgi göstericisini belirtir. Fonksiyon başarı durumunda
|
||
yazılan byte değerine başarısızlık duruymunda EOF değerine geri döner. Başarısızlığın nedeni IO hatası olabilir. (Tabii fonksiyon EOF'tan dolayı başarısz olmaz.
|
||
Çünkü EOF durumunda dosyaya yazma yapılırsa zaten bu durum dosyaya ekleme anlamına gelmektedir.) Fonksiyonb başarısız olduğunda ferrror bayrağı set edilir.
|
||
Dolayısıyla ferror fonksiyonu da sıfır dışı bir değere geri döner.
|
||
|
||
Yazma sırasında IO hatasının muhtemel nedenlerinden bazıları şunlar olabilmektedir:
|
||
|
||
- Diskin bozuk olması
|
||
- Çıkarılabilir (removable) bir diskin (örneğin flash bellekler) o anda çıkartılması
|
||
- Diskin tamamen dolu olması
|
||
- İşletim sistemlerinde dosyaya yazılabilecek maksimum byte sayısı sınırlı olabilmektedir. Bu sınır aşılmış olabilir.
|
||
|
||
Aşağıdaki örnekte char türdne bir dizinin içerisindeki tüm byte'ler null karakter görülene kadar dosyaya yazılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
char s[] = "ankara";
|
||
|
||
if ((f = fopen("test.txt", "w")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (size_t i = 0; s[i] != '\0'; ++i)
|
||
if (fputc(s[i], f) == EOF) {
|
||
fprintf(stderr, "Unexpected IO error!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İşletim sistemine göre dosya byte'lardan oluşmaktadır. Dosyanın içerisinde ne olduğunun bir önemi yoktur. Çünkü her şey byte'lardan oluşmaktadır.
|
||
Ancak C'de işlemleri biraz kolaylaştırabilmek için dosyalar "text" ve "binary" olmak üzere ikiye ayrılmaktadır. İçerisinde yalnızca yazıların bulunduğu
|
||
Örneğin Notepad'te oluşturduğumuz bir dosya text dosyadır. Ya da örneğin bir C programının akynak kodu bir text dosyadır. Ancak derleme ve link işlemi
|
||
sonucunda elde ettiğimiz ".obj", ".exe" dosyalarının içerisinde yazı yoktur. Bu dosyalar binary dosyalardır. Örneğin "jpeg" dosyaları, "bmp" dosyaları
|
||
binary dosyalardır. Ancak "html" dosyaları text dosyalardır. Eğer bir dosyayı editöre çektiğimizde anlamlı şeyler görebiliyorsak bu bir text dosyadır.
|
||
Örneğin biz bir exe dosyayı bir text editörle görüntülemek istesek bu text editör makine kodlarının byte'larını bize karakter olarak gösterecek ve
|
||
bunun sonucunda tuhaf karakterler görüntülenecektir. İşletim sistemi düzeyinde text dosya binary dosya diye bir ayrım yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
75. Ders - 16/03/2023 - Persembe
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir yazıda bir karakterin aşağı satırın başında görüntülenmesi için UNXI/Linux ve macOS sistemlerinde '\n' karakteri kullanılmaktadır. Bu karaktere
|
||
LF (Line Feed - 0A numaralı karakter) karakteri denilmektedir. Ancak Windows sistemlerinde (eskiden DOS sistemlerinde de böyleydi) bir karakterin
|
||
aşağı satırın başında görüntülenmesi için '\r' ve '\n' karakterlerinin bir arada peş peşe kullanılması gerekmektedir. '\r' karakterine
|
||
"Carriage Return (CR) - 0D numaralı karakter" denilmektedir. Yani Windows sistemlerinde "aşağı satırın başına geç" anlamında CR/LF çifti kullanılmaktadır.
|
||
|
||
C'de text dosyalarla işlemleri kolaylaştırabilmek için dosya açış modları "text mode" ve "binary mode" biçiminde ikiye ayrılmıştır. default açış modu
|
||
text moddur. Binary modda açış için açış modunun sonuna "b" harfi eklenir. Örneğin "r" text mod anlamına gelir. Ancak "rb" binaey mod anlamına gelir.
|
||
|
||
Text mode Binary Mode
|
||
|
||
"r" "rb"
|
||
"w" "wb"
|
||
"r+" "r+b"
|
||
"w+" "w+b"
|
||
"a" "ab"
|
||
"a+" "a+b"
|
||
|
||
Pekiyi dosyanın text mod ile binary mod açımı arasında ne farklılık vardır?
|
||
|
||
1) Dosya text modda açılmışsa dosyaya '\n' karakteri yazılmak istendiğinde UNIX/Linux ve macOS sistemlerinde dosyaya yalnızca bir byte'lık
|
||
LF (0A) karakteri basılır. Ancak Windows sistemlerinde CR/LF (OD/0A) biçiminde iki karakter yazdırılır. Ancak dosya binary modda açılmışsa artık yazısal bir
|
||
anlam dikkate alınmayacağı için '\n' karakteri sistem ne olursa olsun LF (0A) byte'ı olarak yazıdırlacktır. Yani text modda '\n' karakterinin
|
||
dosyaya yazılması ilgili işletim sistemine göre o işletim sistemindeki tanıma göre yapılmaktadır. Ancak binary modda böyle bir durum
|
||
söz konusu değildir. Bu durumda biz bir dosyayı Windows sistemlerinde "text" modda açarsak o dosyaya '\n' karakterini yazdığımızda aslında
|
||
dosyaya \r\n karakterleri birlikte yazıdırılacaktır. Ancak binary modda dosyayı açarsak '\n' karakteri içimn yine '\n' karakteri basılacaktır.
|
||
Tabii UNIX/Linux ve macOS sistemlerinde bu anlamda "text mod" ile "binary mod" arasında bir farklılık söz konusu olmayacaktır. Ancak taşınabilirlik
|
||
için programcının yazısal içeriğe sahip olmayan dosyaları binary modda açması gerekir.
|
||
|
||
2) Dosya text modda açılmışsa Windows sistemlerinde dosya göstericisi CR/LF çiftinin başını gösterir durumdaysa dosyadan bir karakter okunmak
|
||
istendiğinde her iki karakter de okunur. Ancak okumanın sonucu olarak LF (\n) karakteri verilir. Yani Windows'ta text modda LF ('\n') için CR/LF karakterleri
|
||
yazdırıldığı için okunurken de ters işlem yapılmaktadır. Bu iki karakterin her ikisi de okunup sanki LF ('\n') karakteri okunmuş gibi davranılmaktadır.
|
||
Tabii UNIX/Linux ve macOS sistemlerinde dosya text modda açılmışsa dosya göstericisi CR/LF çifitini gösterdiğinde dosyadan bir byte okunacağı
|
||
zaman CR (\r) karakteri verilecektir. Sonra yine bir byte okunacağı zaman bu kez LF (\n) karakteri verilecektir. Windows'ta dosya binary modda açılmışsa
|
||
ve dosya göstericisi CR/LF çiftini gösteriyorsa dosyadan bir karakter okunduğunda CR karakteri okunur. Çünkü binary modda yazısal bir anlam uygulanmamaktadır.
|
||
UNIX/Linux ve macOS sistemlerinde de okuma işleminde yine binary mod ya da text modda bir şey değişmemektedir.
|
||
|
||
O halde açış modu için şu tavsiyelerde bulunulabilir: Eğer bir dosyayı yazısal amaçlı açacaksanız text modda açmalısınız. Ancak yazısal olmayan bir dosya
|
||
üzerinde işlem yapacaksanız dosyayı binary modda açmalısınız.
|
||
|
||
Aşağıdaki örnekte dosyay text ve binary modd açarak oluşan durumu gözlemleyiniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
|
||
if ((f = fopen("test.txt", "w")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
fputc('a', f);
|
||
fputc('\n', f);
|
||
fputc('b', f);
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Ekranı temsil eden stdout dosyasının ve klavyeyi temsil eden stdin dosyasının "text modda "açaılmış olduğu varsayılmaktadır. Bu durumda biz Windows'ta
|
||
imleci aşağı satırın başına geçirmek için ekrana yalnızca LF karakterini göndermeliyiz. Ekran CR/LF karakterlerini kullanarak imleci aşağı satırın
|
||
başına geçirecektir. Yinelemek gerekirse Windows'ta biz imleci aşağı satırın başına geçirmek için CR/LF karakterlerini ekrana göndermemeliyiz.
|
||
Yalnızca LF karakterini ekrana göndermeliyiz. Zaten bugüne kadar da imleci ekranda aşağı satırın başına geçirebilmek için yalnızca LF karakterini
|
||
kullandık.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Genel olarak Windows'ta oluşturulmuş olan bir text dosyanın UNIX/Linux ve macOS sistemlerinde okunup ekrana yazdırılmasında bir sorun oluşmaz.
|
||
Çünkü CR/LF çifti okunurken UNIX/Linux ve macOS sistemlerinde iki ayrı karakter olarak okunacaktır. Bu sistemlerde CR karakterş "imleci bulunulan
|
||
satırınb başına geçirdiği" için hemen arkasındaki LF ise "aşağı satıırn başına geçirdiği" için bir sorun oluşmayacaktır. Tabii text dosya daha fazla
|
||
yer kaplar durumda olacaktır. Bunun tersi olan durum eskiden DOS ve Windows sistemlerinde bozuk bir görüntünün oluşmasına yol açıyordu. Çünkü eskiden
|
||
DOS ve Windows sistemlerinde CR karakteri "bulunulan satırın başına geç" ancak LF karakteri "ehemen aşağıdaki satıra geç (sütunu muhafaza ederek)"
|
||
anlamına geliyordu. Ancak Windows daha sonraları artık LF karakteri için de "aşağı satırın abşına geç" semantiği uygulamaya başlamıştır. Fakat bu sistemlerde
|
||
geçmişe doğru uyumu korumak için yine aşağı satırın başına geçmek amacıyla CR/LF karakterleri kullanılmalıdır. Bu dönüşümğ yapan çeşitli utility
|
||
programlar bulunmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
fprintf fonksiyonu printf fonksiyonunun dosyaya yazan biçimidir. Tabii aslında ekran da bir dosya olarak ele alınmaktadır. Bu durumda ters ifade de
|
||
geçerlidir. Yani printf fonksiyonu aslında fprintf fonksiyonunun stdout dosyasına (ekrana) yazan biçmidir. fprintf fonksiyonun prototipi ile
|
||
printf fonksiyonunun prototipini aşağıda veriyoruz:
|
||
|
||
int fprintf(FILE *f, const char *format, ...);
|
||
int printf(const char *format, ...);
|
||
|
||
Görüldüğü gibi fonksiyonlar arasındaki tek fark fprintf fonksiyonunun bir fazla parametreye sahip olmasıdır. fprintf fonksiyonu tamamen printf
|
||
fonksiyonu gibi çalışmaktadır. Tek fark bu fonksiyonun ekran yerine dosyaya yazmasıdır. Tabii fprintf fonksiyonu ile dosyaya bir şeyler yazdıracaksak
|
||
dosyanın text modda açılmış olması uygundur. Her iki fonksiyon da yazıdırılan karakter sayısı ile geri dönmektedir. Fonksiyonların prototiplerinin
|
||
sonunda bulunan "... (ellipsis)" fonksiyonların çağrılırken istenildiği kadar çok argüman alabileceği anlamına gelir.
|
||
|
||
fprintf ve printf fonksiyonlarının geri dönüş değerleri genellikle programcı tarafından kullanılmaz. Ancak bazen faydalı birtakım işlemler için
|
||
bu geri dönüş değerlerinden faydalanılmaktadır.
|
||
|
||
fprintf fonksiyonu ile dosyaya yazdırılan şeylerin yazı gibi yazdırıldığına dikkat ediniz.
|
||
|
||
Aşağıdaki örnekte bir dosya text modda açılmış sonra da fprintf fonksiyonu ile oradan okuma yapılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
|
||
if ((f = fopen("test.txt", "w")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (int i = 0; i < 100; ++i)
|
||
fprintf(f, "Number: %d\n", i);
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Daha önce ekranın bir dosya gibi davrandığını belirtmiştik. İşte ekran dosyasına ilişkin dosya bilgi göstericisi "stdout" ismiyle kullanılabilmektedir.
|
||
Örneğin:
|
||
|
||
fprintf(stdout, "this is a tes\n");
|
||
|
||
Burada ekrana (stdout dosyasın) yazı yazdıırılmıştır. O halde:
|
||
|
||
printf(......);
|
||
|
||
çağrısı ile aşağıdaki çağrı eşdeğerdir:
|
||
|
||
fprintf(stdout, .....);
|
||
|
||
Zaten standartlarda da printf ve fprintf fonksiyonları ayrı ayrı anlatılmamıştır. fprintf fonksiyonu açıklanmıştır. printf fonksiyonunun fprintf
|
||
fonksiyonunun stdout dosyasına yazan biçimi olduğu söylenmiştir.
|
||
|
||
Yani aslında printf gizli bir dosya fonksiyonudur. printf fonksiyonunun genel hali fprintf fonksiyonudur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
printf("this is a test\n");
|
||
fprintf(stdout, "this is a test\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
printf ile fprintf arasındaki scanf ile fscanf arasındaki ilişki ile benzerdir. scanf fonksiyonu stdin dosyasından (klavyeden) okuma yapmaktadır.
|
||
fscanf ise herhangi bir dosyadan okuma yapmaktadır. İki fonksiyonun da prototiplerini aşağıda veriyoruz:
|
||
|
||
int scanf(const char *format, ...);
|
||
int fscanf(FILE *f, const char *format, ...);
|
||
|
||
Bu fonksiyonlar başarı durumunda okunabilen parça sayısına geri dönmektedir. Eğer hiç okuma yapılamadan fonksiyonlar EOF ile karşılaşırsa EOF değerine
|
||
geri dönmektedir.
|
||
|
||
Aşağıdaki örnekte "test.txt" dosyasının içeriği şöyledir:
|
||
|
||
10 20
|
||
|
||
fscanf ile bu dosyadan okuma yapılırken sanki bu içerik klavyeden girilmiş gibi bir etki söz konusu olacaktır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
int a, b;
|
||
|
||
if ((f = fopen("test.txt", "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
fscanf(f, "%d%d", &a, &b);
|
||
|
||
printf("a = %d, b = %d\n", a, b);
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
scanf ve fscanf fonksiyonları kabaca şöyle çalışmaktadır: Bu fonksiyonlar karakterleri tek okuyarak ilerler. Format karaktyeri ile uyuşmayan
|
||
bir karakterle karşılaşırlarsa işlemini sonlandırılar ve başarılı bir biçimde yerleştirilen nesne sayısına geri dönerler. Aşağıdaki gibi
|
||
bir program olsun:
|
||
|
||
,#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
int a = -1, b = -1;
|
||
int result;
|
||
|
||
result = scanf("%d%d", &a, &b);
|
||
|
||
printf("a = %d, b = %d\n", a, b);
|
||
printf("result = %d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Biz klavyeden şu girişi yapmış olalım:
|
||
|
||
10 20
|
||
|
||
scanf girişler arasındaki boşluk karakterlerini dikkate almamaktadır. Dolayısıyla burada başarılı bir biçimde iki yerleşimi yaptığı için scanf
|
||
2 değeri ile geri dönecektir. Girişimiz şöyle olsun:
|
||
|
||
10 ali
|
||
|
||
Bu durumda scanf a için okumayı yapar. Ancak b için okumayı yapamaz ve işleminşi sonlandırır. 1 değeri ile geri döner. Şimdi girişin şöyle
|
||
yapıldığını düşünelim:
|
||
|
||
ali veli
|
||
|
||
scanf a için okumayı yapamayacaktır. Bu durumda işlemini sonlandırı ve 0 ile geri döner. Şimdi girişi şöyle yapmış olalım:
|
||
|
||
10ankara izmir
|
||
|
||
Burada scanf 10 değerini a'ya yerleştirir ve " değeri ile geri döner.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dosya açıldığında dosya göstericisi 0'ıncı offset'tedir. Daha sonra okume ve yazma yapıldıkça dosya göstericisi otomatik olarak ilerletilmektedir.
|
||
Ancak biz dosyanın belirli bir yerinden bir şeyi okumak isteyebiliriz ya da belirli bir yerine bir şeyler yazmak isteyebiliriz. Bunun için dosya
|
||
göstericisini konumlnadırmamız gerekir. Bu işlem fseek isimli fonksiyonla yapılmaktadır.
|
||
|
||
fseek fonksiyonunun prototipi şöyledir:
|
||
|
||
int fseek(FILE *f, long int offset, int whence);
|
||
|
||
Fonksiyonun birinci parametresi dosya göstericisi konumlandırılacak dosyanın bilgi göstericisini almaktadır. İkinci parametre konumlandırma offset'ini
|
||
belirtmektedir. Fonksiyonun ikinci parametresi konumlandırma offset'ini belirtmektedir. Üçücüncü parametre ise konumlandırmanın nerereye göre
|
||
yapılacağını başka bir deyişle konumlandırma orijinini belirtmektedir. Bu üçüncü parametre yalnızca 0, 1 ya da 2 değerini alabilir.
|
||
Diğer değerler geçirsizdir. 0 değeri konumlandırmanın dosyanın başından itibaren yapılacağı anlamına gelmektedir. Bu durumda ikinci parametrenin >= 0
|
||
olması gerekir. 1 değeri konumlandırmanın o anda dosya göstericisinin gösterdiği yerden itibaren yapılacağı anlamına gelmektedir. Bu duurmda ikinci
|
||
parametre pozitif, negatif ya da 0 olabilir. Pozitif değer "bulunulan yerden n ileriye", negatif değer "bulunulan yerden n geriye" konumlandırma anlamına gelir.
|
||
Örneğin:
|
||
|
||
fseek(f, -1, 1);
|
||
|
||
Burada dosya göstericisi her neredeyse bir geriye konumlandırılmıştır. Örneğin:
|
||
|
||
fseek(f, 10, 0);
|
||
|
||
Burada dosya göstericisi 10'uncu offset'e konumlandırılmıştır. Üçüncü parametrenin 2 olması konumlandırmanın EOF pozisyonundan itibaren yapılacağı
|
||
anlamına gelmektedir. Bu durumda ikinci parametrenin <= 0 olması gerekir. Örneğin:
|
||
|
||
fseek(f, 0, 2);
|
||
|
||
Burada konumlandırma EOF pozisyonuna yapılmaktadır. Örneğin:
|
||
|
||
fseek(f, -1, 2);
|
||
|
||
Burada konumlandırma dosyanın son byte'ına yapılmaktadır. Üçücnü parametre okunabilirliği artırmak için <stdio.h> içerisinde aşağıdaki
|
||
üç sembolik sabitle de define edilmiştir:
|
||
|
||
#define SEEK_SET 0
|
||
#define SEEK_CUR 1
|
||
#define SEEK_END 2
|
||
|
||
Örneğin:
|
||
|
||
fseek(f, 10, SEEK_SET);
|
||
fseek(f, 0, SEEK_END);
|
||
|
||
fseek fonksiyonuna olmayan bir offset'i parametre oalrak geçrirsek ya da üçüncü parametreyi uygunsuz bir biçimde girersek fseek başarısız olabilir.
|
||
fseek başarı durumunda 0 değerine, başarısızlık durumunda sıfır dışı herhangi bir değere geri dönmektedir. Ancak programcılar genel olarak
|
||
fonksiyonun başarısını kontrol etmezler.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
76. Ders - 31/03/2023 - Cuma
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte dosya göstericisi EOF durumuna çekilerek fprintf fonksiyonuyla dosyanın sonuna bir şeyler yazılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
|
||
if ((f = fopen("test.txt", "r+")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
fseek(f, 0, SEEK_END);
|
||
|
||
fprintf(f, "this is a test...\n");
|
||
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C standartlarına göre dosya okuma ve yazma modunda açılmışsa okuma işleminden yazma işlemine, yazma işleminden de okuma işlemine geçilirken
|
||
dosya göstericisinin konumlandırılması gerekmektedir. Örneğin dosyadaki byte'lar şunlar olsun:
|
||
|
||
abc
|
||
|
||
Dosya göstericisi de a byte'ını gösteriyor olsun. Aşağıdaki gibi bir işlem tanımsız davranışa (undefined behavior) yol açacaktır:
|
||
|
||
ch = fegtc(f);
|
||
fputc('x', f);
|
||
|
||
Burada 'a' byte'ı okunur. Dosya göstericisi ilerletilir ve 'b' byte'ını gösteriyor duruma gelir. Ancak bu pozisyona yazma yapıldığında
|
||
tanımsız davranış oluşacaktır. Bu tür durumlarda mutlaka fseek fonksiyonuyla konumlandırma yapılması gerekir. Konumlandırmaya gereksinim olmasa bile
|
||
fseek çağrısının yapılması gerekir. Örneğin:
|
||
|
||
ch = fegtc(f);
|
||
fseek(f, 0, 1);
|
||
fputc('x', f);
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Dosya göstericisini konumlandırmak için rewind isminde de bir fonksiyon bulunmaktadır. Bu fonksiyon her zaman dosya göstericisini dosyanın başına (yani 0'ıncı
|
||
offset'e çeker). Prototoipi şöyledir:
|
||
|
||
void rewind(FILE *f);
|
||
|
||
Yani:
|
||
|
||
rewind(f);
|
||
|
||
çağrısı ile,
|
||
|
||
fseek(f, 0, 0);
|
||
|
||
çağrısı tamamen eşdeğerdir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bazen dosya göstericisinin o anki konumunu almak isteyebiliriz. Böylece daha sonra o offset'e fseek ile konumlandırma yapıp oradan bir şeyler okuyabilir
|
||
ya da oraya bir şeyler yazabiliriz. Bunun için ftell fonksiyonu kullanılmaktadır. Fonksiyonun prototipi şöyledir:
|
||
|
||
long ftell(FILE *f);
|
||
|
||
Fonksiyonun offset belirten geri dönüş değerinin long olduğuna dikkat ediniz. Bu long değer her zaman dosyanın başından itibaren bir değer belirtmektedir.
|
||
|
||
Aşağıdaki örnekte dosya göçstericisi önce EOF pozisyonuna çekilmiş sonra da dosya göstericisinin değeri elde edilmiştir. Dolayısıyla aslında dosyanın
|
||
byte uzunluğu elde edilmiş olmaktadır. Tabii dosya uzunluğunu bu biçimde elde etmek aslında iyi bir teknik değildir. Ancak C'de standart fonksiyonlar kullanılarak
|
||
dosya uzunluğunu elde etmenin başka bir yolu da yoktur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
long size;
|
||
|
||
if ((f = fopen("test.txt", "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
fseek(f, 0, SEEK_END);
|
||
size = ftell(f);
|
||
printf("%ld\n", size);
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
fgets isimli standart C fonksiyonu text bir dosyadan bir satır okumak için kullanılmaktadır. Fonksiyonun prototipi şöyledir:
|
||
|
||
char *fgets(char *s, int n, FILE *f);
|
||
|
||
Fonksiyonun birinci parametresi satırdaki bilgilerin yerleştirileceği dizinin adresini belirtir. İkinci parametre birinci parametrede belirtilen dizinin
|
||
uzunluğunu belirtmektedir. Fonksiyon hiçbir zaman burada belirtilen sayıdan daha fazla byte'ı diziye yerleştirmeye çalışmaz. Son parametre ise
|
||
üzerinde işlem yapılacak dosyaya ilişkin dosya bilgi göstericisini belirtmektedir. fgets fonksiyonu şöyle çalışmaktadır:
|
||
|
||
- Fonksiyon dosya göstericisinin gösterdiği yerden itibaren '\n' karakterini görene kadar ('\n' karakteri de dahil olmak üzere) ya da en fazla n - 1
|
||
kadar karakteri birinci parametresiyle belirtilen adresten itibaren diziye yerleştirir. Yazının sonuna da null karakteri ekler. Yani eğer
|
||
n değeri dosya göstericisinin gösterdiği yerden itibaren satır sonuna kadarki ('\n' karakteri de dahil) karakter sayısından büyükse bu durumda fonksiyon
|
||
'\n' karakterini de diziye yerleştirir. '\n' karakterinden sonra null karakteri de diziye ekler ve işlemini sonlandırır. Eğer n değeri dosya göstericisinin
|
||
gösterdiği yerden itibaren satır sonuna kadarki ('\n karakteri dahil) karakter sayısına eşit ya da ondan daha küçük ise bu durumda fonksiyon n - 1 tane
|
||
karakteri diziye yerleştirir ve dizinin sonuna null karakteri de ekleyerek işlemini sonlnadırır. Örneğin satırda şunlar olsun:
|
||
|
||
ankara\n
|
||
|
||
biz de şu çağrıyı yapmış olalım:
|
||
|
||
char buf[100];
|
||
|
||
fgets(buf, 100, f);
|
||
|
||
Burada buf dizisinin içerisinde şunlar olacaktır:
|
||
|
||
ankara\n\0....
|
||
|
||
Şimdi de şu çağrıyı yapmış olalım:
|
||
|
||
char buf[5];
|
||
|
||
fgets(buf, 5, f);
|
||
|
||
bu durumda buf dizisinin içerisinde şunlar olacaktır:
|
||
|
||
anka\0
|
||
|
||
- Eğer fgets çağrıldığında '\n' karakteri daha görülmeden EOF ile karşılaşılmışsa bu durumda fgets okuyabildiği kadar karakteri okur yine dizinin
|
||
sonuna null karakteri yerleştirir ve işlemini sonlandırır. Örneğin:
|
||
|
||
ankaraEOF
|
||
|
||
Şimdi biz şu çağrıyı yapmış olalım:
|
||
|
||
char buf[100];
|
||
|
||
fgets(buf, 100, f);
|
||
|
||
buf dizisine şunlar yerleştirilecektir:
|
||
|
||
ankara\0
|
||
|
||
fgets normal olarak birinci parametresiyle belirtilen adresin aynısıyla geri döner. Ancak fgets hiç okuma yapmadan EOF ile karşılaşırsa
|
||
NULL adresle geri dönmektedir. Bu durumda fgets diziye herhangi bir şey yerleştirmez.
|
||
|
||
Tabii Windows sistemlerinde alt satırın başına geçme işlemi CR/LF karakter çiftiyle yapılmaktadır. Dosya text modda açılmışsa (default durum)
|
||
zaten bu durumda bu iki karakter okunur ancak diziye yalnızca LF ('\n') karakteri yerleştirilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir dosyayı satır satır ekrana (stdout dosyasına) yazdırmak istediğimizi düşünelim. Bunu nasıl yapabiliriz? fgets fonksiyonunun '\n' karakterini de
|
||
dizye yerleştirdiğine dikkat ediniz. Satırı yazdırırken ayrıcı '\n' karakterinin yazdırılmaması gerekir. Örneğin:
|
||
|
||
while (fgets(buf, 4096, f) != NULL)
|
||
printf("%s", buf);
|
||
|
||
Burada printf fonksiyonunun formak karakyterlerinin sonunda '\n' karakterinin olmadığına dikkat ediniz.
|
||
|
||
Aşağıdaki örnekte dosya satır satır yazdırılmıştır.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
char buf[4096];
|
||
|
||
if ((f = fopen("test.txt", "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
while (fgets(buf, 4096, f) != NULL)
|
||
printf("%s", buf);
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aslında yukarıdaki programda biz dizi uzunluğunu küçk tutsak dosyayı satır satır okuyamayız ama dosyanın sonuna kadar her şeyi okuyup yazdırabiliriz.
|
||
n = 3 değeri ile programı yeniden aşağıdaki gibi çalıştırabilirizniz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
char buf[3];
|
||
|
||
if ((f = fopen("test.txt", "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
while (fgets(buf, 3, f) != NULL)
|
||
printf("%s", buf);
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi biz bir satırın tamamını okuyabildiğimizi nasıl anlayabiliriz? Örneğin:
|
||
|
||
fgets(buf, 1024, f);
|
||
|
||
Burada satır 1023 karakterden daha büyük de olabilir. Bunu anlamanın tek yolu okunan bilgilerin sonunda '\n' karakterinin olup olmadığını kontrol etmektedir.
|
||
Örneğin:
|
||
|
||
if (fgets(buf, 1024, f) != NULL) {
|
||
if (buf[strlen(buf) - 1] != '\n'){ /* tam satır okunmamış */
|
||
....
|
||
}
|
||
...
|
||
}
|
||
|
||
Pekiyi fgets ile en az kaç karakter okunabilir. fgets fonksiyonun ikinci parametresinin 1 olması anlamsızdır ama geçerlidir. Bu durumda null
|
||
karakter diziye yerleştirileceğine göre dizinin 1 elemanına null karakter yerleştirilir. Ancak dosyadan okuma yapılmayacağı için dosya göstericisi
|
||
de konum değiştirmez.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
gets fonksiyonunun 2011 yılında C standartlarından çıkartıldığını belirtmiştik. Bunun yerine standartlara "isteğe bağlı (optional)" bir gets_s fonksiyonu
|
||
eklendi. Ancak bu gets_s fonksiyonu UNIX/Linux'ta kullanılan gcc ve clang derleyicilerinin standart C kütüphanelerinde bulunmamaktadır. Bu durumda
|
||
C programcıları genellikle gets fonksiyonu yerine fgets fonksiyonunu kullanmaktadır. fgets fonksiyonun dosya bilgi göstericisi belirten üçüncü parametresi
|
||
"stdin" olarak geçilirse bu durumda stdin dosyasından okuma yapılır. Örneğin:
|
||
|
||
char buf[1024];
|
||
|
||
fgets(buf, 1024, stdin);
|
||
|
||
Ancak bu kullanımın gets fonksiyonundan bir farkı vardır. gets fonksiyonu satırı sonlandırırken bastığımız ENTER tuşu yerine tampona yerleştirilen '\n'
|
||
karakterini tampondan atar ancak diziyte yerleştirmez. Fakat fgets fonksiyonu bu '\n' karakterini de tıpkı dosyalarda olduğu gibi diziye yerleştirmektedir.
|
||
Örneğin:
|
||
|
||
char buf[10];
|
||
|
||
fgets(buf, 10, stdin);
|
||
|
||
Burada biz klavyeden "ali" yazıp ENTER tuşuna basalım. fgtes diziye şunları yerleştirecektir:
|
||
|
||
ali\n\0
|
||
|
||
Halbuki burada gets olsaydı diziye şunlar yerleştirilirdi:
|
||
|
||
ali\0
|
||
|
||
O halde fgets aslında gets_s fonksiyonu gibi davranmamaktadır. İşte fgets fonksiyonunu gets gibi kullanmak için yazının sonundaki '\n' karakterini
|
||
bulup onun yerine null karakteri yerleştirmek gerekir. Örneğin:
|
||
|
||
char buf[10];
|
||
char *str;
|
||
|
||
fgets(buf, 10, stdin);
|
||
|
||
if ((str = strchr(buf, '\n')) != NULL)
|
||
*str = '\0';
|
||
|
||
Buradaki kontrol size anlamsız gelebilir. Çünkü siz her zaman yazının sonunda zaten '\n' olması gerekeceğini düşünülebilirsiniz. Halbuki satırdaki karakter sayısı
|
||
fazla ise fgets bu durumda '\n' karakterini yerleştirmeyecektir. Ayrıca burada henüz anlatmadığımız başka birtakım süreçler söz konusu olabilmektedir.
|
||
(Örneğin stdin dosyası başka bir dosyaya yönlendirilmiş olabilir ve bu dosyanın sonunda '\n' karakteri olmayabilir.) Aslında yine henüz
|
||
bilmediğimiz klavyedne EOF etkisi yaratma konusu nedeniyle fgets fonksiyonun da geri dönüş değerinin NULL adres olup olmadığının kontrol edilmesi gerekir. Örneğin:
|
||
|
||
char buf[10];
|
||
char *str;
|
||
|
||
if (fgets(buf, 10, stdin) != NULL) {
|
||
if ((str = strchr(buf, '\n')) != NULL)
|
||
*str = '\0';
|
||
...
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <string.h>
|
||
|
||
int main(void)
|
||
{
|
||
char buf[10];
|
||
char *str;
|
||
|
||
if (fgets(buf, 10, stdin) != NULL) {
|
||
if ((str = strchr(buf, '\n')) != NULL)
|
||
*str = '\0';
|
||
puts(buf);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C'nin en önemli dosya fonksiyonlarından ikisi fread ve fwrite fonksiyonlarıdır. Bu fonksiyonlar genellikle "binary" dosyalarda bellek ile dosya
|
||
arasında transfer yapmak için kullanılmaktadır. Fonksiyonların prototipleri şöyledir:
|
||
|
||
size_t fread(void *ptr, size_t size, size_t n, FILE *f);
|
||
size_t fwrite(const void *ptr, size_t size, size_t n, FILE *f);
|
||
|
||
Bu fonksiyonların birinci parametreleri bellek transfer adresini belirtir. Fonksiyonlar ikinci ve üçüncü parametrelerin çarpımı kadar byte transfer
|
||
etmektedir. Fonksiyonun son parametresi dosyaya ilişkin dosya bilgi göstericisini belirtir. Genelikle ikinci parametre dizinin bir elemanının byte uzunluğu
|
||
biçiminde üçüncü parametre ise dizinin uzunluğu biçiminde girilmektedir. İki değer çarpıldığında dizinin toplam byte sayısı elde edilir.
|
||
Fonksiyonlar üçüncü parametrede belirtilen okunan ya da yazılan parça sayısı ile geri dönmektedir. EOF durumunda fread fonksiyonu okuma yapamayacağı için
|
||
0 ile geri dönmektedir. Fonksiyonlar başarısız olduğunda da 0 ile geri dönmektedir. Bu durumda örneğin fread fonksiyonu 0 ile geri dönerse başarısızlığın
|
||
EOF'tan kaynaklanıp kaynaklanmadığını anlayabilmek için ferror ya da feof fonksiyonlarının kullanılması gerekir.
|
||
|
||
Aşağıdaki örnekte bir binary dosyaya 10 elemanlı int dizi tek hamlede fwrite fonksiyonuyla yazılmış ve sonra da yine tek hamlede fread fonksiyonuyla
|
||
okunmuştur.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
int a[10] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
|
||
int b[10];
|
||
|
||
if ((f = fopen("test.dat", "w+b")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
if (fwrite(a, sizeof(int), 10, f) != 10) {
|
||
fprintf(stderr, "cannot write file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
rewind(f);
|
||
|
||
if (fread(b, sizeof(int), 10, f) != 10) {
|
||
fprintf(stderr, "cannot read file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d ", b[i]);
|
||
printf("\n");
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
fread fonksiyonu dosya göstericisinin gösterdiği yerdne itibaren eğer ikinci ve üçüncü parametresiyle belirtilen miktarda byte dosyada yoksa
|
||
okuyabildiği kadar byte'ı okur okuyabildiği parçe sayısına geri döner. Yani biz fread ile talep ettiğimizden daha az byte okuyabiliriz. Bu durum
|
||
bir hata kabul edilmemektedir. Örneğin dosyada 20 byte olsun. Biz de aşağıdaki gibi bir okuma yapmak isteyelim:
|
||
|
||
unsigned char buf[100];
|
||
size_t result;
|
||
|
||
result = fread(buf, 1, 100, f);
|
||
|
||
Biz burada her bir parçası 1 byte olan 100 parçe okumak istedik. Halbuki dosyada 20 byte vardır. O zaman biz her bir parçası 1 byte olan 20 parça okuyabiliriz.
|
||
Bu durumda fread 20 değeri ile geri dönecektir.
|
||
|
||
fread ve fwrite fonksiyonlarının iki paremetresinin çarpımı kadar byte okumaya çalıştığına ancak üçüncü parametresinde belirtilen parça sayısına geri
|
||
döndüğüne dikkat ediniz. Bu tasarım biraz tuhaf ve kullanışsız gibidir. Ancak fonksiyonlar bu biçimde tasarlanmıştır.
|
||
|
||
fread fonksiyonu parçalı okuma yapabilir. Bu durumda okumanın yapıldığı dizinin son elemanı "kararsız (interminate)" bir durumda kalır. Örneğin dosyada
|
||
10 byte olsun. int türünün uzunluğu da 4 byte olsun. Şöyle bir okuma yapmış olalım:
|
||
|
||
int a[10];
|
||
size_t result;
|
||
|
||
result = fread(a, sizeof(int), 10, f);
|
||
|
||
Burada fread fonksiyonu 10 byte'ın hepsini okur. Ancak int türü 4 byte olduğu için a dizisinin 2'incisli elemanı parçalı bir okumaya maruz kalmıştır
|
||
ve "kararsız (indeterminate)" bir durumdadır. fread fonksiyonu başarılı olarak okunan parça sayısına geri dönmektedir. Yani bu örnekte fread fonksiyonu 2
|
||
ile geri dönecektir. fread bu örnekte 10 byte okuduğu halde başarılı okuduğu parça sayısı 2 olduğu için 2 ile geri dönecektir. Maalesef bu tasarım
|
||
biraz gereksiz bir karmaşıklık oluşturmaktadır. Programcıların çoğu fonksiyonun ikinci parametresini 1 değerinde tutup açıkça byte okuması yapar. Örneğin:
|
||
|
||
int a[10];
|
||
size_t result;
|
||
|
||
result = fread(a, sizeof(int), 10, f);
|
||
|
||
Bu işlemin benzeri şöyledir:
|
||
|
||
int a[10];
|
||
size_t result;
|
||
|
||
result = fread(a, 1, sizeof(int) * 10, f);
|
||
|
||
Dosyada yeteri kadar byte olduğunu düşünelim. Birinci durumda fread 10 ile geri döner. Ancak ikinci durumda 40 ile geri dönecektir.
|
||
|
||
fread fonksiyonu hiçbir okuma yapamadan EOF ile karşılaşırsa ya da IO hatası oluşursa 0 ile geri döner. Bu durumun kontrol edilmesi gerekebilir.
|
||
|
||
Örneğin:
|
||
|
||
result = fread(a, 1, sizeof(int) * 10, f);
|
||
if (result == 0) {
|
||
if (ferror(f)) {
|
||
fprintf(stderr, "IO error!..\n);
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
/* end of file encountered */
|
||
}
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
fread ve fwrite fonksiyonlarını kullanarak dosya kopyalaması yapabiliriz. Bunun için kaynak dosyadan belli bir büyülükte byte bir döngü içerisinde okunur.
|
||
Hedef dosyaya yazılır. Döngünün yapısı şöyle olacaktır:
|
||
|
||
char buf[BUFFER_SIZE];
|
||
FILE *fs, *fd;
|
||
...
|
||
|
||
while ((result = fread(buf, 1, BUFFER_SIZE, fs)) > 0)
|
||
if (fwrite(buf, 1, result, fd) != result) {
|
||
fprintf(stderr, "cannot write file!...\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
if (ferror(fs)) {
|
||
fprintf(stderr, "cannot read file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
Burada BUFFER_SIZE kopyalama sırasında kullanılacak çanak büyüklüğünü belirtmektedir. Bu döngüden iki nedenle çıkılabilir. Birincisi hiç byte okuyamadan
|
||
EOF ile karşılaşılması ikincisi ise IO hatasının olmasıdır. Bu nedenle döngü çıkışında fread fonksiyonunun neden 0 ile geri döndüğü sorgulanmıştır.
|
||
Burada BUFFER_SIZE değerinin 1024 olduğunu düşünelim. Kaynak dosya da 1020 byte uzunlupunda olsun. Döngüde fread önce 1024 byte'ı okur ve 1024 ile geri döner.
|
||
Sonra 1024 byte okumak ister ancak 6 byte okur ve 6 değeri ile geri döner. Sonra da EOF'tan dolayı okuma yapamaz ve 0 ile geri döner. Klasik dosya kopyalama
|
||
algoritması bu biçimdedir. Aşağıda kodun tamamı verilmiştir.
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
#define MAX_PATH 1024
|
||
#define BUFFER_SIZE 1024
|
||
|
||
int main(void)
|
||
{
|
||
FILE *fs, *fd;
|
||
char spath[1024], dpath[1024], *str;
|
||
size_t result;
|
||
char buf[BUFFER_SIZE];
|
||
|
||
printf("Source file path: ");
|
||
fgets(spath, 1024, stdin);
|
||
if ((str = strchr(spath, '\n')) != NULL)
|
||
*str = '\0';
|
||
else {
|
||
fprintf(stderr, "path too long...\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
printf("Target file path: ");
|
||
fgets(dpath, 1024, stdin);
|
||
if ((str = strchr(dpath, '\n')) != NULL)
|
||
*str = '\0';
|
||
else {
|
||
fprintf(stderr, "path too long...\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
if ((fs = fopen(spath, "rb")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
if ((fd = fopen(dpath, "wb")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
while ((result = fread(buf, 1, BUFFER_SIZE, fs)) > 0)
|
||
if (fwrite(buf, 1, result, fd) != result) {
|
||
fprintf(stderr, "cannot write file!...\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
if (ferror(fs)) {
|
||
fprintf(stderr, "cannot read file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
printf("file successfully copied...\n");
|
||
|
||
fclose(fs);
|
||
fclose(fd);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
77. (Son) Ders - 14/04/2023 - Cuma
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yapıların elemanları bellekte ardışıl bir biçimde bulunduğuna göre bir yapı nesnesi tek hamlede fwrite fonksiyonu ile dosyaya yazılabilir ve fread
|
||
fonksiyonu ile dosyadan okunabilir. Örneğin:
|
||
|
||
struct PERSON {
|
||
char name[64];
|
||
int no;
|
||
};
|
||
...
|
||
struct PERSON per = {"Kaan Aslan", 123};
|
||
|
||
if (fwrite(&per, sizeof(struct PERSON), 1, f) != 1) {
|
||
fprintf(stderr, "cannot write file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
Aşağıda örnekte bir döngü içerisinde kişilerin bilgileri alınıp dosyaya fwrite fonksiyonu ile yazdırılmıştır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
struct PERSON {
|
||
char name[64];
|
||
int no;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
struct PERSON per;
|
||
char *str;
|
||
|
||
if ((f = fopen("test.dat", "wb")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
for (;;) {
|
||
printf("Adi soyadi:");
|
||
fgets(per.name, 64, stdin);
|
||
if ((str = strchr(per.name, '\n')) != NULL)
|
||
*str = '\0';
|
||
if (!strcmp(per.name, "quit"))
|
||
break;
|
||
printf("No:");
|
||
scanf("%d", &per.no);
|
||
while (getchar() != '\n')
|
||
;
|
||
if (fwrite(&per, sizeof(struct PERSON), 1, f) != 1) {
|
||
fprintf(stderr, "cannot write file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
}
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki daha önceki örnekte dosyaya yazmış olduğumuz bilgileri döngü içerisinde fread fonksiyonu ile okuyoruz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
struct PERSON {
|
||
char name[64];
|
||
int no;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
struct PERSON per;
|
||
|
||
if ((f = fopen("test.dat", "rb")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
while (fread(&per, sizeof(struct PERSON), 1, f) == 1)
|
||
printf("%s, %d\n", per.name, per.no);
|
||
|
||
if (ferror(f)) {
|
||
fprintf(stderr, "cannot read file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte belli bir kayıt isme göre "sıralı (sequential)" biçimde dosyada aranmıştır. Kayıt sayısı çok fazlaysa sıralama arama iyi bir
|
||
yöntem değildir. Bunun için özel algoritmik arama yöntemleri kullanılmaktaıdır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
#include <string.h>
|
||
|
||
struct PERSON {
|
||
char name[64];
|
||
int no;
|
||
};
|
||
|
||
int main(void)
|
||
{
|
||
FILE *f;
|
||
struct PERSON per;
|
||
char *str;
|
||
char name[64];
|
||
|
||
printf("Aranacak kisinin adi soyadini giriniz:");
|
||
fgets(name, 64, stdin);
|
||
if ((str = strchr(name, '\n')) != NULL)
|
||
*str = '\0';
|
||
|
||
if ((f = fopen("test.dat", "rb")) == NULL) {
|
||
fprintf(stderr, "cannot open file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
while (fread(&per, sizeof(struct PERSON), 1, f) == 1)
|
||
if (!strcmp(per.name, name)) {
|
||
printf("record found : %s, %d\n", per.name, per.no);
|
||
break;
|
||
}
|
||
|
||
if (feof(f))
|
||
printf("record not found!..\n");
|
||
|
||
if (ferror(f)) {
|
||
fprintf(stderr, "cannot read file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
İşletim sistemlerinde ekran ve klavye birer "aygıt sürücü (device driver)" tarafından yönetilmektedir. Aygıt sürücüler ise dosyalar gibi işleme sokulmaktadır.
|
||
Ekran ve klavyeyi kontrol eden aygıt sürücülere "terminal aygıt sürücüleri" denilmektedir. C'de "stdout" değişkeni FILE * türünden bir dosya bilgi
|
||
göstericisi belirtir. Benzer biçimde "stdin" isimli değişken de yine FILE * türünden bir dosya bilgi göstericisi belirtmektedir. Her iki değişken de
|
||
<stdio.h> içerisinde bildirilmiştir.
|
||
|
||
C standartlarında "ekran" ve "klavye" lafı edilmemektedir. Çünkü bir bilgisayar sisteminde ekran ve klavye olmak zorunda değildir. C standartlarında
|
||
ekran ve klavye yerine "stdout dosyası" ve "stdin dosyası" lafları edilmektedir. Örneğin standratlara göre printf fonksiyonu "ekrana değil stdout dosyasına"
|
||
yazma yapmaktadır. Benzer biçimde scanf fonksiyonu da klavyeden değil "stdin dosyasından" okuma yapmaktadır. C standratlarında stdout aslında çıktının
|
||
bir biçimde gönderileceği bir aygırı temsil etmektedir. stdout ilgili sistemde ekran aygıt sürücüsü olabileceği gibi, printer'a ilişkin ya da başka bir
|
||
aygıta ilişkin aygıt sürücüsü olabilir. Tabii stdout bir dosya olarak normak bir disk dosyasını da temsil edebilir. Benzer biçimde yanı durum stdin dosyası
|
||
için de geçerlidir. Tabii klasik bilgisayar sistemlerinde default durumda stdout ekranı, stdin klavyeyi temsil etmektedir.
|
||
|
||
printf fonksiyonu aslında fprintf fonksiyonunun stdout dosyasına yazan biçimidir. Yani aşağıdaki iki çağrı eşdeğerdir:
|
||
|
||
printf(...);
|
||
fprintf(stdout, ...);
|
||
|
||
Benzer biçimde scanf fonksiyonu da aslında fscanf fonksiyonunun stdin dosyasından okuma yapan biçimdir. Aşağıdaki iki çağrı eşdeğerdir:
|
||
|
||
scanf(...);
|
||
fscanf(stdin, ...);
|
||
|
||
stdin, stdout ve stderr dosyaları program çalışmaya başladığında otomatik olarak açılmış kabul edilmektedir. Bunlar programcı tarafından açılmazlar
|
||
ve dolayısıyla programcı tarafından kapatılmazlar. Programcı tarafından bu dosyalar doğrudan kullanılabilirler. stdin dosyasının "read only text modda"
|
||
açıldığı, stdout ve stderr dosyalarının ise "write only text modda açıldığı" kabul edilmektedir. Yani biz stdin dosyasına yazma yapamayız. stdout ve stderr
|
||
dosyalarından da okuma yapamayız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
stdin, stdout ve stderr dosyaları başka aygırlara ve diskteki başka dosyalara yönlendirilebilmektedir. Bu konuya "IO yönlendirmesi (IO redireciton)"
|
||
denilmektedir. IO yönlendirmesinin işletim sistemine özgü ayrıntıları vardır. Windows, UNIX/Linux ve macOS sistemlerinde IO yönlendirmesi
|
||
> ve < sembolleriyle yapılmaktadır. > sembolü stdout dosyasının yönlendirilmesi için < sembolü ise stdin dosyasının yönlendirimesi için kullanılmaktadır.
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
for (int i = 0; i < 10; ++i)
|
||
printf("%d\n", i);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Bu program 1'den 10'a kadar sayıları stdout dosyasına yazdırmaktadır. Bu program çalıştırıldığında stdout dosyası default oalrak terminal aygıt
|
||
sürücüsünü belirttiği için yazılanlar ekrana çıkacaktır. Ancak biz komut satırında > sembolü ile stdout dosyasını bir disk dosyasına yönledirebiliriz.
|
||
Örneğin:
|
||
|
||
./sample > test.txt
|
||
|
||
Artık ekrana yazılacak şeyle "test.txt" dosyasının ieçrisine yazılacaktır. IO yönlendirmesi IDE'lerde de hiç komut satırına geçmeden yapılabilmektedir.
|
||
Örneğin Visual Studio IDE'sinde proje seçeneklerinden "Debugging/Command Line Arguments" kısmımında > sembolü ile yönlendirme yapılabilemktedir.
|
||
|
||
Benzer biçimde klavyeden okunan değerler bir dosyadan okunuyormuş etkisi de yaratılabilir. Bunun için stdin dosyasını < sembolü ile yönlendirmek gerekir.
|
||
Örneğin:
|
||
|
||
./sample < x.txt
|
||
|
||
Burada stdin dosyasından okunacaklar x.txt dosyasından okunacaktır. Pekiyi EOF etkisi klavyede nasıl sağlanmaktadır? İşte UNIX/Linux sistemlerinde
|
||
klavyeden Ctrl+d tuşlarına basıldığında sanki stdin dosyasında EOF ile karşılaşılmış etkisi yaratılmaktadır. Windows sistemlerinde ise bu etki Ctrl+z
|
||
tuşuyla yapılmaktadır. Tabii biz bu özel tuşlara bastığımızda klavyeye ilişkin dosyasını kapatmış olmayız. O defalık EOF etkisi yaratılmış olur. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
double val;
|
||
|
||
for (;;) {
|
||
if (scanf("%lf", &val) == EOF)
|
||
break;
|
||
|
||
printf("%f\n", val * val);
|
||
}
|
||
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
Burada scanf EOF ile karşılaştığında EOF değeri ile geri dönmektedir. Biz de döngüden çıkmaktayız. İşte bu programı çalıştırırken
|
||
EOF etkisinin yaratılması için Ctrl+d ya da Ctrl+z tuşları kullanılmaktadır. Biz bu proramı aşağıdaki gibi çalıştırırsak prtogram klavyeden okuma
|
||
yapmak yerine "x.txt" dosyasından okuma yapacaktır:
|
||
|
||
./sample < x.txt
|
||
|
||
Tabii stdin dosyası ile stdout dosyası birlikte de yönlendirilebilir. Örneğin:
|
||
|
||
./sample < x.txt > y.txt
|
||
|
||
(Windows 11'deki Visual Studio'nun son versiyonlarında scanf fonksiyonunda EOF etkisi yaratma konusunda muhtemel bir bug bulunmaktadır.)
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Pekiyi stderr dosyası nedir? stderr dosyası hata mesajlarının yazdırılması için düşünülmüş bir dosyadır. İşletim sistemlerinde default durumda stderr
|
||
dosyası da stdout ile aynı biçimde terminal sürücüsüne (yani ekrana) yönlendirilmiş durumdadır. Yani default durumda stderr dosyasına yazılanlar yine
|
||
ekrana çıkacaktır. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
printf("stdout\n");
|
||
fprintf(stderr, "stderr\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
Her iki yazı da ekrana çıkacaktır. Pekiyi o zaman stderr dosyasına ne gerek vardır?
|
||
|
||
Biz hata mesajlarını print ile stdout dosyasına değil, fprintf fonksiyonu ile stderr dosyasına yazdırmalıyız. Zaten bugüne kdara da böyle yapmıştık.
|
||
Örneğin:
|
||
|
||
char *str;
|
||
|
||
if ((s = (char *)malloc(1024)) == NULL) {
|
||
fprintf(stderr, "cannot allocate memory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
Her ne kadar default durumda stdout da stderr de yazıların ekrana çıkmasına yol açıyorsa da IO yönlendirmesi yoluyla bu iki dosyanın hedefleri değiştirilebilir.
|
||
|
||
Yukarıdaki programı yeniden yazmış olalım:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
printf("stdout\n");
|
||
fprintf(stderr, "stderr\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
Şimdi programı şöyle çalıştırmış olalım:
|
||
|
||
./sample > x.txt
|
||
|
||
Burada ekranda yalnızca "stderr" yazısını göreceğiz. Çünkü > semboli yalnızca stdout dosyasını yönlnedirmektedir. Eğer stderr dosyası yönlendirilmek
|
||
istenirse bu durumda 2> sembolünün kullanılması gerekir. Örneğin:
|
||
|
||
./sample 2> x.txt
|
||
|
||
Burada da ekran yalnızca "stdout" yazısını göreceğiz. Çünkü biz bu örnekte yalnızca stderr dosyasını yönlendirmiş olduk. İşte programcıo eğer
|
||
hata mesajlarını stderr dosyasına yazarsa kullanıcı programın normal mesajlarıyla hata mesajlarını birbirinden ayırabilir. Örneğin:
|
||
|
||
find / -name "sample.c"
|
||
|
||
Bu komut kökten itibaren her dizinde "sample.c" dosyasını aramaktadır. Ancak yekisiz dizinlerle karşılaştığında stderr dosyasına hata mesajları yazmaktadır.
|
||
İşin başında stdout ile stderr ekrana yönlendirildiği için programın normal mesajları ile hata mesajları birlikte ekranda görünecektir. Karışık bir
|
||
görüntü oluşacaktır. Halbuki biz bu programı çalıştırırken stderr dosyasını başka bir dosyay yönlendirirsek kafamızın karışmasını engellemiş oluruz.
|
||
Örneğin:
|
||
|
||
find / -name "sample.c" 2> err.txt
|
||
|
||
Artık programın hata mesajları ekrana değil "err.txt" dosyasına yazdırılacaktır. İşte kullanıcıya bu ayırma olanağını vermek için hata mesajlarının
|
||
stderr dosyasına yazdırılması gerekmektedir. Linux sistemlerinde /dev/null isimli özel bir dosya vardır. Bu dosyaya yazılan her şey atılmaktadır.
|
||
Dolayısıyla yönlendirme bu dosyaya yapılırsa hata mesajlarından tamamen kurtulabiliriz. Örneğin:
|
||
|
||
find / -name "sample.c" 2> /dev/null
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir program komut satırından çalıştırılırken argümanlar girilebilmektedir. Bu argümanlara programın "komut satırı argümanları (command line arguments)"
|
||
denilmektedir. Örneğin:
|
||
|
||
./sample ali veli selami
|
||
|
||
Buradaki "ali veli selami" çalıştırdığımız sample programının komut satırı argümanlarıdır. Komut satırı argümanları olmasaydı pek çok programı istediğimiz
|
||
işleri yapacak biçimde çalıştıramzdık. Örneğin:
|
||
|
||
find / -name "sample.c"
|
||
|
||
Burada find programı komut satırı argümanlarını alıp ne yaapacağına karar vermektedir. Örneğin:
|
||
|
||
ls -l
|
||
|
||
Burada ls programı görüntülemenin nasıl yapılacağına "-l" argümanı ile karar vermektedir.
|
||
|
||
Komut satırı argümanları kabul programlar tarafından alınır ve C programına iletilir. C programcısı da bunları alarak kullanır. Standartlara göre
|
||
main fonksiyonu iki parametrik yapıya sahip olabilmektedir:
|
||
|
||
int main(void)
|
||
{
|
||
...
|
||
}
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
...
|
||
}
|
||
|
||
Ancak standartlar o derleyiciye ve işletim sistemine özgü biçimde main fonksiyonun başka parametrelerinin de olabileceğini belirtmiştir. Fakat taşınabilirlik
|
||
bakımından bu iki parametrik yapı kullanılmaktadır. main fonksiyonun aşağıdaki biçimi komut satırı argümanlarını almak için kullanılmaktadır:
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
...
|
||
}
|
||
|
||
Burada parametre değişkenlerinin isimleri argc ve argv olmak zoruda değildir. Ancak geleneksel olarak programcılar bu isimleri kullanmaktadır.
|
||
main fonksiyonun ikinci parametresi göstericiyi gösteren göstericidir. Yani yukarıdaki yazım ile aşağıdaki yazım tamamen eşdeğerdir:
|
||
|
||
int main(int argc, char **argv)
|
||
{
|
||
...
|
||
}
|
||
|
||
Ancak geleneksel olarak programcılar her nedense önceki yazımı tercih etmektedir. Pekiyi bu argc ve argv ne anlama gelmektedir? Programın aşağıdaki gibi
|
||
çaşıltırıldığını varsayalım:
|
||
|
||
./sample ali veli selami
|
||
|
||
Burada argc parametresine program ismi dahil olmak üzere boşluklarla ayrılmış yazıların sayısı geçirilmektedir. Yani örneğimizde argc 4 değerini alacaktır.
|
||
argv ise bir göstericiyi gösteren göstericidir. Yani bir gösterici dizisini göstermektedir. İşte bu gösterici dizisi program ismi dahil olmak üzere
|
||
komut satırı argümanlarını gösterir. Her komut satırı argümanın sonunda yine null karakter bulunmaktadır. Standartlar argv dizisinin sonundaki son elemanın
|
||
NULL adres içereceğini garanti etmektedir. Yukarıdaki çalıştırmada argv dizisinin ieçriği şöyle olacaktır:
|
||
|
||
argv ---> [0] ---> ./sample\0
|
||
[1] ---> ali\0
|
||
[2] ---> veli\0
|
||
[3] ---> selami\0
|
||
[4] NULL
|
||
|
||
Örneğin "sample.c" programı şöyle olsun:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
printf("%d\n", argc);
|
||
|
||
for (int i = 0; i < argc; ++i)
|
||
puts(argv[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Biz de UNIX/Linux sistemlerinde programı çeşitli biçimlerde çalıştıralım:
|
||
|
||
kaan@kaan-virtual-machine:~/Study/C$ ./sample ali veli selami
|
||
4
|
||
./sample
|
||
ali
|
||
veli
|
||
selami
|
||
kaan@kaan-virtual-machine:~/Study/C$ ./sample 10 20
|
||
3
|
||
./sample
|
||
10
|
||
20
|
||
kaan@kaan-virtual-machine:~/Study/C$ ./sample
|
||
1
|
||
./sample
|
||
kaan@kaan-virtual-machine:~/Study/C$ ./sample ankara izmir
|
||
3
|
||
./sample
|
||
ankara
|
||
izmir
|
||
|
||
Mademki programın ismi de komut satırı argümanlarında dahildir. O halde argc hiçbir zaman 0 olamaz. En azından 1 olmak zorundadır.
|
||
|
||
argc ve argv parametrelerinden komut satırı argümanlarını alıp kullanmak programcının görevidir. İşletim sistemi yalnızca bu argümanları main
|
||
fonksiyonuna geçirmektedir. Başka bir şeye karışmamaktadır. Tabi programcı main fonksiyonunun parametresini void yaparsa bu argümanları elde edemez.
|
||
Yani programcı main fonksiyonun paraöetresini void yaparsa komut satırından argüman girmek bir soruna yol açmaz. Ancak bu durumda programcı
|
||
bu argümanları kullanamaz. Burada bir kez daha "argc" ve "argv" isimlerinin zorunlu olmadığını ancak geleneksel oladuğunu vurgulamak istiyoruz.
|
||
Yani program aşağıdaki gibi yazılsaydı da hiçbir sorun oluşmazdı:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(int a, char **b)
|
||
{
|
||
printf("%d\n", a);
|
||
|
||
for (int i = 0; i < a; ++i)
|
||
puts(b[i]);
|
||
|
||
return 0;
|
||
}
|
||
|
||
"argc" ismi "argument counter" sözcüklerinden "argv" ismi ise "argument vector" isminden (burada "vector" dizi anlamında kullanılmıştır)
|
||
ya da "argument variable list" sçzcüklerinden kısaltmadır.----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
78. Ders 28/04/2023 - Cuma
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıdaki örnekte komut satırından alınan değerlerin toplamı ekrana yazdırılmıştır. Programının isminin "sample" olduğunu varsayalım:
|
||
|
||
./sample 10 20 30
|
||
60.000000
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
double total;
|
||
|
||
if (argc < 2) {
|
||
fprintf(stderr, "wrong number of arguments!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
total = 0;
|
||
for (int i = 1; i < argc; ++i)
|
||
total += atof(argv[i]);
|
||
|
||
printf("%f\n", total);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Aşağıda örnekte bir komut satırı argümanı biçiminde alınan yol ifadesi ile belirtilen dosyanın içeriğini yazdıran örnek bir C programı
|
||
görülmektedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
int main(int argc, char *argv[])
|
||
{
|
||
FILE *f;
|
||
int ch;
|
||
|
||
if (argc != 2) {
|
||
fprintf(stderr, "wronh number of arguments!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
if ((f = fopen(argv[1], "r")) == NULL) {
|
||
fprintf(stderr, "cannot open file: %s\n", argv[1]);
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
while ((ch = fgetc(f)) != EOF)
|
||
putchar(ch);
|
||
|
||
if (ferror(f)) {
|
||
fprintf(stderr, "cannot read file!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
|
||
fclose(f);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir program komut satırı argümanlarını kullanmıyor olsa bile (yani örneğin main fonksiyonunun parametresi void olsa bile) programın çalıştırılması sırasında
|
||
komut satırı argümanı verilmesi genel olarak bir soruna yol açmamaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Biz şimdiye kadar önişlemci komutları olarak yalnızca #include ve #define komutlarını gördük. Şimdi çok kullanılan bazı önişlemci komutlarını
|
||
göreceğiz.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#if komutu adeta if deyiminin önişlem aşamasında işlem gören biçimi gibidir. #if komutunun genel biçimi şöyledir:
|
||
|
||
#if <sabit_ifadesi>
|
||
...
|
||
#else
|
||
...
|
||
#endif
|
||
|
||
Komutta #else kısmı hiç olmayabilir. Ancak #if ve #endif kısımları olmak zorundadır. #else kısmı olmayan #if komutu şu biçimdedir:
|
||
|
||
#if <sabit_ifadesi>
|
||
...
|
||
#endif
|
||
|
||
Sabit ifadesi önişlemci aşamsında net sayısal değeri hesaplanacak bir ifadeden oluşmalıdır. Örneğin:
|
||
|
||
#if 1
|
||
...
|
||
#else
|
||
...
|
||
#endif
|
||
|
||
Örneğin:
|
||
|
||
#if MAX > 10
|
||
...
|
||
#else
|
||
...
|
||
#endif
|
||
|
||
Bu #if komutunun geçerli olabilmesi için MAX değişkenin de #define ile oluşturulmuş bir sembolik sabit olması gerekmektedir.
|
||
|
||
#if komutu şöyle çalışır: Önişlemci #if komutunun yanındaki ifadenin sayısal değerini hesaplar. Bu değer sıfır dışı bir değer ise #if ile #else
|
||
arasındaki kısım, 0 ise #else ile #endif arasındaki kısım derleme modülüne verilir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
#if 1
|
||
printf("Yes\n")
|
||
#else
|
||
printf("No\n");
|
||
#endif
|
||
|
||
return 0;
|
||
}
|
||
|
||
Bu kod derleme modülüne geldiğinde derleme modülü kodu şöyle görecektir:
|
||
|
||
<stdio.h dosyasının içeriği>
|
||
|
||
int main(void)
|
||
{
|
||
printf("Yes\n");
|
||
|
||
return 0;
|
||
}
|
||
|
||
#if komutunu normal if deyimiyle karıştırmayınız. Normal if deyiminde if deyiminin doğruysa kısmı da yanlışsa kısmı da kodda bulunmaktadır.
|
||
Akış programın çalışma zamanı sırasında sapar. Halbuki #if komutunda derleme modülüne hangi kısmın verileceği belirlenmektedir. Yani çalışan programda
|
||
bir if deyimi yoktur.
|
||
|
||
Pekiyi #if komutunun ne anlamı vardır? İşte bazen çeşitli durumlarda çeşitli bazı kod parçalarının atılıp bazı kod parçalarının derleme modülüne
|
||
verilmesi istenebilir. Örneğin bir kütüphanenin 3 versiyonundan sonra bazı şeylerin değişmiş olduğunu varsayalım. Bu durumda biz kodumuzu hem 3 versiyonundan
|
||
sonraki versiyonlarda hem de önceki versiyonlarda çalışacak biçimde düzenlemek isteyebiliriz. Bu ayarlamayı da tek yerden yapabiliriz:
|
||
|
||
#define VERSION 3.4
|
||
...
|
||
|
||
#if VERSION > 3
|
||
...
|
||
#else
|
||
...
|
||
#endif
|
||
|
||
Bazen programcılar kodun bir bölümünü derleme modülünden çıkartmak için yorum satırı kullanmak yerine #if komutunu tercih ederler. Örneğin:
|
||
|
||
#if 1
|
||
|
||
int main(vodid)
|
||
{
|
||
...
|
||
}
|
||
|
||
#endif
|
||
|
||
#if 0
|
||
|
||
int main(vodid)
|
||
{
|
||
...
|
||
}
|
||
|
||
#endif
|
||
|
||
Yorum satırı kodun bir bölümünün derleme dışında bırakılmasının çeşitli problemleri vardır. Aşağıdaki gibi fonksiyon olsun:
|
||
|
||
|
||
void foo(void)
|
||
{
|
||
/* this function performs ... */
|
||
...
|
||
}
|
||
|
||
Biz şimdi bu fonksiyonu yorum satırı içerisine alarak derleme dışı bırakmak isteyelim:
|
||
|
||
/*
|
||
void foo(void)
|
||
{
|
||
/* this function performs ... */
|
||
...
|
||
}
|
||
*/
|
||
|
||
Görüldüğü gibi burada içi içe yorum satırları oluşmuş durumdadır. İç içe yorum satırlarının düzgün bir biçimde ele alınıp alınmayacağı
|
||
derleyicileri yazanların isteğine bırakılmıştır. Bazı derleyicilerde yukarıdaki kod error ile sonuçlanabilir. Halbuki aşağıdaki gibi
|
||
bir devre dışı bırakmada bir sorun yoktur:
|
||
|
||
#if 0
|
||
|
||
void foo(void)
|
||
{
|
||
/* this function performs ... */
|
||
...
|
||
}
|
||
|
||
#endif
|
||
|
||
#if komutunda iç içe #if'ler için #elif (else if anlamında) bir parça da eklenmiştir. Örneğin:
|
||
|
||
#if VERSION == 1
|
||
....
|
||
#else
|
||
#if VERSION == 2
|
||
...
|
||
#else
|
||
#if VERSION == 3
|
||
...
|
||
#endif
|
||
#endif
|
||
#endif
|
||
|
||
Böylesi iç içe #if'lerin oluşturulması oldukça zordur. Bunun yerine aynı iişlemler #elif kullanılarak daha kolay gerçekleştirilebilir. Örneğin:
|
||
|
||
#if VERSION == 1
|
||
....
|
||
#elif VERSION == 2
|
||
...
|
||
#elif VERSION == 3
|
||
...
|
||
#endif
|
||
|
||
#elif anahtar sözcüğünün yanında yine bir sabit ifadesinin olması gerektiğine ve her #elif kısmının #endif ile kapatılmadığına toplamda
|
||
yalnızca bir tane #endif kullanıldığına dikkat ediniz. #elif merdivenin sonunda #else de kullanılabilmektedir. Örneğin:
|
||
|
||
#if VERSION == 1
|
||
....
|
||
#elif VERSION == 2
|
||
...
|
||
#elif VERSION == 3
|
||
...
|
||
#else
|
||
....
|
||
#endif
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#ifdef komutu en çok kullanılan önişlemci komutlarından biridir. Komutun genel biçimi şöyledir:
|
||
|
||
#ifdef <sembolik_sabit>
|
||
...
|
||
#else
|
||
...
|
||
#endif
|
||
|
||
#ifdef anahtar sözcüğünün yanında bir ifade değil bir sembolik sabit ismi olur. Komut eğer bu sembolik sabit define edilmişse #else'e kadar olan kısmı
|
||
derleme modülüne verir, eğer bu sembolik sabit define edilmemişse #else ile #endif arasındaki kısmı derleme modülüne verir. #else kısmı yine
|
||
olmak zorunda değildir. Buradaki sembolik sabitin ne olarak define edildiğinin bir önemi yoktur. Asıl önemli olan bu sembolik sabitin define edilip
|
||
edilmediğidir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#define MAX
|
||
|
||
int main(void)
|
||
{
|
||
#ifdef MAX
|
||
printf("Yes\n");
|
||
#else
|
||
printf("No\n");
|
||
#endif
|
||
|
||
return 0;
|
||
}
|
||
|
||
#ifndef komutu da tamamen #ifdef komutunun tersini yapmaktadır. Genel biçimi şöyledir:
|
||
|
||
#ifndef <sembolik_sabit>
|
||
...
|
||
#else
|
||
...
|
||
#endif
|
||
|
||
Önişemci eğer #ifndef komutunun yanındaki sambolik sabit define edilmemişse #else'e kadar olan kısmı, edilmişse #else ile #endif arasındaki kısmı
|
||
derleme modülüne vermektedir. Yine komutun #else kısmı bulunmayabilir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#if, #ifdef ve #ifndef komutları en fazla kullanılan önişlemci komutlarıdır. Pek çok proje derlenmeden önce "konfigüre"
|
||
edilmektedir. Konfigürasyon projedeki çeşitli öğelerin belirlenmesi anlamına gelmektedir. Konfigürasyon aşamasında
|
||
kullanıcının belirlemelerinden hareketle çeşitli sembolik sabitler define edilmekte ve projenin kodu da #if, #ifdef
|
||
ve #ifndef komutları ile bu sembolik sabitlere başvurularak yazılmaktadır. Örneğin Linux işletim sisteminin çekirdeğini
|
||
yeniden derlemek isteyelim. Ancak çekirdeğin pek çok parametresini kendimize göre değiştirmek isteyelim. İşte bunun
|
||
için önce çekirdek konfigüre edilmektedir. Bu konfigürasyon sonucunda çeşirli sembolik sabitler bir başlık dosyası
|
||
biçiminde oluşturulmaktadır. Zaten çekirdeğin kaynak kodları da bu sembolik sabitlere dayalı biçimde yazıldığı için
|
||
uygulamacının belirlemekleri çekirdek kodlarına yanısıtılmış olmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#ifndef komutu "include korumaları (include guard)" için sıkça kullanılmaktadır. include koruması "bir dosyanın doğrudan ya da dolaylı olarak
|
||
birden fazla kez include edilmesi durumunda sorun çıkmaması için" oluşturulan yapıya denilmektedir. Bir dosyanın ikinci kez include edilmesi her
|
||
zaman olmasa da genellikle sorunlara yol açabilen bir durum oluşturmaktadır. Örneğin "sample.h" dosyasının içeriği şöyle olsun:
|
||
|
||
/* sample.h */
|
||
|
||
#define MAX 10
|
||
|
||
struct POINT {
|
||
int x, y;
|
||
};
|
||
|
||
void draw_line(const struct POINT *pt1, const struct POINT *pt2);
|
||
|
||
Biz bu dosyayı aşağıdaki gibi iki kez include etmiş olalım:
|
||
|
||
#include "sample.h"
|
||
#include "sample.h"
|
||
|
||
Aynı yapının aynı isimle ikinci kez bildirilmesi derleme sırasında error oluşmasına yol açacaktır.
|
||
|
||
#ifndef komutu ile include koruması şöyle oluşturulmaktadır:
|
||
|
||
#ifndef <sembolik_sabit_ismi>
|
||
#define <sembolik_sabit_ismi>
|
||
|
||
<dosya içeriği>
|
||
|
||
#endif
|
||
|
||
Burada #ifndef'in yanındaki makro ismi dosya isminden hareketle uydurulmuş herhangi bir isimdir. Örneğin include dosyasının "sample.h"
|
||
isminde olduğunu varsayalım. Biz bu ismi SAMPLE_H_ biçiminde uydurabiliriz:
|
||
|
||
#ifndef SAMPLE_H_
|
||
#define SAMPLE_H_
|
||
|
||
<dosya içeriği>
|
||
|
||
#endif
|
||
|
||
include koruması şöyle çalışmaktadır: Önişlemci dosyayı ilk kez gördüğünde SAMPLE_H_ isimli makronun define edilmemiş olduğunu tespit edecek ve
|
||
böylece dosyanın içeriğini derleme modülüne verecektir. Ancak bu sırada ilgili makro define edilmiş olmaktadır. Bu durumda artık önişlemci dosyanın
|
||
içeriğini ikinci kez derleme modülüne vermeyecektir. Programcının kendi oluşturduğu başlık dosyalarında yukarıdaki gibi include koruması uygulaması gerekir.
|
||
Örneğin:
|
||
|
||
#ifndef SAMPLE_H_
|
||
#define SAMPLE_H_
|
||
|
||
#define MAX 10
|
||
|
||
struct POINT {
|
||
int x, y;
|
||
};
|
||
|
||
void draw_line(const struct POINT *pt1, const struct POINT *pt2);
|
||
|
||
#endif
|
||
|
||
Şüphesiz programcılar bir include dosyasını açıkça aşağıdaki gibi include etmezler:
|
||
|
||
#include "sample.h"
|
||
#include "sample.h"
|
||
|
||
Ancak include dosyaları dolaylı bir biçimde birden fazla kez include edilebilmektedir. Örneğin projemizde "a.h" ve "b.h" isimli iki başlık dosyası olsun.
|
||
Biz de bu iki başlık dosyasını aşağıdaki gibi include etmiş olalım:
|
||
|
||
#include "a.h"
|
||
#include "b.h"
|
||
|
||
Bu başlık dosyaları kendi içerisinde aynı başlık dosyalarını mecburen include ediyor olabilir. Örneğin:
|
||
|
||
/* a.h */
|
||
|
||
#include <stddef.h>
|
||
|
||
struct SAMPLE {
|
||
size_t a;
|
||
size_t b;
|
||
};
|
||
|
||
/* b.h */
|
||
|
||
struct MAMPLE {
|
||
size_t x;
|
||
size_t y;
|
||
};
|
||
|
||
Burada görüldüğü gibi <stddef.h> dosyası iki kez önişlemci modülü tarafından dolaylı bir biçimde görülecektir. Tabii <stddef.h> içerisinde ve C'nin
|
||
standart başlık dosyaları içerisinde zaten include koruması yapılmış durumdadır. Dolaylı include işlemiyle bir dosyanın birden fazla kez include edilmesi
|
||
durumlarıyla sık karşılaşılmaktadır.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C standartlarında bir grup "önceden tanımlanmış (predefined)" makrolar vardır. Bu makrolar genel olarak __XXX__ biçiminde isimlendirilmiştir.
|
||
Bu önceden tanımlanmış makrolar herhangi bir yerde define edilmiş değildir. Bunların kullanılması için herhangi bir başlık dosyasının include edilmesine
|
||
de gerek yoktur. Bunların önemli olanları şunlardır:
|
||
|
||
__FILE__: Bu makroyu önişlemci gördüğünde dosyanın ismini (bu isim yol ifadesi de içerebilir) iki tırnak içerisinde makronun bulunduğu yere yerleştirmektedir.
|
||
Dosya isminin yalnızca bir isim mi yoksa yol ifadesi ile birlikte bir isim mi olacağı derleyiciden derleyiciye değişebilmektedir. Böylece biz bir program
|
||
içerisinde program dosyasının ismini elde edebiliriz. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
char fname[] = __FILE__;
|
||
|
||
printf("%s\n", fname);
|
||
|
||
return 0;
|
||
}
|
||
|
||
__FILE__ makrosu özellikle hata mesajlarının iletilmesinde kullanılmaktadır.
|
||
|
||
__LINE__: Önişlemci bu makroyu gördüğünde makronun bulunduğu satırın numarasını int bir sabit olarak makronun bulunduğu yere yerleştirmektedir.
|
||
Böylece biz dosyanın belli bir satırının satır numarasını program içerisinden elde edebiliriz.
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
printf("%d\n", __LINE__);
|
||
printf("%d\n", __LINE__);
|
||
|
||
return 0;
|
||
}
|
||
|
||
__DATE__ ve __TIME__: Önişlemci bu makroları gördüğünde o anki tarih ve zaman bilgilerini iki tırnak içerisinde kod yerleştirmektedir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
printf("%s\n", __DATE__);
|
||
printf("%s\n", __TIME__);
|
||
|
||
return 0;
|
||
}
|
||
|
||
__cplusplus: Bu makro eğer derleme C'de yapılıyorsa define edilmemiş, C++'ta yapılıyorsa define edilmiş kabul edilmektedir. Dolayısıyla programcılar
|
||
C kodlarının C++ derleyicileri tarafından derlenmesi durumunda iki dilin uymsuzluklarını gidermek için bu makroyu kullanabilirler. Örneğin C'deki bazı
|
||
özellikler C++'ta geçerli olmayabilmektedir. Bu durumda programcı her ne kadar C dilinde yazıyorsa da C++'ın kabaca C'yi kapsadığı fikriyle
|
||
kodunun C++ derleyicisi ile derlenmesini de sağlayabilir. Örneğin "restrict göstericiler" C99 ile birlikte C'ye eklenmiştir. Ancak C++ bu özelliği
|
||
desteklememektedir. Bu durumda biz aşağıdaki gibi bir prototipi C++ uyumlu hale getirebiliriz:
|
||
|
||
#ifdef __cplusplus
|
||
void foo(void * p);
|
||
#else
|
||
void foo(void * restrict p);
|
||
#endif
|
||
|
||
__STDC_VERSION__: Önişlemci bu makroyu gördüğünde 201ymmL biçiminde long bir sabit yerleştirmektedir. Bu sayede biz C'nin hangi versiyonunda
|
||
çalıştırığımızı derleme sırasında anlayabiliriz. Bu makro C99 ile birlikte C'ye eklenmiştir. Bazı derleyiciler (örneğin Microspft C derleyicileri)
|
||
hala bu makroyu desteklememktedir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Yukarıda ele almış olduğumuz önceden tanımlanmış standart makroların dışında pek çok derleyicinin kendine özgü "önceden tanımlanmış (predifined)"
|
||
makroları da vardır. Örneğin gcc derleyicileri __GNUC__ makrosunun define edildiğini varsaymaktadır. Böylece gcc derleyicisine özgü birtakım işlemleri
|
||
derleme sırasında koda dahil edebiliriz. Makroyu aşağıdaki test koduyla test edebilirsiniz:
|
||
|
||
#include <stdio.h>
|
||
|
||
int main(void)
|
||
{
|
||
#ifdef __GNUC__
|
||
printf("gcc\n");
|
||
#else
|
||
printf("not gcc\n");
|
||
#endif
|
||
|
||
return 0;
|
||
}
|
||
|
||
Örneğin UNIX/Linux sistemlerindeki C derleyicileri standart olmasa da __unix__ isimli bir makronun define edilmiş olduğunu varsaymaktadır. Böylece biz
|
||
kodumuzda UNIX/Linux sistemlerine özgü işlemleri derleme aşamasında koda dahil edip çıkartabiliriz. Benzer biçimde Microsoft C derleyicilerinde de
|
||
32 bit Windows sistemleri için _WIN32 makrosu, 64 bit Windows sistemleri için _WIN64 makrosu define edilmiş kabul edilmektedir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <stdlib.h>
|
||
|
||
#if defined(__unix__)
|
||
#include <sys/stat.h>
|
||
#elif defined(_WIN32) || defined(_WIN64)
|
||
#include <windows.h>
|
||
#endif
|
||
|
||
#define DIR_NAME "xxx"
|
||
|
||
int main(void)
|
||
{
|
||
#if defined(__unix__)
|
||
if (mkdir(DIR_NAME, 0777) == -1) {
|
||
fprintf(stderr, "cannot create directory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
#elif defined(_WIN32) || defined(_WIN64)
|
||
if (!CreateDirectory(DIR_NAME, NULL)) {
|
||
fprintf(stderr, "cannot create directory!..\n");
|
||
exit(EXIT_FAILURE);
|
||
}
|
||
#else
|
||
#error operating system not supported
|
||
#endif
|
||
|
||
return 0;
|
||
}
|
||
|
||
Bu kodda dizin yaratılmaya çalışılmıştır. Ancak dizin yaratma işlemi UNIX/Linux sistemlerinde ve Windows sistemlerinde farklı fonksiyonlarla yapılmaktadır.
|
||
Programcı da yukarıdaki örnekte o anda hangi sistemde çalışılıyorsa o sistemdeki fonksiyonu çağırmıştır. Yukarıdaki kodd ahenüz görmediğimiz
|
||
birkaç özelliği de kullanmış olduk.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#error önişlemci komutu o anda derleme (yani önişlem) bir ölümcül hata mesajıyla derleme işlemini kesmektedir. Komutun kullanımı şöyledir:
|
||
|
||
#error <mesaj yazısı>
|
||
|
||
Mesaj yazısının iki tırnak içerisinde olmadığına dikkat ediniz. Örneğin biz bir programı UNIX/Linux sistemleri için yazmış olabiliriz. Bu programı
|
||
birisi Windows'ta derlemeye çalışırsa hata mesajıyla derlemenin kesilmesini isteyebiliriz:
|
||
|
||
#ifndef __unix__
|
||
#error operatating system is not UNIX/linux
|
||
#endif
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
defined isimli önişlemci operatörü belli bir makro ya da sembolik sabit define edilmişse 1 değerini define edilmemişse 0 değerini vermektedir.
|
||
Bu önişlemci operatörü makro ya da sembolik sabitlere ilişkin karmaşık koşulları oluşturmak için kullanılmaktadır. Örneğin SIZE ve COUNT define edilmişse
|
||
bir kod parçasını derleme modülüne vermek isteyelim. Bunu şöyle yapmaya çalışabiliriz:
|
||
|
||
#ifdef SIZE
|
||
#ifdef COUNT
|
||
...
|
||
#endif
|
||
#endif
|
||
|
||
Ancak defined operatörü sayesinde bu işlem basit bir biçimde şöyle de yapılabilir:
|
||
|
||
#if defined(SIZE) && defined(COUNT)
|
||
....
|
||
#endif
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define SIZE 10
|
||
#define COUNT 20
|
||
|
||
int main(void)
|
||
{
|
||
|
||
#if defined(SIZE) && defined(COUNT)
|
||
printf("yes\n");
|
||
#else
|
||
printf("no\n");
|
||
#endif
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#define komutunda bir makro parametresi komutun STR2 kısmında başına # önişlemci operatörü getirilerek kullanılırsa bu parametre
|
||
önişlemci tarafından iki tırnak içerisine alınmaltadır. Örneğin:
|
||
|
||
#define NAME(name) #name
|
||
|
||
Burada biz kod içerisinde NAME(x) gibi bir ifade kullandığımızda bunun yerine önişlemci "x" yerleştirecektir.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
#include <stdio.h>
|
||
|
||
#define MSG(msg) puts(#msg)
|
||
|
||
int main(void)
|
||
{
|
||
MSG(this is a test);
|
||
|
||
return 0;
|
||
}
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
#define komutunda komutun STR2 kısmında ## önişlemci operatörü kullanılırsa bu işleme "atom yapıştırma (token pasting)" denilmektedir. Örneğin:
|
||
|
||
#define WSTR(msg) L##msg
|
||
|
||
Burada biz WSTR makrosuna iki tırnaklı bir yazı verdiğimizde makro bunun başına L harfi iliştirecektir. Örneğin:
|
||
|
||
#include <stdio.h>
|
||
#include <wchar.h>
|
||
|
||
#define WSTR(msg) L##msg
|
||
|
||
int main(void)
|
||
{
|
||
wprintf(L"%s\n", WSTR("test"));
|
||
|
||
return 0;
|
||
}
|
||
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#define Unicode
|
||
|
||
#ifdef Unicode
|
||
#define STR(str) L##str
|
||
#else
|
||
#define STR(str) str
|
||
#endif
|
||
|
||
#ifdef Unicode
|
||
#define _tprintf wprintf
|
||
#else
|
||
#define _tprintf printf
|
||
#endif
|
||
|
||
int main(void)
|
||
{
|
||
_tprintf(STR("%s\n"), STR("this is a test\n"));
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada Unicode sembolik sabiti deine edilmişse _tprintf ismi wprintf ile define edilmemişse printf ile yer değiştirmektedir. Programcı tüm
|
||
string'leri STR makrosuyla kullanırsa bu sayede Unicode ile ASCII arasında kodunu büyük ölüçüde uyumluı tutabilir.
|
||
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#define TEST(a, b) a##b
|
||
|
||
int main(void)
|
||
{
|
||
int TEST(x, y);
|
||
|
||
xy = 10;
|
||
printf("%d\n", xy);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Örneğin:
|
||
|
||
#include <stdio.h>
|
||
|
||
#define PREFIXED_NAME(name) sys_##name
|
||
|
||
int main(void)
|
||
{
|
||
int PREFIXED_NAME(info);
|
||
|
||
sys_info = 10;
|
||
|
||
printf("%d\n", sys_info);
|
||
|
||
return 0;
|
||
}
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Biz daha önce küçük kod parçalarının "fonksion çağrılarını elimine etmek" için makro biçiminde yazılabileceğini söylemiştik. Örneğin bir sayının
|
||
karesini alan bir fonksiyon söz konusu olsun:
|
||
|
||
int square(int a)
|
||
{
|
||
return a * a;
|
||
}
|
||
|
||
Biz de bu fonksiyonu şöyle çağırmış olalım:
|
||
|
||
result = square(++x);
|
||
|
||
Burada bir sorun yoktur. Çünkü önce argümanın değeri hesaplanmakta daha sonra parametre değişkenine kopyalama yapılmaktadır. Şimdi de bu
|
||
fonksiyonu makro biçiminde yazalım:
|
||
|
||
#define square(a) ((a) * (a))
|
||
|
||
Artık makronun aşağıdaki gibi kullanımı "tanımsız davranış (undefined behavior)" oluşturacaktır:
|
||
|
||
result = square(++x);
|
||
|
||
|
||
İşte fonksiyonları fonksiyon çağrılarını elimine etmek için makro biçiminde yazmak şu nedenlerden dolayı sorunlu bir konudur.
|
||
|
||
- Makro yazımı zordur. Makro parametrelerinin parantez içerisine alınması, makronun en dıştan paranteze alınması aokunabilirliği zorlaştırmaktadır.
|
||
|
||
- Makro yazımının birden fazla satıra yaydırılması zordur.
|
||
|
||
- Makro açımı önişlemci tarafından yapıldığı için çeşitli kontroller makrolar üzerinde sağlanamamaktadır.
|
||
|
||
- Makroların çağrılması sırasında tanımsız davranışların oluşmaması için dikkat edilmesi gerekmektedir.
|
||
|
||
- Makrolar içerisinde bloklu işlemleri oluşturmak zordur.
|
||
|
||
İşte fonksiyon çağrılarını elimine etmek makrolara daha iyi bir alternatif oluşturan ""inline fonksiyonlar" denilen bir fonksiyon çeşidi düünülmüştür.
|
||
inline fonksiyonlar C'ye resmi olarak C99 ile birlikte sokulmuştur. Ancak C++'ta ilk standartlardan beri (C++98) inline fonksiyon bulunmaktadır.
|
||
Her ne kadar inline fonksiyonlar C'ye C99 ile resmi olarak sokulmuşsa da aslında derleyicilerin önemli bir bölümü bir "eklenti (extension)" biçiminde
|
||
inline fonksiyonları destekliyordu. Ancak standart öncesinde derleyicilerin inline fonksiyon semantikleri arasında küçük farklılıklar bulunabiliyordu.
|
||
Her ne kadar C++ Programlama Dili C programlama Dilini kapsıyorsa da C++'ın inline fonksiyon semantiği ile C99'un inline fonksiyon semantiği arasında
|
||
farklılıklar bulunmaktadır. Biz burada C99 ile C'ye eklenmiş olan inline fonksiyon semantiği üzerinde duracağız.
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
Bir fonksiyonu inline yapmak için tek yapılacak şey fonksiyonun başına aşağıdaki belirleyicileri getirmektir:
|
||
|
||
inline
|
||
static inline (ya da inline static)
|
||
extern inline (ya da inline extern)
|
||
|
||
inline anahtar sözcüğü C99'da "fonksiyon belirleyicisi (function specifier)" denilen bir gruba dahil edilmiştir.
|
||
|
||
inline bir fonksiyon çağrıldığında derleyici makine kodlarına fonksiyon çağırma kodunu (CALL komutu) eklemek yerine doğrudan kodun iç kısmını
|
||
tıpkı bir makroymuş gibi çağrılma yerine enjekte edebilmektedir. Örneğin:
|
||
|
||
inline int square(int a)
|
||
{
|
||
return a * a;
|
||
}
|
||
|
||
Burada biz fonksiyonu şöyle çağırmış olalım:
|
||
|
||
result = square(1 + 2);
|
||
|
||
Derleyici square fonksiyonunu çağırmak yerine fonksiyonun iç kodunu çağırma yerine aşağıdaki gibi enjekte edebilmektedir:
|
||
|
||
result = 3 * 3;
|
||
|
||
Böylece daha önce görmüş olduğumuz makrolara benzer bir etki yaratılmış olur. Ancak bu etki önişlemci tarafından değil bizzat derleme modülü tarafından
|
||
sağlanmaktadır.
|
||
|
||
inline fonksiyonlar normal bir fonksiyon gibi yazılmaktadır. Derleyici inline fonksiyonlar üzerinde normal fonksiyonlarda yaptığı bütün kontrolleri
|
||
yapmaktadır. Ancak bir inline fonksiyon çağrıldığında derleyici onu bir fonksiyon gibi çağırmak yerine bizzat onun iç kodunu çağırma yerine enjekte edebilmektedir.
|
||
Böylece C99 ve sonrasında artık fonksiyon çağırma işlemini elimine etmek için küçük fonksiyonların makro yerine inline fonksiyon biçiminde yazılması
|
||
tavsiye edilmektedir. Zaten inline fonksiyonlar makroların yukarıda sıraladığımız olumsuzluklarını gidermek amacıyla dile sokulmuştur.
|
||
|
||
Ancak C'de inline fonksiyonların bazı ayrıntıları vardır. Öncelikle "inline" belirleyicisi "bir emir değil rica" niteliğindedir. Yani biz bir
|
||
fonksiyonu inline yaptığımız zaman derleyici o fonksiyonu inline olarak açmayabilir. inline fonksiyonların derleyiciler tarafından inline olarak
|
||
açılmaları zorunlu değildir. Pek çok derleyicide "inline açım (inline expansion)" derleyicinin optimizasyon seçenekleriyle ilişkilendirilmiştir.
|
||
Yani derleyicilerin optimizasyon seçenekleri açılmazsa genellikle derleyiciler inline açım yapmamaktadır. Microsoft derleyicilerinin inline açım yapması
|
||
için en azından /O2 optimizasyon seçeneğinin kullanılması gcc ve clang derleyicilerinde de en azından -O2 seçeneğinin kullanılması gerekmektedir. Örneğin:
|
||
|
||
cl /O2 sample.c (Microsoft)
|
||
gcc -O2 -o sample sample.c (gcc)
|
||
|
||
Tabii programcı bu derleyicilerde optimizasyon seçeneklerini açmış olsa bile derleyici yine de inline açımı yapmayabilir. Derleyiciler genellikle inline
|
||
açım yapmadıkları durumunda herhangi bir uyarı vermezler. Programcı da inline açımın yapılıp yapılmadığını ancak derleyicinin ürettiği kodu sembolik makine
|
||
kodlarını inceleyerek anlayabilmektedir.
|
||
|
||
Pekiyi derleyici neden fonksiyonu inline olarak açmayabilmektedir? Bunun çeşitli nedenleri olabilir. Örneğin:
|
||
|
||
- Özyinelemeli fonksiyonların inline açımları mümkün değildir.
|
||
- İçlerinde karmaşı deyimler olan fonksiyonalar (örneğin iç içe if deyimleri gibi) inline açılamayabilir.
|
||
- Çalışması uzun zaman alan kodların inline olarak açılması uygun değldir. Örneğin bir fonksiyonun içerisinde 1000000 kere dönen büyük
|
||
bir döngü olsun. Böyle bir fonksiyonun inline açılmasının bir faydası olmayacağı gibi kodu büyütebilmesi gibi zararları söz konusu olabilmektedir.
|
||
- Fonksiyonun adresi alınıp kod içerisinde kullanılıyorsa derleyici fonksiyonu inline olarak açmak istemeyebilir.
|
||
- Fonksiyon çok fazla satırdan oluşuyorsa inline açım kodu ciddi biçimde büyütebilecektir. Derleyiciler bu durumda inline açım yapmak istemeyebilirler.
|
||
|
||
Tersten gidersek "basit, birkaç satırlık, uzun döngüler ve karmaşık if deyimleri gibi deyimleri içermeyen" fonksiyonlar inline olarak açılmaya aday fonksiyonlardır.
|
||
|
||
Pekiyi derleyici inline fonksiyonu inline olarak açmazsa ne yapacaktır? İşte bu durumda C99 ve sonrasında fonksiyonun nasıl inline tanımlandığna göre
|
||
derleyicinin davranışı farklılaşmaktadır. Yukarıda da belirttiğimiz gibi C'de inline fonksiyonlar üç biçimde olabilmektedir:
|
||
|
||
inline
|
||
static inline
|
||
extern inline
|
||
|
||
C++'ta böyle bir ayrım yoktur. Ancak C'de yukarıdaki üç inline tanımlama farklı anlamlara gelmektedir.
|
||
|
||
Eğer C'de inline fonksiyon static ya da extern anahtar sözcüğü olmadan yalnızca inline anahtar sözcüğü ile tanımlanmışsa bu durumda derleyici fonksiyonu
|
||
inline açarsa sorun olmaz. Ancak derleyici fonksiyonu inline olarak açmazsa fonksiyonun tanımlamasını amaç kod içerisine yerleştirmez. Yani sanki fonksiyon
|
||
hiç tanımlanmamış gibi işlem yapar. Fakat fonksiyonu CALL makine komutuyla çağırır. İşte bu durumda eğer inline açım yapılamadıysa ve başka bir modülde de
|
||
aynı isimli bir fonksiyon yoksa muhtemelen build işlemi link aşamasında çağrılan fonksiyonu linker'ın bulamaması biçiminde error ile sonuçlanacaktır.
|
||
Örneğin:
|
||
|
||
/* sample.c */
|
||
|
||
#include <stdio.h>
|
||
|
||
inline int square(int a)
|
||
{
|
||
return a * a;
|
||
}
|
||
|
||
int main(void)
|
||
{
|
||
int result;
|
||
|
||
result = square(3);
|
||
|
||
printf("%d\n", result);
|
||
|
||
return 0;
|
||
}
|
||
|
||
Burada square fonksiyonu static ya da extern belirleyicisi olmadan yalnızca inline belirleyicisi ile tanımlanmıştır. Eğer fonksiyon inline açılamazsa
|
||
link aşamasında error olulacaktır. Örneğin biz gcc ve clang derleycilende bu fonksiyonu optimizasyon seçeneklerini açmadan aşağıdaki gibi derlemek isteyelim:
|
||
|
||
gcc -o sample sample.c
|
||
|
||
Bu durumda link aşamasında şöyle bir hata alırız:
|
||
|
||
/usr/bin/ld: /tmp/ccWaZ5zm.o: in function `main':
|
||
sample.c:(.text+0x1d): undefined reference to `square'
|
||
collect2: error: ld returned 1 exit status
|
||
|
||
Ancak biz program- -O2 seçeneği ile derlersek derleyici muhtemelen inline açım yapacağı için her ne kadar square fonksiyonunu amaç koda yerleştirmeyecekse de
|
||
programın derlenip çalışmasında muhtemelen bir hata oluşmayacaktır:
|
||
|
||
gcc -O2 -o sample sample.c
|
||
|
||
Yalnızca inline belirleyicisi ile fonksiyonu tanımladığımızda derleyici inline açımı yapsın ya da yapmasın fonksiyon kodunu amaç koda yazmadığına dikkat ediniz.
|
||
|
||
static inline fonksiyonlar eğer derleyici tarafından inline olarak açılmazlarsa static biçimde amaç dosyaya yerleştirilmektedir. Böylece derleyici CALL makine komutu
|
||
ile bu static fonksiyonu çağırmış olmaktadır. Yani bu durumda derleyici inline fonksiyonu açsa da açmasa da programın derlenip çalışmasında bir sorun oluşmayacaktır. Örneğin:
|
||
|
||
static inline int square(int a)
|
||
{
|
||
return a a a;
|
||
}
|
||
|
||
Tabii static inline fonksiyonlarda derleyici inline açımı yaparsa aslında fonksiyonu hiç amaç dosyaya yazmayabilir. (Nasıl olsa fonksiyonun iç kodu
|
||
enjekte edişlmiştir ve fonksiyon da başka modülden zaten kullanılamamaktadır.) Ancak inline açımı yapamazsa static düzeyde fonksiyonu amaç dosyaya
|
||
yazacaktır. Tabii fonksiyon static olduğu için başka bir modülden çağrılması mümkün olmayacaktır.
|
||
|
||
static inline fonksiyonlar C'de en çok kullanılan inline fonksiyonalrdır. Ancak bunların da en önemli dezavantajı birden fazla modülde aynı static inline
|
||
fonksiyonun kullanılması durumunda eğer inline açım yapılamazsa bu fonksiyonların kodlarının ayrı ayrı bu modüllerde bulunması zorunluluğudur.
|
||
|
||
Normal bir fonksiyon zaten extern biçimdedir. Yani onun tanımlamasının önüne extern getirilip getirilmemesinin bir farkı yoktur. Örneğin:
|
||
|
||
extern void foo(void)
|
||
{
|
||
...
|
||
}
|
||
|
||
Burada extern belirleyicisi gereksiz kullanılmıştır. Fonksiyonlar zaten default external linkage'a sahiptir. Ancak extern anahtar sözcüğü ile
|
||
inline anahtar sözcüğü bir arada kullanılırsa bu başka bir anlam ifade etmektedir. extern inline fonksiyonlar derleyici tarafından inline açılsalar da açılmasalar da
|
||
her zaman object dosyaya extern linkage biçiminde yazılırlar. Örneğin:
|
||
|
||
extern inline int square(int a)
|
||
{
|
||
return a * a;
|
||
}
|
||
|
||
Burada fonksiyon extern ilnine olarak tanımlanmıştır. Derleyici çağrı sırasında bu fonksiyonu inline olarak açsa da açmasa da kod yine bu fonksiyonu
|
||
normal external linkage'a sahip bir fonksiyon olarak yazar. Bu durumun dezavatajı şudur: Biz extern inline bir fonksiyonu birden fazla modülde kullanırsak
|
||
birden fazla extern tanımalası modüllerde bulunur. Bu da link aşamasında soruna yol açar. Özetle üç inline fonksiyon arasındaki farlılıklar şunlardır:
|
||
|
||
1) inline: Derleyici inline açım yaparsa sorun olmaz, ancak yapamazsa CALL makine komutunu yerleştirir. Başka modülde bu fonksiyon yoksa
|
||
link aşamasında error oluşur.
|
||
|
||
2) static inline: Derleyici inline açım yapsa da yapmasa da sorun oluşmaz. Ancak fonksiyonu inline açamazsa static linkage'a sahip biçimde amaç dosyaya
|
||
yazar ve fonksiyonu CALL makine komutuyla çağırır.
|
||
|
||
3) extern inline: Derleyici inline açım yaparsa sorun olmaz. Açamazsa CALL makine komutunu kullanır. Ancak her zaman derleyici fonksiyonu
|
||
amaç dosyaya extern linkage'a sahip olacak biçimde yazar. Dolayısıyla birden fazla modülde bu fonksiyon kullanılırsa error oluşur.
|
||
|
||
Burada programcı nasıl bir yol izlemeidir? Eğer programcı inline fonksiyon derleyici tarafından inline açılamıyorsa bir sorun oluşmasın istiyorsa
|
||
ya static inline tanımlamasını tercih etmelidir ya da inline tanımlamasını tercih edip ayrıca fonksiyonun static olmayan bir tanımlamasını başka bir modülde
|
||
bulundurmalıdır. C'de inline fonksiyonlar için en çok tercih edilen biçim "static inline" biçimidir.
|
||
|
||
inline fonksiyonlar her derleme işleminde derleyici tarafındna görülmek zorundadır. Dolayısıyla bu fonksiyonlar kütüphanelere yerleştirilemezler.
|
||
Birden fazla modülle çalışırken biz tipik olarak inline fonksiyonları bir başlık dosyasına yerleştirir. Bu başlık dosyasını da birden fazla modülde
|
||
include edebiliriz. Örneğin:
|
||
|
||
/* project.h */
|
||
|
||
#ifndef PROJECT_H_
|
||
#define PROJECT_H_
|
||
|
||
static inline foo()
|
||
{
|
||
...
|
||
}
|
||
|
||
#endif
|
||
|
||
/* sample.c */
|
||
|
||
#include <stdio.h>
|
||
#include "project.h"
|
||
...
|
||
|
||
/* mample.c */
|
||
|
||
#include <stdio.h>
|
||
#include "project.h"
|
||
...
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
C99 ile birlikte standartlara "restrict" isimli bir tür niteleyicisi daha eklenmiştir. C++ C'yi temel aldığı halde bu restrict niteleyicisini bünyesine katmamıştır.
|
||
restrict niteleyicisi yalnızca göstericilerle "üst düzey (top level)" biçimde kullanılablir. Yani sıradan bir nesne restrict olamaz. Yalnızca göstericiler restrict olabilir.
|
||
Göstericilerin gösterdiği yer restrict olamaz yalnızca kendileri restrict olablir. Örneğin:
|
||
|
||
restrict int a; /* geçersiz */
|
||
restrict int *pi; /* geçersiz! */
|
||
char * restrict pc; /* geçerli */
|
||
|
||
restrict anahtar sözcüğü "pointer aliasing" denilen bir optimizasyonu mümkün hale getirebilmek için düşünülmüştür. Bir göstericinin gösterdiği yere erişim en az
|
||
iki makine komutu ile yapılmaktadır. Örneğin:
|
||
|
||
a = *pi;
|
||
|
||
Bunun muhtemel makine kaomutları şöyle olacaktır:
|
||
|
||
mov reg1, pi
|
||
mov reg2, [reg1]
|
||
mov a, reg2
|
||
|
||
Pekiyi bir kod içerisinde *p gibi bir kullanım birden fazla yerde varsa derleyici daha önceden elde ettiği *p değerini opimizasyon amaçlı saklayıp kullanabilir mi?
|
||
Yanıt kullanabilir. Ancak derleyicinin o göstericinin gösterdiği yerdeki nesnenin değişmemiş olduğunu garanti etmesi gerekir. Örneğin:
|
||
|
||
for (;;) {
|
||
a = *pi;
|
||
foo();
|
||
}
|
||
|
||
Derleyici burada foo fonksiyonun pi'nin gösterdiği yerdeki nesneyi değiştirmiş olabileceğini dikkate almak zorundadır. Dolayısıyla *pi'ye yeniden başvuru yapabilir.
|
||
Tabii derleyici foo'nun içini görürse ve *pi'nin yerel bir nesneyi gösterdiğini bilirse yani ""bazı koşullar altında"" foo'nun *pi'yi değiştirmeyeceğini anlayabilir.
|
||
Bu durumda optimizasyon yapabilir. Ancak bu karmaşık bir durumdur. Bazen bunu anlamak çok zor bazen de mümkün olmayabilir. İşte restrict anahtar sözcüğü
|
||
bu tarz optimizasyonları derleyici yapabilsin diye uydurulmuştur. restrict anahtar sözcüğü derleyiciye şunu demektedir: "Derleyici bu göstericinin gösteridiği
|
||
yerdeki nesneye ben yalnızca bu gösterici ile erişiyorum. Başka bir fonksiyon ya da başka bir yolla ona erişmeyeceğime söz veriyorum". Böylece restrict
|
||
göstericilerde derleyici o göstericilerin gösterdiği yerdeki nesnelerin erişiminde optimizasyonlar yapabilmektedir. Pekiyi biz bir göstericiyi restrict yaptığımız halde
|
||
o göstericinin gösterdiği yere başka bir nesne ya da gösterici yoluyla erişirsek ne olur? Bu durum C'de tanımsız davranışa yol açmaktadır. Çünkü burada
|
||
restrict ile verilen söz programcı tarafından tutulmamıştır.
|
||
|
||
restrict göstericiler C99'da fonksiyon prototiplerinde de sıkça karşımıza çıkmaktadır. Örneğin:
|
||
|
||
char *strcpy(char * restrict dest, const char * restrict source);
|
||
|
||
Burada restrict gösterici gördüğümüzde ne anlamalıyız? İşte mademki restrict göstricilerin gösterdiği yere yalnızca o gösterciler yoluyla erişilir o halde
|
||
burada fonksiyona verdiğimiz iki adres çakışık olmamalıdır. Eğer bu iki adres çakışık olursa "tanımsız davranış (undefined behavior)" oluşur. Örneğin:
|
||
|
||
void *memcpy(void * restrict s1, const void * restrict s2, size_t n);
|
||
|
||
Burada da blokların çakışık olmaması gerekir. Ancak örneğin:
|
||
|
||
void *memmove(void *s1, const void *s2, size_t n);
|
||
|
||
Burada restrict gösterici kullanılmadığına göre bloklar çakışık olabilir. Şüphesiz göstericinin gösterdiği yerde bir güncelleme yapmayan fonksiyonlarının
|
||
prototiplerinde restrict gösterici bulundurmanın bir anlamı yoktur. Örneğin:
|
||
|
||
int strcmp(const char *s1, const char *s2);
|
||
|
||
Bazı işlemcilerde blok kopyalaması yapan makine komutları bulunmaktadır. Örneğin böyle bir işlemci söz konusu olsun. Biz de aşağıdaki gibi
|
||
bir fonksiyonu yazmış olalım:
|
||
|
||
void reverse_copy(void *dest, void *source, size_t n)
|
||
{
|
||
...
|
||
}
|
||
|
||
Bu fonksiyon ikinci parametresiyle verilen diziyi birinci parametresiyle verilen diziye tersten kopyalacak olsun. Bir işlemci bunu tek bir makine komutuyla
|
||
önce source'dan n kadar byte kendi içine çekip sonra bunu dest'e kopyalayabilir. Ancak burada bloklar çalıkışıksa fonksiyonun iç kodunun yapma çalıştığı
|
||
şey bu makine komutuyla yapılamayacaktır. Bu durumda biz derleyiciye bir güvence verebiliriz:
|
||
|
||
void reverse_copy(void * restrict dest, void * restrict source, size_t n)
|
||
{
|
||
...
|
||
}
|
||
|
||
Şimdi artık biz derleyiciye şunu demekteyiz: "Derleyici ben hiçbir zaman source ve dest adreslerinin gösterdiği yerlere başka bitr yolla erişmeyeceğim.
|
||
Dolayısıyla benim adresini geçtiğim diziler çakışık olmayacaktır. Sen bunların çakışık olmayacağı garantisiyle istediğin optimizasyonu yap".
|
||
|
||
C++'ta restrict göstericiler olmadığı için eğer kodun C++ uyumu olması isteniyorsa restrict göstericiler kullanılmamalıdır.
|
||
----------------------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
/*----------------------------------------------------------------------------------------------------------------------
|
||
|
||
----------------------------------------------------------------------------------------------------------------------*/
|
||
|
||
|
||
|
||
|