C Programlama Dili Kurs Notları Kaan ASLAN C ve Sistem Programcıları Derneği Güncelleme Tarihi: 12/07/2003 Bu kurs notları Kaan ASLAN tarafından yazılmıştır. Kaynak belirtilmek koşuluyla her türlü alıntı yapılabilir. --------------------------------------------------------------------------------------------------------------------------------------------------- C Programlama Dilinin Tarihsel Gelişimi C Programalama Dili 1970-1971 yıllarında AT&T Bell Lab.'ta UNIX İşletimn Sisteminin geliştirilmesi sürecinde bir yan ürün olarak tasarlandı. AT&T o zamanlar Multics isimli bir işletim sistemi projesinde çalışıyordu. AT&T bu projeden çakildi ve kendi işletim sistemlerini yazma yoluna saptı. Bu işletim sistemine Mulctics'ten kelime oyunu yapılarak UNIX ismi verildi. UNIX DEC firmaısnının PDP-7 makinalarında ilk kez yazılmıştır. O zamanlar işletimsistemleri sembolik Makine dilinde yazılıyordu. Ken Thompson (grup lideri) işleri kolaylaştırmak için B isimli bir programlama dilciği tasarladı. Sonra Dennis Ritchie bunu geliştirrek C haline getirdi. C Programlama dili UNIX proje ekibindeki Dennis Ritchie tarafından geliştirilmiştir. UNIX İşletim Sistemi 1973 yılında sil baştan yeniden C ile yeniden yazılmıştır. O zamana kadar bir işletimsistemi yüksek seviyeli bir dilde yazılmış değildi. Bunedenle UNIX'in C'de yazılması bir devrim niteliğindir. UNIX sayesinde C Programalam Dili 1970'lerde tanınmaya başladı. 1980-81 yıllarında IBM ilk kişisel bilgisayarı çıkarttı. C Programlama Dili kişisel bilgisayarlarda kullanılan en yaygın dil oldu. 1978 yılında Dennis Ritchie ve Brian Kernigan tarafından “The C Programming Language” kitabı yazıldı. Bu kitap üm zamanların en önemli bilgisayar kitaplarından biridir. (Hatta HelloWorld programı ilk kez burada yazılmıştır.) C Programlama dili ilk kez 1989 yılında ANSI tarafından standardize edildi. ISO 1990 yılında ANSı stanadrşlarını alarak (bölüm numaralandırmasını değiştirerek) ISO standardı olarak onayladı. Bu standardın resmi ismi ISO/IEC 9899: 1990'dı. Bu standartlar kısaca C90 ismiyle bilinmektedir. C 1999 yılında bazı özelliker eklenerek yeniden standardize edildi. Bu standartlara da ISO/IEC 9899: 1999 denilmektedir ve kısaca C99 olarak bilinir. C99 derleyici yazan firmalar ve kurumlar tarafından çok fazla destek görmedi. C'ye nihayet 2011 yılında bazı özellikler eklenmiştir. Bu son standartlar ISO/IEC 9899:2011 kod adıyla yayınlanmıştır. Buna da kısaca C11 denilmektedir. Bu kursta klasik C olan C90 ele alınmaktadır. C99 ve C11'deki yeni özellikler "Sistem Programlama Ve C İleri C Uygulamaları" kursunda ele alınacaktır. C denildiğinde default olarak akla C90 gelmelidir. Anahtar Notlar: Bu kurs C Programlama Dilini her yönüyle anlatan bir kurstur. Bu kurstan sonra aşağıdaki kursalara katıllınabilir: - Sistem Programlama ve İleri C Uygulamaları (I) (kesinlikle tavsiye edilir) - C++ (kesinlikle tavsiye edilir) - Qt ile C++'ta Programlama (pencereli Windows ve Linux programları yazmak için) - UNIX/Linux Sistem Programalama - Windows Sistem Programalama - Sistem Programlama ve İleri C Uygulamaları (II) - PIC mikrodenetleyicileri ile programlama (biraz elektonik bilgisi tabanı gerekir). 1 - PIC mikrodenetleyicileri ile programlama (biraz elektonik bilgisi tabanı gerekir). C Programlama Dili şu an itibari ile Tiobe Index'e göre dünyanın en fazla kullanılan programlama dilidir. Programalama Dillerinin Sınıflandırılması Programlama dilleri teorisyenler tarafından genellikle üç biçimde sınıflandırılmaktadır: 1) Seviyelerine Göre Sınıflandırma 2) Kullanım Alanlarına Göre Sınıflandırma 3) Programlama Modeline Göre Sınıflandırma Seviyelerine Göre Sınıflandırma Seviye (level) bir programlama dilinin insan algısına yakınlığının bir ölçüsüdür. Yüksek seviyeli diller insana yakın yani kolay dillerdir. Alçak seviyeli diller makinaya yakın fakat zor öğrenilen dillerdir. Olabilecek en aşağı seviyeli dil saf makina dilidir (machine language). Bu dil yalnızca 1'lerden ve 0'lardan oluşur. Görsel diller, veritabanı dilleri vs. çok yüksek seviyeli dilledir. Java, C#, Pascal, Basic, PHP vs. diller yüksek seviyeli dillerdir. C Programlama Dili orta seviyeli bir dildir. Yani makinaya diğer dillerden daha yakındır. Kullanım Alanlarına Göre Sınıflandırma Programlama dilleri bazı konularda bazı iddialara sahiptir. Bzı dillerde web işlemleri yapmak daha kolayken, bazı dillerde veritabanı işlemleri yapmak daha kolaydır. İşte bir dilin hangi tür uygulamalarda kullanılabileceğine yönelik sınıflandırnadır bu. Bilimsel ve Mühendislik Diller: Bu tür diller bilimsel ve mühendislik problemlerin çözülmesi için birincil olarak tercih edilen dillerdir. Örneğin C, C++, Fortran, Pascal, Java, C#, Matlab vs. Veritabanı Dilleri: Bunlar veritabanlarının oluşturulması ve idaresinde tercih edilen dillerdir. SQL, Foxpro, Dbase vs. gibi... Web Dilleri: İnteraktif web sayfalarını oluşturabilmek için tercih edilen dillerdir. Örneğin Java, C#, PHP, Python gibi... Yapay Zeka Dilleri: İnsan düşüncesini eşitli boyutlarda taklit eden programlara yapay zeka programları denir. Bu programların yazılması için tercih edilen dillere yapay zeka dilleri denir. Örneğin C, C++, Java, C#, Lisp, Prolog vs. gibi Görsel ve Animasyonm Dilleri: Animasyon programları için kullanılan yüksek seviyeli script dilleridir. Action Script gibi... Sistem Programlama Dilleri: Sistem Programlama yapmak için kullanılan dilledir. Tipik olarak C, C++ ve sembolikmakina dilleri sistem programlama dilleridir. Anahtar Notlar: Sistem programları bilgisayar donanımı ile arayüz oluşturan, uygulma programlarını çeşitli bakımlardan hizmet veren, aşağı seviyeli taban programlardır. Örneğin: İşletim sistemleri, editörler, derleyiciler ve bağlayıcılar, haberleşme programları vs. Programlama Modeline Göre Sınıflandırma Programlama modeli o dilde programı hangi teknikle yazdığımıza ilişkindir. Bazı dillerin bazı tekniklerin kullanılmasını açıkça desteklemektedir. Temel olarak (fakat daha fazla olabilir) programlama dillerini 2 prograramlama modellerine (programming paradigm) 4 biimde sınıflandırabiliriz: 1) Prosedürel Programlama Dilleri: Bu diller prosedürel programlama modelini destekleyen dillrdir. Örneğin C prosedürel bir programlama dilidir. Prosedürel teknikte program altprogramların (prosedürlerin) birbirlerini çağırmasıyla yazılır. Bunun dışında klasik Pascal, Basic, Fortran vs. gibi diller prosedürel dillerdir. 2) Nesne Yönelimli Programlama Dilleri: Bu diller nesne yönelimli programlama modelini (object oriented programming) uygulayabilecek yetenkte dillerdir. Bu modelde program sınıflar oluşturarak yazılır. C++, C'nin nesne yönelimli versiyonudur. Pek prosedürel dilin daha sonra nesne yönelimli versiyonları oluşturulmuştur. Örneğin Object Pascal, VB.NET. C#, Java temelde nesne yönelimli dillerdir. 3) Fonksiyonel Programlama Dilleri: Bu tür diller fonksiyonel programlama modelini (functional programming paradigm) açıkça destekleyen dillerdir. Örneğin Haskell, F#, R, Scheme, Erlang. Fonksiyonel teknikte programlar sanki formül yazarmış gibi yazılırlar. 4) Çok Modelli Programlama Dilleri: Bu dillerde birden fazla programlama modeli kullanılabilmektedir. Örneğin C++'ta hem prosedürel hem de nesne yönelimli teknik kullanılabilir. C++ çok modelli (multi paradigm) bir programlama dilidir. Yeni özelliklerle C# ve Java'da artık çok modelli olmuştur. Temel Kavramlar İşletim Sistemi (Operating System): İşletim sistemi makinanın donanımını yöneten temel bir sistem yazılımıdır. Bir kaynak yöneticisi gibi çalışır. Yönettiği tipik donanımsal kaynaklar şunlardır: İşlemci, RAM, Disk, Yazıcı, Network kartı ve haberleşme sistemi. Eğer işletim sistemi olmasaydı bilgisayar hiç kolay kullanılamazdı. İşletim sistemi temel pek çok hizmeti bizim için sağlamaktadır. Kullanıcı ile makine donanımı arasında köprü kurmaktadır. İşletim sistemi insan vücudu gibi çeşitli alt sistemlere sahiptir. Bu alt sistemler karşılıklı etkileşim halindedir. İşletim sistemlerini kabaca çekirdek (kernel) ve kabuk (shell) olmak üzere iki katmana ayırabiliriz. Çekirdek makinanın donanımını yöneten motor kısımdır. Kabuk kullanıcı ile ilişki kurankısımdır. (Örneğin Windows'un masaüstü kabuk durumundadır). Tipik olarak (fakat her zaman değil) biz işletim sisteminin üzerinde çalışırız. En çok kullanılan Masaüstü işletim sistemleri Windows, Linux (UNIX türevi sistemler), Mac OS X sistemleridir. En çok kullanılan mobil işleim sistemleri de Android, IOS, Windows Mobile sistemleridir. Çevirici Programlar, Derleyiciler ve Yorumlayıcılar: Bir programlama dilinde yazılmış olan programı eşdeğer olarak başka bir dile dönüştüren programlara çevirici programlar (translators) denilmektedir. Burada asıl programın diline kaynak dil (source language), dönüştürülecek dile hedef dil (target language) denilmektedir. Bir çevirici programda hedef dil alçak seviyeli bir dilse (sembolik Makine dili, ara kod ya da saf makine dili) böyle çevirici programlara derleyici (compiler) denir. Yorumlayıcılar (interpreters) kaynak kodu doğrudan okuyup o anda çalıştırırlar. Bir hedef kod üretmezler. Dolayısıyla bunlar çevirici program durumunda değillerdir. Bazı diller için yalnızca derleyiciler kullanılır (Örneğin C gibi). Bazıları için yalnızca yorumlayıcılar kullanılır (örneğin PHP, Perl gibi), Bazı dillerin derleyicileri de yorumlayızıları da vardır (örneğin Basic gibi). Genel olarak yorumlayıcı yazmak derleyici yazmaktan daha kolaydır. Derleyiciler kodun daha hızlı çalışmasına yol açarlar. Yorumlayıcılarda kaynak kod gizlenemez. Açık Kaynak Kod ve Özgür Yazılım 1980'li yılların ortalarında Richard Stallman, FSF (Free Software Foundation) isimli bir dernek kurdu ve özgür yazılım (free software) kavramını ortaya attı. Özgür yazılım kabaca, yazılan programların kaynak kodlarını sahiplenmemek, başkalarının da onu devam ettirmesini sağlamak anlamına gelir. Birisi özgür yazılım lisansına sahip bir ürünü değiştirdiği ya da özelleştirdiği zaman onu o da açmak zorundadır. Fakat alıcı varsa bunlar parayla da satılabilir. Açık kaynak kod (open source) aşağı yukarı (fakat tamamen değil) özgür yazılımla aynı anlama 3 gelmektedir. Her yazılımın bir lisansı vardır. Fakat çeşitli yazılım akımları için çeşitli lisanslar kalıp olarak oluşturulmuştur. Örneğin Özgür yazılımın ana lisansı GPL(Gnu Public Licence)'dir. Fakat GPL'ye benzer pek çok lisans vardır (BSD, Apachie, MIT, ...). Richard Stallman FSF'yi kurduğunda GNU isimli bir proje başlattı. Bu projenin amacı bedava, açık kaynak kodlu bir işletim sistemi ve geliştirme araçları yazmaktı. Gerçekten de gcc derleyicisi, ld bağlayıcısı ve diğer pek çok utility program bu proje kapsamında yazıldı. Linux işletim sistemi GNU malzemeleri kullanılşarak Linus Torwalds'ın önderliğinde geniş bir ekip tarafından geliştirilmştir ve sürdürülmektedir. Linux aslında bir çekirdek geliştirme projesidir. Bugün Linux diye dağıtılan sistemlerde binlerce açık kaynak kodlu yazılım bulunmaktadır. Yani Linux adeta GNU projesinin işletim sistemi gibi olmuştur (maalesef GNU projesinin öngörülen işletim sistemi bir türlü son haline getirilememiştir.) Bu nedenle bazı kesimler bu işletim sisteminin isminin Linux değil GNU/Linux olması gerektiğini söylemektedir. Çok kullanılan C Derleyicileri Pek çok derleyicisi olsa da bugün için en önemli iki derleyici Microsoft'un C derleyicileri ve gcc derleyicileridir. Ekiden Borland derleyicileri de çok kullanılıyordu. Intel firmasının da ayrı bir C derleyicisi vardır. UNIX/Linux sistemlerinde gcc derleyicileri çok yoğun kullanılırken, Windows sistemlerinde Microsoft C derleyicileri yoğun kullanılmaktadır. Gcc'nin Windows portuna MinGW denilmektedir. IDE Kavramı Normal olarak derleyiciler komut satırından çalıştırılan programlardır. Yani biz programı editör denilen bir ortamda yazarız. Bunu save ederiz. Sonra da komut satırından derleriz. Bu tarz çalışma biraz zahmetli olduğu için IDE (Integrated Development environment) denilen özel yazılımlardan faydalanılır. IDE'lerin editörleri vardır, menüleri vardır ve birtakım araçları vardır. IDE derleyici değildir. Derleyicinin dışındaki yazılım geliştirmeyi kolaylaştıran araçlar toplamıdır. Bugün pek çok IDE seçenek olarak bulunmaktdır. Microsoft'un ünlü IDE'sinin ismi “Visual Studio”dur. Kursumuzda ağırlıklı bu IDE kullanılacaktır. Eclipse, Netbeans, MonoDevelop gibi open source IDE'ler de vardır. Apple'ın da X-Code denilen kendi bir IDE'si vardır. IDE derleyici değildir. IDE'ye “derle” denildiği zaman IDE gerçek derleyiciyi çağırır. Biz bilgisayarımıza bir IDE yüklediğimizde yalnızca IDE değil, onun kullanacağı derleyici de yüklenecektir. Tabi IDE olmadan yalnızca drleyiciyi de yükleyebiliriz. Visual Studio paralı bir IDE'dir. Ancak Microsoft “Express Edition” ismiyle bu IDE'nin bedava bir versiyonunu da oluşturmuştur. Bu kurs için ve genel olarak C programlama için Express Edition yeterlidir. Visual Studio Windows sistemlerinde kullanılabilen bir IDE'dir. Oysa Eclipse ve Netbeans IDE'leri corss platform'dur. Linux sistemlerinde IDE olarak QtCreator, Eclipse, Netbeans ya da MonoDevelop tercih edilebilir. Şu anda Visual Studio IDE'sisn son versiyonu “Visual Studio 2013”tür. Sayı Sistemleri Biz günlük hayatımızda 10'luk sistemi kullanmaktayız. 10'luk sistemde sayıları ifade etmek için 10 sembol kullanılır: 0 1 2 3 4 5 6 7 8 9 (toplam 10 tane) 4 Aslında 10'luk sistemdeki sayıların her bir basamağı 10'un kuvvetlerini temsil eder. Örneğin: 123 = 3 * 100 + 2 * 101 + 1 * 102 10'luk sistemde her bir basamağ “digit” denilmektedir. Makina ikilik sistemi kullanır. Çünkü sayıların elektriksel olarak ifade edilmeleri ikilik sistemde çok kolaydır ve standartlaşma böyle oluşmuştur. Elektroniğin bu alanına dijital elektronik denilmektedir. İkilik sistemde toplam 2 sembol vardır: 0 ve 1 (toplam 2 tane) İkilik sistemdeki bir sayı ikinin kuvvetleriyle çarpılarak elde edilir. Örneğin: 1010 = 0 * 20 + 1 * 21 + 0 * 22 + 1 * 23 İkilik sistemdeki her bir basmağa bit (binary digit'ten kısaltma) denilmektedir. Örneğin: 10100010 sayısı 8 bitlik bir sayıdır. Bilgisayarımızın belleği de yalnızca bitleri tutar. Yani bellkekteki herşey bit'lrden oluşmaktadır. Bit en düşük bellek birimidir. 8 bite byte denilmektedir. Kilo 102luk sistemde 1000 katı anlamına gelir (örneğin 1 Kilometre 1000 metredir.) Fakat bilgisayar bilimlerinde kilo olarak 1000 anlamlı gözükmemektedir. Çünkü 1000 sayısı 10'un kuvvetidir. İşte bilgisayar bilimlerinde kilo 1024 katı anlamına gelir (1024, 2'nin 10'uncu kuvvetidir ve 1000'e de benzemektedir.). Mega da kilonun 1024 katıdır. Bu durumda 1 MB = 1024 * 1024 byte'tır. Giga da mega'nın 1024 katıdır. Bilgisayarın belleğindeki her şey 2'lik sistemdeki sayılardan oluşur: Komutlar, yazılar, sayılar hep aslında ikilik sistemde bulunurlar. Tamsayıların 2'lik Sistemde İfade Edilmesi Tamsayılar için iki sistem söz konusudur: İşaretsiz sistem ve işaretli sistem. İşaretsiz (unsigned) sistemde tamsayı ya pozitif ya da sıfır olabilir. Fakat negatif yorumlanamaz. İşaretli (signed) sistemde tamsayı negatif de olabilir. Örneğin aşağıdaki 8 bit sayı işaretsiz tamsayı olarak yorumlanırsa kaçtır? 1000 1010 = 138 8 bitle işaretsiz tamsayı sisteminde ifade edilecek en küçük sayı 0, en büyük sayı 255'tir. 0000 0000 ......... 1111 1111 --> 0 --> 255 2 byte (16 bit) ile işaretsiz sistemde ifade edilebilecek en küçük sayı 0, en büyük sayı 65535'tir. 0000 0000 0000 0000 --> 0 .... .... .... .... 1111 1111 1111 1111 --> 65535 5 İşaretli tamsayıları ifade etmek için tarih boyunca 3 sistem denenmiştir. Bugün ikiye tümleme sistemi denilen sistem en iyi sistem olarak kullanılmaktadır. Bu sistemin özellikleri şöyledir: - Sayının en solundaki bit işaret bitidir. Bu bit 1 ise sayı negatif, 0 ise pozitiftir. - Negatif ve pozitif sayılar birbirlerinin ikiye tümleyenidirler. - Bu sistemde bir tane sıfır vardır. Bir sayının ikiye tüöleyene sayının 1'e tümleyenine 1 eklenerek elde edilir (Syının 1'e tümleyeni 1'lerin 0, 0'ların 1 yapılmasıyla elde edilir.) Örneğin: Sayı: 1011 1010 1'e tümleyeni: 0100 0101 2'ye tümleyeni: 0100 0101 + 1 = 0100 0110 Bu işlemin klay bir yolu vardır: Sayının sağında sola doğru ilk 1 görene kadar (ilk 1 dahil olmak üzere) aynısı yazılarak ilerlenir. Sonra 0'lar 1, 1'ler 0 yapılarak devam edilir. Örneğin: Sayı: 1010 1100 2'ye tümleyeni: 0101 0100 Örneğin: Sayı: 1111 1111 2'ye tümleyeni: 0000 0001 Sayının 2'ye tümleyeninin 2'ye tümleyeni sayının kendisine eşittir. Örneğin: Sayı: 1111 1111 2'ye tümleyni: 0000 0001 2'ye tümleynin 2'ye tümleyeni: 1111 1111 Bu sistemde negatif ve pozitif sayılar birbirlerinin 2'ye tümleyenidirler. Örneğin: 0000 1010 --> +10 1111 0110 --> -10 Bu sistemde negatif bir sayı yazmak istersek önce onun pozitiflisini yazıp sonra 2'ye tümleyenini alarak yazabiliriz. (Tabi doğrudan yazabiliyorsak ne mutlu bize.) Örneğin, bu sistemde -1 yazmak isteyelim: 0000 0001 --> +1 1111 1111 --> -1 Bu sistemde birisi bize “bu sayı kaç” diye sorduğunda şöyle yanıtı bulabiliriz: Önce sayının işaret bitine bakıp onun pozitif mi yoksa negatif mi olduğunu belirleriz. Sayı pozitif ise doğrudan hesaplarız. Negatif ise, sayının ikiye tümleyeni alırız. Ondan faydalanarak sayıyı belirleriz. Örneğin, aşağıdaki sayı işaretli tamsayı sisteminde kaçtır? 1111 0101 Sayı negatif, 2'ye tümleyenini alaım: 0000 1011 6 bu +11 olduğuna göre demek ki o sayı -11'dir. Bu sistemde 1 tane sıfır vardır. Bu sistemde 2 tuhaf sayı vardır ki bunların 2'ye tümleyenleri yoktur (yani kendisiyle aynıdır). Bunlar tüm bitleri 0 olan sayı ve ilk biti 1, diğer bitleri 0 olan sayıdır: 0000 0000 (2'ye tümleyeni yok) 1000 0000 (2'ye tümleyeni yok) İşte bu ilk sayı 0'dır, diğeri ise -128'dir. Yani 8 bit içerisinde işaretli tamsayı sisteminde yazılabilecek en büyük pozitif ve en küçük negatif sayılar şöyledir: 0111 1111 --> +127 1000 0000 --> -128 Peki 2 byte (16 bit) içerisinde yazılabilecek en büyük ve en küçük işaretli sayılar nelerdir? 0111 1111 1111 1111 --> +32767 1000 0000 0000 0000 --> -32768 Peki 4 byte (32 bit) içerisinde yazılabilecek en büyük ve en küçük işaretli sayılar nelerdir? 0111 1111 1111 1111 1111 1111 1111 1111 --> +2147483647 1000 0000 0000 0000 0000 0000 0000 0000 --> -2147483648 Soru: Aşağıdaki 1 byte'lık sayı işaretli ve işaretsiz tamsayı sistemlerinde kaçtır? 1111 1111 Cevap: İşaretsiz sistemde +255, işaretli sistemde -1'dir. Peki bu sistemde örneğin 1 byte içerisindeki en büyük pozitif sayıya 1 toplarsak ne olur? Bir kere zaten sınırı aşmış oluruz. Peki aşınca ne olur? 0111 1111 --> +127 0000 0001 --------------1000 0000 --> -128 Peki 2 toplasaydık ne olurdu? Yanıt: 1000 0001 = -127 Demek ki bu sistemde pozitif sınırı yanlışlıkla aşarsa kendimi negatif yüksek sayılarda buluruz. Buna programlamada üstten taşma (overflow) denilmektedir. Taşma alttan da (underflow) olabilir. Örneğin -128'den 1 çıkartırsak +127 elde ederiz. Gerçek Sayıların (Noktalı Sayıların) 2'lik Sistemde İfade Edilmesi Noktalı sayıları 2'lik sistemde ifade edebilmek için tarih boyunca iki tür format grubu kullanılmıştır. Sabit noktalı (fixed point) formatlarda noktanın yeri sabittir. Onun solunda sayının tam kısmı, sağında noktadan sonraki kısım utulur. Örneğin 12.567 bu tür formatlarda şöyle tutulmaktadır: Tabi sayının tam ve noktalı kısımları burada 10'luk sistemde değil, 2'lik sistemde tutulmaktadır. Sabit noktalı formatlar pek verimli değildir. Çünkü dinamik değildir. Yani sayının tam kısmı büyük fakat noktalı kısmı küçükse ya da tersi durumda sayı ifade edilemez. Halbuki yeteri kadar yer vardır. Bunun için kayan noktalı 7 (floating point) formatlar geliştirilmiştir. Bugün bilgisayarlarımızda bu formatlar kullanılmaktadır. Kayan noktalı formatlarda sayı sanki noktası yokmuş gibi yazılır. Noktanın yeri ayrıca format içerisinde belirtilir. Örneğin: Sayının noktası kaldırılmış olan haline mantis (manissa) denir. Noktanın yerini belirten kısmına ise genellikle üstel kısım (exponential part) dnilmektedir. Çeşitli kaynak noktalı formatlar olsa da bugün hemen tüm sistemlerde IEEE'nin 754 numaralı formatı kullanılmaktadır. Tabi 2'lik sistemde sayının noktadan sonraki kısmı 2'nin negatif kuvvetleriyle çarpılarak oluşturulmaktadır. Bu da yuvarlama hatası (rounding error) bir hatanın oluşmasına zemin hazırlar. Yuvarlama hatası noktalı bir sayının tam olarak ifade edilemeyip ona yakın bir sayının ifade edilmesiyle oluşan bir hatadır. Yuvarlama hatası elimine edilemez. Olsa olsa onun etkisi aazaltılabilir. Bunun için de sayının daha geniş olarak ifade edilmesi yoluna gidilir. Örneğin C, C# ve Java'da float türü 4 byte'lık gerçek sayı türünü, double türü 8 byte'lık gerçek sayı türünü temsil eder. Ve C programcıları bu nedenle en çok double türünü kullanırlar. Anahtar Notlar: İlk elektronik bilgisayarlar 40'lı yıllarda yapıldı. Bunlar da vakum tüpler kullanılıyordu. Sonra transistör icad edildi. 50'li yıllarda bilgisayarlar transistörlerle yapılmaya başlandı. 70'li yıllarda entegre devre (integrated circuit) teknolojisi geliştirilince artık bilgisayarların işlem birimi entegre devre olarak yapılmaya başlandı. Bunlara mikroişlemci dediler. Kişisel bilgisayarlar bunlardan sonra gelişti. Yuvarlama hataları her noktalı sayıda değil bazı sayılarda ortaya çıkar. Sayı ilk kez depolanırken de oluşabilir. İşlem sonucunda da oluşabilir. Bu nedenle programlama dillerinde gerçek sayıların “eşit mi”, “eşit değil mi” biçiminde karşılaştırılması iyi bir teknik değildir. Yazıların 2'lik Sistemde İfade Edilmesi Yazılar karakterlerden oluşur. Yazının her karakteri bir byte kodlanabilir. Böylece aslıunda yazı 2'lik sistemde sayılarla ifade edilmiş olur. İşte hangi karakterin hangi sayıyla ifade edileceğini belirlemek için ASCII (American Standard Code Information Interchange) tablosu denilen bir tablo yaygın bir biçimde kullanılmaktadır. ASCII tablosu orijinal olarak 7 bitti. Sonra bu 8'bite çıkartıldı. ASCII tablosunun uzunluğu 256 satırdır. Bu 256 satır ile tüm karakterler ifade edilemez. (Japonlar ne yapsın?). ASCII tablosunun ilk 128 karakteri standarttır. Diğer 128 karakteri ülkelere göre değişmektedir. Böylece ASCII tablosunun çeşitli varyasyonları oluşmuştur. Bunlara code page denilmektedir. Ancak son 15 yıldır artık karakterlerin 2 byte ile kodlanması yaygınlık kazanmaya başlamıştır. UNICODE denilen tablo 2 byte'lık ulaslarası kabul görmüş en önemli tablodur. Ve bugün artık en yoğun kullanılan tablo haline gelmiştir. 16'lık Sayı Sistemi (Hexadecimal system) 16'lık sayı sisteminde toplam 16 sembol vardır: 0123456789ABCDEF 16'lık sistem bilgisayar bilimlerinde 2'lik sistemin basit ve yoğun bir gösterimini sağlamak için kullanılır. 16'lık sistemdeki her hex digit 4 bit ile ifade edilebilir: 8 0 1 2 3 4 5 6 7 8 9 A B C D E F 0000 0001 0010 0011 0100 0101 0110 0111 1000 1001 1010 1011 1100 1101 1110 1111 16'lık sistemdeki bir sayıyı 2'lik sisteme dönüştyürmek için her hex digit için 4 bit yazılır. Örneğin: C4A8 = 1100 0100 1010 1000 Tam ters işlem de yapılabilir. Örneğin: 1001 0101 1100 0010 = 95C2 8'lik Sayı Sistemi (Octal System) Sekizlik sisemde her bir octal digit 3 bitle açılır. Örneğin: 0 1 2 3 4 5 6 7 000 001 010 011 100 101 110 111 Örneğin 8'lik sistemdeki 756 sayısı 111 101 110 biçiminde 2'lik sisteme dönüştürülebilir. Bilgisayarın Basit Mimarisi Bir bilgisayar sisteminde üç önemli birim vardır: CPU, RAM ve Disk. İşlemleri yapan elektronik devrelerin bulunduğu bölüme CPU denir. Bugün CPU'lar entegre devre biçiminde üretilmektedir ve onlara mikroişlemci de denilmektedir. CPU ile elektriksel olarak bağlantılı olan ve CPU2nun çalışması sırasında sürekli kullanılan belleklere Ana Bellek (Main Memory) ya da Birincil Bellek (Primary Memory) denilmektedir. Bunlar RAM teknolojisiyle üretildiklerinden bunlara RAM de denir. Bir programın çalışması için ana belleğe yüklenmesi gerekir. Bilgisayarın güç kaynağı kesildiğinde ana bellekte bilgiler kaybolur. Bu durumda güç kaynağı kesilince bilgileri tutan birime ihtiyaç duyulmuştur. İşte onlara İkincil Bellekler (Secondary Storage Devices)" denilmektedir. Bugün kullandığımız diskler, flash EPROM'lar, EEPROM bellekler ves. hep ikincil belleklerdir. 9 Bir program diskte bulunur. Onu çalıştırmak istediğimizde işletim sistemi onu diskten alır, birincil belleğe yükler ve orada çalıştırır. Örneğin : a = b + c; gibi bir ifadede a, b ve c RAM'dedir. CPU önce a ve b'yi RAM'den çeker. Toplama işlemini yapar, sonucu yeniden RAM'deki c'ye yazar. Dil Nedir? Dil olgusunun tek bir cümleyle tanımını yapmak kolay değildir. Fakat “iletişimde kullanılan semboller kümesi” biçiminde tanımlanabilir. Diller kabaca doğal diller ve yapay diller (ya da kurgusal diller) olmak üzere ikiye ayrılır. Doğal dillerde sentaks kesin ve açık olarak ifade edilemez. İstisnalar vardır ve bunlar kurala dönüştürülememktedir. Yapay dilelr ise insanlar tarafından tasarlanmış kurgusal dillerdir. Programlama dilleri yapay dillerdir. Bir dilin bütün kurallarına gramer denir. Gramerin iki önemli alt alanı vardır: Sentaks ve semantik. Doğal dillerin fonetik gibi, morfoloji gibi başka kurallar kümesi olsa da sentaks ve semantik bütün dillerde olan en önemli olgudur. Bir olguya dil diyebilmek için kesinlikle sentaks ve semantik kuralların bulunuyor olması gerekir. Bir dil aslında yalın elemanlardan oluşur. (örneğin doğal dillerdeki sözcükler). Buelemanlar gelişigüzel dizilemez. İşte sentaks dildeki öğelerin doğru yazılmasına ve doğru dizilmesine ilişkin kurallardır. Örneğin aşağıda bir sentaks hatası yapılmıştır: i am school going to Aşağıda yine bir sentaks hatası yapılmıştır: if a == 2)( Doğru yazılmış öğelerin ne anlam ifade ettiğine ilişkin kurallara semantik denilmektedir. Yani örneğin “I am going to school” sentaks olark düzgün bir sünledir. Fakat ne denilmek istenmektedir. Bu semantiktir. Benzer biçimde: if (a == 10) printf(“evet\n”); Sentaks olarak geçerlidir. Semantik olarak “a 10'a eşitse ekrana evet yazdır” anlamına gelir. Bilgisayar Dilleri (Computer Languages) ve Programalam Dilleri (Programming Languages) Bilgisayar bilimlerinde kullanılan, insanlar tarafından tasarlanmış ve grameri matematiksel olarak ifade edilebilen dillere bilgisayar dilleri denir. Bilgisayar bilimlerinde kullanılan ve sentaks ve semantik yapıya sahip her olgu bir bilgisayar dilidir. (html, xml vs.). Bir bilgisayar dili özellikle akış öğesi de içerisiyorsa bir programlama dilidir. Her programlama dili bilgisayar dilidir. Fakat tersi her zaman doğru değildir. (C, C++, Java, C# vs)” 10 C dili bir programlama dilidir. Bir C Programını Oluşturmak Bir C programının çalıştırılabilir program haline getirilmesi için genel olarak aşağıdaki adımlar uygulanır: 1. Kaynak dosyanın oluşturulması: Kaynak dosya metin düzenleyici bir programla yazılır. C dilinin kurallarına göre yazılan dosyanın uzantısı geleneksel olarak “.c” dir. 2. Kaynak dosyanın derleyici program (compiler) tarafından derlenmesi: Derleyici program kullanılarak .c uzantılı dosya derlenir. Derleme işlemi yazılan programın C dilinin kurallarına uygunluğunun belirlenmesi işlemidir. Derleme işlemi her zaman başarılı olmayabilir. Derleme işlemi başarılıysa derleyici program ismine amaç dosya (object file) denilen bir dosya üretir. Amaç dosyalar işletim sitemine göre değişiklik gösterebilir. Örneğin Windows sistemleri için amaç dosya uzantısı “.obj” dur. Unix/Linux sitemleri için amaç dosyanın uzantısı genel olarak “.o” dur. Derleyiciler komut satırından kolayca çalıştırılabilen programlardır. Başka programlar tarafından çağrılabilmeleri için basit bir kullanıma sahiptirler. 3. Amaç dosya(lar) bağlayıcı (linker) program tarafından birleştirilerek çalıştırılabilir (executable) dosya oluşturulur. Anahtar Notlar: Windows sistemlerinde çalıştırılabilir dosyalar “.exe” uzantılıdır. Unix/Linux sistemlerinde uzantınıın önemi yoktur. Özel bir biti set edilmiş dosyalar Unix/Linux sistemleri için çalıştırılabilir dosya olarak algılanır. Windows sistemlerinde derleme ve bağlama işlemi Windows sistemlerinde genel olarak kullanılan Microsoft firmasınının cl isimli derleyicisidir. Bu program bağlama işlemini de yapabilmektedir. Anahtar Notlar: Windows sistemlerinde komut yorumlayıcı program üzerinde “cd” komutu ile dizin geçişleri yapılabilir. (cd c:\Users\csd\Desktop\Test) cl derleyici programının hem derleme hem de bağlama işlemini yapabildiği basit bir kullanımı şu şekildedir: cl hello.c Üretilen hello.exe programı aşağıdaki gibi çalıştırılabilir. hello Unix/Linux Sistemlerinde Derleme ve Bağlama İşlemi Unix/linux sistemlerinde derleyici program olarak çoğunlukla “gcc” kullanılmaktadır. Unix/Linux sistemleri için bu program parasız olarak kurulabilmektedir. Anahtar Notlar: Bir çok Linux dağıtımı bulunmaktadır. Bunlardan lisanssız olarak kullanılabilen çeşitli dağıtımlar da mevcuttur. (Ubuntu, Mint, Debian, Open Suse vs). Bu sistemler sanal makine programlarına da kurulabilir. (Virtualbox vs.) 11 gcc programıyla derlemeve bağlama işlemi basit olarak aşağıdaki biçimde yapılabilir. gcc -o hello hello.c Burada “hello” isimli çalıştırılabilir bir dosya üretilecektir. Bu sistemlerde programın çalıştırılması terminal penceresinde aşağıdaki gibi yapılabilir. ./hello Visual Studio IDE'sinde Derleme ve Çalıştırma İşlemleri Visual Studio IDE'si ile bir programı derleyip çalıştırmak için sırasıyla şunların yapılması gerekir: 1) Öncelikle bir proje oluşturmak gerekir. Projeler tek başlarına değil “Solution” denilen kapların içerisinde bulunur. Dolayısıyla bir proje yaratırken aynı zamanda bir solution da yaratılır. Proje yaratmak için File/New/Project seçilir. Karşımıza “New Project” dialog penceresi çıkar. Burada template olarak “Visual C++” “Win32 Console Application” seçilir. Sonra Proje dizininin yaratılacağı dizin ve Proje ismi belirlenir. “Create Directory for Solution” seçenk kutusu default çarpılanmış durumda olabilir. Bunun çarpısı kaldırılabilir. Bu durumda solution bilgileri ile proje bilgileri aynı klasörde tutulacaktır. Bu dialog penceresi kapatılınca karşımıza yeni bir dialog penceresi gelir. Burada “Empty Project” seçilmelidir. SDL seçenk kutusu da unchecked yapılabilir. 2) Solution yaratıldıktan sonra bununla ilgili işlem yapabilmek için “Solution Expolorer” denilen bir pencere kullanılır. Solution Explorer View menüsü yoluyla,, araç çubuğı simgesiyle ya da Ctrl + Alt + L kısayol tuşuyla açılabilir. Solution Explorer “yuvalanabilir (dockable)” bir penceredir. Solution Explorer bir “treeview” kontrolü içermektedir. 3) Artık projeye bir kaynak dosyanın eklenmesi zamanı gelmiştir. Bunun için “Project/Add New Item” menüsü ya da Solution Explorer'da projenin üzerinde bağlam menüsünden “Add/New Item” seçilir. Karşımıza “Add New Item” dialog penceresi gelir. Burada kaynak dosyaya isim vererek onu yaratırız. Dosya uzantısının .c olması gerekmektedir. Artık programı dosyaya yazabiliriz. 4) Programı derlemek için “Build/Compile” seçilir. Link işlemi için “Build/Build Solution” ya da “Build/Build XXX” seçilir (Buradaki XXX projenin ismidir). Programı çalıştırmak için “Debug/Start Without Debugging” ya da Ctrl+F5 tuşlarına basılır. Sonraki bir aşama seçilirse zaten öncekiler de yapılır. Böylece programı derleyip, bağlaıp çalıştırabilmek için tek yapılacak şey Ctrl+F5 tuşlarına basmaktır. 5) Projeyi tekrar açabilmek için “File/Open/Project-Solution” seçilir. Projenin dizinine gelinir ve .sln dosyası seçilir. 6) IDE'den çıkmadan projeyi kapatmak için “File/Close Solution” seçilir. Atom (Token) Kavramı Bir programlama dilindeki anlamlı en küçük birime atom (token) denir. Program aslında atomların yan yana gelmesiyle oluşturulur. (Atomlar doğal dillerdeki sözcüklere benzetilebilirler). Örneğin “merhaba dünya” programını atomlarına ayıralım: #include int main(void) { printf(“Merhaba Dunya\n”); 12 return 0; } Atomlar: # include < ) { printf return 0 ( ; stdio.h > int “Merhaba Dunya” ) } main ; ( void Atomlar daha fazla parçaya ayrılamazlar. Atomları altı gruba ayırabiliriz: 1) Anahtar Sözcükler (Keywords/Reserved Words): Dil için özel anlamı olan, değişken olarak kullanılması yasaklanmış sözcüklerdir. Örneğin if, for, int, return gibi... 2) Değişkenler (Identifiers/Variables): İsmini bizim istediğimiz gibi verebildiğimiz atomlardır. Örneğin: x, y, count, printf gibi... 3) Sabitler (Literals/Constants): Bir değer ya b,ir değişkenin içerisinde bulunur ya da doğrudan yazılır. İşte doğrudan yazılan sayılara sabit denir. Örneğin: a = b + 10; ifadesinde a ve b birer değişken atom, 10 ise sabit atomdur. 4) Operatörler (Operators): Bir işleme yol açan, işlem sonucunda bir değer üretilmesini sağlayan atomlara operatör denir. Örneğin: a = b + 10; Burada + ve = bierer operatördür. 5) String'ler (Strings): Programlama dillerinde iki tırnak içerisine yazılmış yazılar iki tırnaklarıyla tek bir atom belirtirler. Bunlara string denir. Örneğin: “Merhaba Dunya\n” gibi... 6) Ayıraçlar (Delimeters/Punctuators): Yukarıdakşi grupların dışında kalan, ifadeleri ayırmak için kullanılan tüm atomlar ayıraç atomdur. Örneğin ; gibi, } gibi... Sentaks Gösterimleri Programlama dillerinin sentakslarını betimlemek için en çok kullanılan yöntem BNF notasyonu ve türevleridir. Ancak biz kurusumuzda açısal parantez, köşeli parantez tekniğini kullanacağız. Açısal parantezler içerisinde yazılanlar mutlaka bulunması zorunlu öğeleri, köşeli parantez içerisindekiler “yazılmasa da olur (optional)” öğeleri belirtir. Örneğin: [geri dönüş değerinin türü] ([parametre bildirimi]) { /* ... */ } Açısal ve köşeli parantezlerin dışındaki tüm atomlar aynı biçimde bulundurulmak zorundadır. Merhaba Dunya Programının Açıklaması 13 C programlarının başında genellikle aşağıdaki satır bulunur: #include Bu bir önişlemci (preprocessor) komutudur. Bu komut açısal parantez içerisindeki dosyanın içeriğininn derleme sırasında komutun yerleştirildiği yere kopyalanacağını beliritr. (Yani bu satırı silip onun yerine stdio.h isimli dosyanın içeriğini oraya yerleştirmek aynı etkiye yol açar). Neden bu dosyanın içeriğinin bulunmasına gereksinim duyulduğu ileride açıklanacaktır. C programları kabaca fonksiyonlardan (functions) oluşur. Altprogramlara C'de fonksiyon denilmektedir. Bir fonksiyonun tanımlanması (definition) o fonksiyonun bizim tarafımızdan yazılması anlamına gelir. Çağrılması (call) onun çalıştırılması anlamına gelir. “Merhaba Dunya” programında main fonksiyonu tanımlanmıştır. Fonksiyon tanımlamanın genel biçimi şöyledir: [geri dönüş değerinin türü] ([parametre bildirimi]) { /* ... */ } Anahtar Notlar: Genel biçimlerdeki /* ... */ gösterimi “burada birşeyler var, fakat biz şimdilik onlarla ilgilenmiyoruz” anlamına gelmektedir. İki küme parantezi arasındaki bölgeye blok denilmektedir. C'de her fonksiyonun bir ana bloğu vardır. C programları main isminde bir fonksiyondan çalışmaya başlar. main bitince program da biter. Örnek programımızda main fonksiyonunda printf isimli bir fonksiyon çağrılmıştır. Printf standart bir C fonksiyonudur. Standart C fonksiyonu demek, tüm C derleyicilerinde bulunmak zorunda olan, derleyicileri yazanlar tarafından zaten yazılmış olarak bulunan fonksiyonlar demektir. Bir fonksiyon çağırmanın genel biçimi şöyledir: ([argüman listesi]); printf fonksiyonu iki tırnak içerisindeki yazıları imlecin (cursor) bulundauğu yerden itibaren ekrana yazar. İmleç program çalıştığında sol üst köşededir. printf imleci yazının sonunda bırakır. “\n” “imleci aşağıdaki satırın başına geçir” anlamına gelir. return deyimi fonksiyonu sonlandırır. Eğer return yoksa fonksiyon kapanış küme parantezine geldiğinde sonlanır. Bir C programında istenildiği kadar çok fonksiyon tanımlanabilir. Bunların sırasının bir önemi yoktur. Main fonksiyonunun da özel bir yerde bulunması gerekmez. Fakat program her zaman main fonksiyonundan çalışmaya başlar. C'de iç içe fonksiyon tanımlanamaz. Her fonksiyon diğerinin dışında tanımlanmak zorundadır. Örneğin: #include foo() { printf("I am foo\n"); } main() { foo(); } 14 Anahtar Notlar: Örneklerimizde fonksiyon isimleri uydurulurken foo, bar, tar gibi, func gibi isimler kullanılacaktır. Bu isimlerin hiçbir özel anlamı yoktur. İfade (Expression) Kavramı Değişkenlerin, sabitlerin ve operatörlerin herbir birleşimine (kombinasyonuna) ifade denir. Örneğin: x 30 x + 30 x + 30 – y foo() birer ifadedir. Görüldüğü gibi tekbaşına bir değişken ve sabit ifade belirtir, fakat tek başına bir operatör ifade belirtmez. Nesne (Object) Kavramı Bellekte yer kaplayan ve erişilebilir olan bölgeler nesne denir. Örneğin programlama dillerindeki değişkenler tipik birer nesnedir. Bir ifade ya bir nesne belirtir ya da nesne belirtmez. Örneğin: 100 bir ifadedir fakat nesne belirtmez. Örneğin: x bir ifadedir ve nesne belirtir. Örneğin: x + 10 bir ifadedir, nesne belirtmez. Sol Taraf Değeri (Left Value) ve Sağ Taraf Değeri (Right Value) Nesne belirten ifadelere sol taraf değeri (lvalue), belirtmeyene ifadeleri sağ taraf değeri (rvalue) denilmektedir. Örneğin: 10 --> sağ taraf değeri x --> sol taraf değeri x + 10 --> sağ taraf değeri Sol taraf değeri denmesinin nedeni atama operatörünün solunda bulunabilmesindendir. Sağ taraf değeri denmesinin sebebi atama operatörünün solunda bulunamamasındandır (tipik olarak sağında bulunduğu için). C'nin Veri Türleri Tür (type) bir nesnenin bellekte kaç byte yer kaplayacağını ve onun içerisindeki 0'ların ve 1'lerin nasıl yorumlanacağını anlatan temel bir kavramdır. Her nesnin C'de bir türü vardır. Ve bu tür programın çalışması sırasında değişmez. (Aslında yalnızca nesnelerin değil genel olarak her ifadenin bir türü vardır. Bundan ileride bahsedilecektir.) C'nin temel türleri aşağıdaki tabloda gösteriöektedir: Tür Belirten Uzunluk (byte) Sınır Değerler (Windows) Anahtar Sözcükler Windows (UNIX/Linux) [signed] int 4 (4) [-2147483648, +2147483647] 15 unsigned [int] 4 (4) [0, +4294967295] [signed] short [int] 2 (2) [-32768, +32767] unsigned short [int] 2 (2) [0, +65535] [signed] long [int] 4 (8) [-2147483648, +2147483647] unsigned long [int] 4 (8) [0, +4294967295] char 1 (1) [-128, +127] [0, +255] signed char 1 (1) [-128, +127] unsigned char 1 (1) [0, +255] float 4 (4) [±3.6*10-38, ±3.6*10+38] double 8 (8) [±1.8*10-308, ±1.8*10+308] long double 8 (8) [±1.8*10-308, ±1.8*10+308] [signed] long long 8 (8) [-9223372036854775808, +9223372036854775807] unsigned long long (C99 ve C11 ve C++11) 8 (8) [0, +18446744073709551615] _Bool (C99, C11) 1 (1) true, false - int türü işaretli bir tamsayı türüdür. int türünün uzunluğu sistemden sisteme değişebilir. Standartlarda en az 2 byte olması zorunlu tutulmuştur. Ancak derleyicileri yazanların isteğine bırakılmıştır. - C'de her tamsayı türünün işaretli ve işaretsiz versiyonları vardır. int türünün işaretsiz biçimi unsigned int türüdür. Yalnızca unsigned demekle unsigned int demek aynı anlamdadır. - Standartlara göre short türü ya int türü kadardır ya da int türünden küçüktür. Örneğin DOS'ta int türü de short türü de 2 byte uzunluktadır. Oysa Windows'ta ve UNIX/Linux sistemlerinde int türü 4 byte, short türü 2 byte uzunluktadır. - short türünün işaretsiz biçimi unsigned short türüdür. - long türü standartlara göre ya int türü kadar olmak zorundadır ya da ondan büyük olmak zorundadır. 32 ve 64 bit Windows sistemlerinde long türü int türü aynı uzunluktadır (4 byte). 32 bit UNIX/Linux sistemlerinde long türü 4 byte, 64 bit UNIX/Linux sistemlerinde 8 byte'tır. - long türünün işaretsiz biçimi unsigned long türüdür. - char türü standartlara göre her sistemde 1 byte olmak zorundadır. (Bu türün ismi belki byte olsaydı daha iyiydi). char ismi her ne kadar karakteri çağrıştırıyorsa da bunun karkterle bir ilgisi yoktur. char türü C'de 1 byte uzunlukta bir tamsayı türüdür. C'de yalnızca char denildiğinde bunun sisgned char mı, yoksa unsignd char mı olacağı derleyicileri yazanların isteğime bırakılmıştır. Örneğin Windows ve UNIX/Linux sistemlerindeki derleyicilerde char denildiğinde signed char anlaşılmaktadır (fakat bu durum ayarlardan da değiştirilebilmektdir). - C'de gerçek sayı türlerinin işaretli ve işaretsiz biçimleri yoktur. Onlar zaten her zaman default işaretlidir. - float türü en az 4 byte olması öngörülen bir gerçek sayı (noktalı sayı) türüdür. float türünün yuvarlama hatalarına direnci zayıftır. Bu nedenle float yerine programcılar daha çok double türünü tercih ederler. 16 - double türü standartlara göre en az float kadar olmak zorundadır. Yani float türüyle aynı duyarlıkta olabilir ya da ondan daha geniş olabilir. - long double türü en az double kadar olmak zorundadır. double ile aynı uzunlukta olabilir ya da ondan daha uzun olabilir. - Birden fazla sözcükten oluşan türler için bu sözcüklerin yerleri değiştirilebilir (örneğin signed long int yerine int signed long denilebilir.) - C'de en fazla kullanılan tamsayı türü int, en fazla kullanılan gerçek sayı türü double türüdür. Programcının öncelikle bunları tercih etmelidir. Özel durum varsa diğerlerini düşünmelidir. - C99'da long long isimli bir tamsayı türü daha eklendi. Bu türün long türünden daha uzun olması öngörülmüştür. Standartlara göre bu tür ya long türüyle aynı uzunluktadır ya da ondan daha uzundur. - C'ye C99'la birlikte nihayet bir bool türü de eklenmiştir. Fakat klasik C'de böyle bir tür yoktur. Derleyicilerin Hata Mesajları Derleyicilerin hata mesajları üçe ayrılmaktadır: 1. Uyarılar (Warnings): Uyarılar gerçek hatalar değildir. Program içerisindeki program yapmış olabileceği olası mantık hatalarına dikkati çekmek için verilirler. Uyarılar derleme işleminin başarısızlığına yol açmazlar. Ancak programcıların uyarılara çok dikkat etmesi gerekir. Çünkü pek çok uyarıda derleyici haklı bir yere dikkat çekmektedir. 2. Gerçek Hatalar (Errors): Bunlar dilin sentaks ve semantik kurallarına uyulmaması yüzünden verilirler. Bunların mutlaka düzeltilmesi gerekir. Bir programda bir tane bile “error” olsa program başarılı olarak derlenemez. 3. Ölümcül Hatalar (Fatal Errors): Dereleme işleminin bile devam etmesini engelleyen ciddi hatalardır. Normal olarak bir programda ne kadar hata olursa olsun tüm kod gözden geçirilir. Tüm hatalar en sonında listelenir. Fakat bir ölümcül hata oluştuğunda artık derleme işlemi sonlandırılır. Ölümcül hatalar genellikle sistemdeki ciddi sorunlar yüzünden ortaya çıkmaktadır (örneğin diskte yeterli alan olmayabilir, ya da sistemde yeterli RAM bulunmuyor olabilir.) Verilen hata mesajlarının metinleri derleyiciden derleyiciye değişebilir. Ayrıca bir hata durumunda bir derleyici buna birmesaj verirken diğeri daha fazla mesaj verebilir. C Programlarının Geçerliliği Standartlar bir C programının geçerliliği ve derlenip derlenmeyeceği konusunda şunları söylemektedir: 1. Standartlarda belirtilen sentaks ve semantik kurallara uygun programlar kesinlikle derlenmek zorundadır. 2. Standartlara uygun bir program başarılı olarak derlendikten sonra birtakım mesajlar (diagnostics) da verilebilir. (Tabi bu durumda bu mesajlar uyarı mesajları olacaktır) 3. Sentaks ve semantik kurllara uyulmadığı her durumda derleyici bu durumu belirten bir mesaj (diagnostic) vermek zorundadır. Bu mesaj error ya da warning olabilir. 4. Geçersiz bir program yine de başarılı olarak derlenebilir. Ya da tersten şöyle düşünebiliriz: “Bir programın başarılı olarak derlenmesi onun geçerli olduğu anlamına gelmez”. Özellikle 4. Madde C programcıları tarafından maalesef bilinmemektedir. Bazı programcılar sırf derleyici programılarını derledi diye programlarının geçerli olduğunu sanmaktadırler. Böyle bir program başka C derleyicileri tarafından derlenmeyebilir. 17 Taşınabilirlik (Portability) Nedir? Taşınabilirlik bir programlama dilinde yazılmış olan programın başka bir sisteme götürüldüğünde orada derlenerek sorunsuz çalışabilmesine denilmektedir. Taşınabilirlik bu anlamda bir standadizasyonun bulunmasını gerektirir. Çünkü derleyicileri yazanlar hep aynı kuralları kabul etmişlerse taşınabilirlik oluşabilir. C Programlama dili oldukça taşınabilir bir dildir. Burada söz konusu edilen taşınabilirlik kaynak kodun taşınabilirliğidir. Yoksa biz derlenip exe yapılmış bir programı başka bir sisteme götürüp orada çalıştıramayız. Ancak bunun mümkün olduğu ortamlar da vardır (.NET ve Java gibi). Bu ortamlarda derlenmiş olan kod yeniden derlenmeden başka sistemlere götürüldüğünde çalıştırılabilmektedir. Bildirim ve Tanımlama Kavramları (Declaration & Definitions) C, C++, C# ve Java gibi katı tür kontrolünün uygulandığı dillerde (strongly typed languages) bir değişken kullanılmadan önce derleyiciye tanıtılmak zorundadır. Kullanılmadan önce değişkenlerin derleyiciye tanıtılması işlemine bildirim (declaration) denilmektedir. Bir bildirim yapıldığında eğer derleyici bildirilen değişken için bellekte bir yer ayırıyorsa o bildirim aynı zamanda bir tanımlama (definition) işlemidir. Yani tanımlama derleyicinin yer ayırdığı bildirim işlemleridir. Bildirim daha geneldir. Her tanımlama bir bildirimdir fakat her bildirim bir tanımlama değildir. Kurusumuzda aksi belirtilmediği sürece bildirimler aynı zamanda tanımlama işlemi olarak kubul edilmelidir. Bildirim işleminin genel biçimi şöyledir: ; Örneğin: int a; long b, c, d; double x, y; Değişken listyesi bir'den fazla değişkenden oluşuyorsa onları ayırmak için ',' atomu kullanılır. Atomlar arasında istenildiği kadar boşluk karakteri bırakılabildiğine göre aşağıdaki bildirim d geçerlidir. long b , c, d; C'de (C90) bildirimler 3 yerde yapılabilir: 1) Blokların başlarında. Yani blok açıldığında henüz hiçbir fonksiyon çağrısı yapılmadan bildirimler yapılmalıdır. Blok başlarında bildirilen değişkenlere yerel değişkenler (local variables) de denilmektedir. 2) Tüm blokların dışında. C'de tüm blokların dışında bildirilen değişkenlere global değişkenler (global variables) de denilmektedir. 3) Fonksiyonların parametre parantezleri içerisinde. Böyle bildirilmiş olan değişkenlere “parametre değişkenleri (parameters)” denilmektedir. Değişken isimlendirmede şu kurallar söz konusudur: - C büyük harf-küçük harf duyarlılığı olan (case sensetive) bir programlama dilidir. Yani büyük harflerle küçük harfler tamamen farklı karakterlermiş gibi ele alınırlar. 18 - Değişkenler boşluk içermez. Değişken isimleri sayısal karakterlerle başlatılamaz. Ancak alfabetik karakterlerle ya da _ ile başlatılıp sayısal devam ettirilebilirler. - Değişken isimlendirmede yalnızca İngilizce büyük harfler, küçük harfler, sayısal karakterler ve alt tire karakterleri kullanılabilir. (Yani değişkenlere Türkçe isimler veremeyiz). - C standartlarına göre değişken uzunlukları için derleyiciler minimum 32 karakteri dikkate almak zorundadır. Bu limit en az 32 olmak koşuluyla derleyiciden derleyiciye değişebilir. Fakat daha uzun değişken isimleri derleyici tarafından geçerli olarak değerlendirilir. ncak en az ilk 32 karakter ayırıcı olarak dikkate alınacaktır. - Anahtar sözcükler de değişken ismi olarak kullanılamazlar Değişkenlere İlkdeğer Verilmesi (Initialization) Bildirim sırasında bildirim işleminin bir parçası olarak değişkenlere değer vermeye ilkdeğer verme (initialization) denilmektedir. Örneğin: int a, b = 10, c; burada a ve c'ye ilkdeğer verilmemiştir, fakat b'ye verilmiştir. İlkdeğer vermekle ilk kez değer vermek çoğu zaman aynı etkiye yol açsa da gramatik olarak aynı şey değildir. Örneğin: int a = 0; int b; b = 10; /* ilkdeğer verme işlemi */ /* ilkdeğer verme işlemi değil, ilk kez değer verme işlemi */ İçerisine heniz değer atanmamış yerel değişkenlrin içerisinde rastgele değerler vardır. C terminolojisinde buna çöp değer (garbage value) denilmektedir. Halbuki ilkdeğer verilmemiş global değişkenlerin içerisinde kesinlikle sıfır değeri bulunur. Nesnelerin İçerisindeki Değerlerin printf Fonksiyonuyla Yazdırılması printf fonksiyonunda iki tırnak içerisindeki karakterler ekrana yazdırılır. Fakat iki tırnağın içerisinde % karakteri varsa, printf bu % karakterini ve yanındaki bir ya da birkaç karakteri format karakteri olarak kabul eder. printf % karakterini ve onu izleyen formak karaktrlerini ekrana yazdırmaz. Bunlar yer tutucudur. Bunların yerine iki tırnaktan sonraki ifadelrin değerleri yazdırılır. İki tırnak içerisindeki her format karakteri sırasıyla iki tırnaktan sonraki argümanlarla eşleştirilir. Formak karakterleri yerine bunların değerleri yazdırılır. Örneğin: #include main() { int a = 10, b = 20; printf("a = %d, b = %d\n", a, b); printf("a = %d, b = %d\n", b, a); printf("%d%d\n", a, b); /* a = 10, b = 20 */ /* a = 20, b = 10 */ /* 1020 */ } % karakterinin yanındaki format karakterleri nelerdir? Format karakterleri yazdırılacak ifadenin türüne bağlıdır. Ayrıca format karakterleri yazdırma işleminin nasıl yapılacağını da (örneğin kaçlık sistemde) belirler. Temel format karakterleri ve anlamları şöyledir: 19 Format Karakterleri Anlamı %d int, short ve char türlerini 10'luk sistemde yazdırır %ld long int türünü 10'luk sistemde yazdırır %x, %X int, short ve char türlerini hex sistemde yazdırır %lx, %lX long int ve unsigned long int türlerini hex sistemde yazdırır %u unsigned int, unsigned short ve unsigned char türlerini 10'luk sistemde yazdırır %lu unsigned long int türünü 10'luk sistemde yazdırır %f float ve double türlerini 10'luk sistemde yazdırır %c char, short ve int türlerini karakter görüntüsaü olarak yazdırır %o char, short ve int türlerini octal sistemde yazdırır %lo long ve unsigned long türlerini octal sistemde yazdırır printf %f formatında default olarak noktadan sonra 6 basamak yazdırır. Yazdırılacak değer noktadan sonra 6 basamaktan fazlaysa yuvarlama yapılır. eğer printf ile noktadan sonra istediğimiz kadar basamak yazdırmak istiyorsak %.nf formatı kullanılmalıdır (burada n yerine bir sayı olmalıdır. Örneğin %.10f gibi). scanf Fonksiyonuyla Klavyeden Okuma Yapılması scanf fonksiyonun kullanımı printf fonksiyonuna çok benzemektedir. Yine scanf fonksiyonunun bir format kısmı vardır. Bu format kısmını içerisine okunan değerin yerleştirileceği nesne adresleri izler. scanf'te iki tırnak içerisinde yalnızca formak karakterleri bulunmalıdır. Bu fonksiyon iki tırnak içerisindekileri ekrana yazdırmaz. scanf'te iki tırnak içerisindeki format karakterlerinin dışındaki karakterler tamamen başka anlama gelirler. scanf fonksiyonunda değerin yerleştirileceği nesnelerin başında & operatörü bulunmalıdır. (Bu & operatörü bir adres operatörüdür. Nesnenin adresini elde etmekte kullanılır. Örneğin: #include main() { int a; printf("sayi giriniz:"); scanf("%d", &a); printf("Girilen deger: %d\n", a); } scanf kullanırken şunlara dikkat etmek gerekir: - İki tırnak içerisinde yalnızca format karakteri bulunmalıdır. Orada boşuk karakterleri ya da \n karakteri bulunmamalıdır. - Değişkenlerin önündeki & operatörü unutulmamalıdır. Scanf fonksiyonuyla bir'den fazla değer girilirken girişler arasında istenildiği kadar boşluk karakterleri bırakılabilir. Örneğin: #include main() { int a; 20 int b; printf("Iki sayi giriniz:"); scanf("%d%d", &a, &b); printf("a = %d, b = %d\n", a, b); } printf fonksiyonundaki format karakterleri scanf fonksiyonunda klavyeden girişi belirlemektedir. Yani örneğin biz pir int değeri printf ile %x kullanarak yazdırırsak bu değer 16'lık sistemde ekrana yazdırılır. Fakat bir int değeri scanf ile %x ile okumak istersek klavyeden yaptığımız girişin 16'lık sistemde olduğu kabul edilir. Örneğin: #include main() { int a, b; printf("Bir sayi giriniz : "); scanf("%x", &a); printf("a = %d\n", a); } printf fonksiyonunda hem float hem de double türleri %f ile yazdırılır. Ancak scanf fonksiyonunda float %f ile, double %lf ile okunur. Örneğin: #include main() { float f; double d; printf("float bir deger giriniz:"); scanf("%f", &f); printf("double bir deger giriniz:"); scanf("%lf", &d); printf("f = %f, d = %f\n", f, d); } Sınıf Çalışması İsmi a, b, c olan double türden 3 değişken tanımlayınız. Sonra a ve b için klavyeden scaf fonksiyonuyla okuma yapınız. Bu ikisinin toplamını c'ye atayınız ve c'yi yazdırınız. Çözümü #include main() { double a, b, c; printf("a degerini giriniz :"); scanf("%lf", &a); printf("b degerini giriniz :"); scanf("%lf", &b); c = a + b; 21 printf("c = %f\n", c); } Fonksiyonların Geri Dönüş Değerleri (return value) Bir fonksiyon çağrıldığında akış fonksiyona gider. Fonksiyonun içerisindeki kodlar çalışır. Fonksiyonun çalışması bitince akış kalınan yerden devam eder. İşte fonksiyonun çalışması bittiğinde onu çağıran fonksiyona ilettiği değer geri dönüş değeri denilmektedir. Fonksiyonun geri dönüş değerinin de bir türü vardır. Bu tür tanımlama sırasında fonksiyonun isminin soluna yazılır. Örneğin: double foo() { /* ... */ } long bar() { /* ... */ } Fonksiyonun geri dönüş değeri yerine birşey yazılmazsa sanki int yazılmış gibi işlem görür. Yani örneğin: foo() { /* ... */ } ile int foo() { /* ... */ } tamamen aynı anlamdadır. C99 ve C11'de geri dönüş değerinin yazılmaması seçeneği kaldırılmıştır. C'nin bu yeni versiyonlarında fonksiyonların geri dönüş değerlerinin türleri yazılmak zorundadır. Örneğin: result = foo() * 2; Burada önce foo fonksiyonu çağrılır, geri dönüş değeri elde edilir, ikiyle çarpılır ve result değişkenine atanır. Fonksiyonun geri dönüş değeri return deyimiyle oluşturulur. return deyiminin genel biçimi şöyledir: return [ifade]; Programın akışı return anahtar sözcüğünü gördüğünde önce ifadenin değeri hesaplanır, sonra fonksiyon bu değerle sonlandırılır. Yani return deyiminin iki işlevi vardır: 1) Fonksiyonu sonlandırır 2) Geri dönüş değerini oluşturur 22 Örneğin: #include int foo() { printf("Foo\n"); return 100; printf("test\n"); /* unreachable code! */ } int main() { int result; result = foo() * 2; printf("%d\n", result); } Anahtar Notlar: Linux Sanal makinaya nasıl kurulur? Öncelikle sanal makina programını bilgisayarımıza yüklememiz gerekir. Bunun için iki önemli seçenek vardır. Birincisi VMWare firmasının VMWare programı, ikincisi open source VirtualBox programı. VMWare normalde paralı bir programdır. Fakat bunun VMWare Player isminde bedava bir sürümü de vardır. Sanal makinaya Linux kurulur (örneğin Mint sürümü). Kuruum doğrudan ISO dosyasıyla yapılabilir. Kurulum sırasında bizden bir user name ve password istenecktir (Örneğin derste yaptığımız kurulumda user name = csd ve password = csd1993 verdirk.) C'de bir fonksiyonun geri dönüş değeri olduğu halde return ile belli bir değere geri dönülmezse geri dönüş değeri olarak çöp bir değer elde edilir. (Halbuki C# ve Java gibi bazı dillerde fonksiyonların geri dönüş değeri varsa return kullanmak zorunludur.) C'de main fonksiyonun geri dönüş değeri int olmak zorundadır. Yani örneğin void, long vs. olamaz. Ayrıca main fonksiyonunda hiç return kullanmazsak sanki 0 ile geri dönmüş gibi işlem görür. Yani main fonksiyonunda return kullanmamakla ana bloğun sonunda 0 ile geri dönmek aynı anlamdadır. Örneğin: #include int main() { printf("I am main\n"); return 0; /* bu olmasaydı da aynı anlam oluşacaktı */ } Fonksiyonun geri dönüş değeri yerine void anahtar sözcüğü yazılırsa bu durum fonksiyonun geri dönüş değerinin olmadığı anlamına gelir. Böyle fonksiyonlara void fonksiyonlar denir. void fonksiyonlarda return kullanılabilir fakat yanına ifade yazılamaz. void fonksiyonlarda return kullanılmamışsa fonksiyon ana blok sonlanınca sonlanır. Örneğin: #include void foo() { printf("foo\n"); } int main() { 23 printf("I am main\n"); foo(); return 0; } Fonksiyonun geri dönüş değeri return işlemi sırasında önce geçici bir değişkene aktaraılır, oradan alınarak kullanılır. Örneğin: return 100; ----> temp = 100; ... result = foo() * 2---> result = temp * 2; Aslında return işlemi geçici değişkene yapılan atama işlemidir. Fonksiyonun geri dönüş değerinin türü de geçici değişkenin türüdür. Geçici değişken derleyici tarafından return işlemi sırasında yaratılır, kullanım bittiğinde yok edilir. return işlemi de bir çeşit atama işlemidir. Değişkenlerin Faaliyet Alanları (Scope) Bir değişkenin derleyici tarafından tanınabildiği program aralığına faaliyet alanı (scope) denilmektedir. Değişkenler belirli faliyet alanlarına sahiptir. C'de 3 çeşit faaliyet alanı vardır: 1) Blok faaliyet alanı (block scope) 2) Fonksiyon faaliyet alanı (function scope) 3) Dosya faaliyet alanı (file scope) Bir fonksiyonun bir ana bloğu olmak zorundadır. O ana bloğun içerisinde istenildiği kadar çok iç içe ya da ayrık blok açılabilir. Örneğin: void foo() { { ... { ... } } { ... } } C'de (C++, C# ve Java'da da öyle) iç içe fonksiyon bildirilemez. Örneğin: void foo() { ... void bar() { ... } ... /* geçersiz */ 24 } Blok faaliyet alanı yalnızca bir blokta ve o bloğun kapsadığı bloklarda tanınma alanıdır. Fonksiyon faaliyet faaliyet alanı tek bir fonksiyonun her yerinde tanınma aralığıdır. Dosya faaliyet alanı bir dosyanın her yerinde tanınma aralığıdır. Yerel Değişkenlerin Faaliyet Alanları Yerel değişkenler blok faaliyet alanı kuralına uyarlar. Yerel değişken hangi blokta bildirilmişs yalnızca o blokta ve bloğun kapsadığı bloklarda kullanılabilir. Örneğin: void foo() { int a; { int b; /* a ve b burada kullanılabilir */ } /* a burada kullanılabilir, b kullanılamaz */ } Örneğin: #include int main() { int a; { int b; b = 20; a = 10; printf("a = %d, b = %d\n", a, b); } printf("a = %d\n", a); printf("b = %d\n", b); /* geçerli */ /* geçerli */ /* error! */ return 0; } C'de aynı faaliyet alanına sahip birden fazla aynı isimli değişken tanımlanamaz. Farklı faaliyet alanlarına sahip aynı isimli değişkenler tanımlanabilir. Bu durumda örneğin, iç içe yerel bloklarda aynı isimli değişkenler tanımlanabilir. Örneğin: #include void foo() { int a; { int a; /* ... */ } /* ... */ } /* geçerli */ 25 int main() { int a; /* geçerli */ return 0; } C'de bir blokta bir'den fazla değişken faaliyet gösteriyorsa o blokta o değişken ismi kullanıldığında dar faaliyet alanına sahip olan değişkene erişilir. Örneğin: #include int main() { int a; a = 10; { int a; a = 20; printf("%d\n", a);/* 20 */ } printf("%d\n", a); /* 10 */ return 0; } Global Değişkenlerin Faaliyet Alanı Bildirimleri fonksiyonların dışında yapılan değişkenlere global değişkenler denir. Global değişkenler dosya faaliyet alanına (file scope) sahiptir. Yani tüm fonksiyonlarda tanınırlar. #include int a; void foo() { a = 10; } int main() { a = 20; printf("%d\n", a); foo(); printf("%d\n", a); /* 20 */ /* 10 */ return 0; } Bir global değişkenle aynı isimli yerel değişkenler tanımlanabilir. Çünkü bunlar farklı faaliyet alanlarına sahiptir. Tabi ilgili blokta bu değişken ismi kullanıldığında dar faaliyet alanaına sahip olana (yani yerel olana) erişilir. 26 Örneğin: #include int a; void foo() { int a; a = 10; /* yerel olan a */ } int main() { a = 20; /* global olan a */ printf("%d\n", a); /* 20 */ foo(); printf("%d\n", a); /* 20 */ return 0; } C'de derleme işleminin bir yönü vardır. Bu yön yukarıdan aşağıya doğrudur. Derleyicinin önce değişkenin bildirimini görmesi gerekir. Bu nedenle bir global değişkeni aşağıda bildirip daha yukarıda kullanamayız. Örneğin: #include void foo() { a = 10; } /* geçersiz! */ int a; int main() { a = 10; /* geçerli */ printf("%d\n", a); /* geçerli */ return 0; } Bu durumda global değişkenler için en iyi bildirim yeri programın tepesidir. Fonksiyonların Parametre Değişkenleri Fonksiyonlar onları çağıran fonksiyonlardan değerler alabilirler. Bunlara fonksiyonların parametre değişkenleri (parameters) denilmektedir. Parametre değişkenlerini bildirmenin C'de iki yolu vardır: Eski biçim (old style) ve yeni biçim (modern style). Biz kursumuzda hiç eski biçimi kullanmayacağız. Bu eski biçim çok eskiden kullanılıyordu. Yeni biçim ortaya çıkınca programcılar bunu kullanmaz oldular. C standartları oluşturulduğunda her iki biçimi de destekledi. Eski biçim C++'ta C99'da ve C11'de desteklenmemektedir. Eski biçimde parametre bildirimi yapılırken önce parametre parantezlerinin içerisine parametre değişkenlerinin isimleri aralarına virgül atomu getirilerek listelenir. Daha sonra henüz ana blok açılmadan bunların bildirimleri yapılır. Örneğin: void foo(a, b, c) int a; 27 long b, c; { ... } Yeni biçimde parametre değişkenleri parametre parantezinin içerisinde tür belirtilerek bildirilir. Örneğin: void foo(int a, long b, long c) { ... } Yeni biçimde parametre değişkenleri aynı türden olsa bile her defasında tür belirten atomları yeniden belirtmek gerekir. Örneğin: void foo(int a, b) { /* ... */ } /* geçersiz! */ bildirimin şöyle yapılması gerekirdi: void foo(int a, int b) { /* ... */ } /* geçerli */ Parametreli bir fonksiyon çağrılırken parametre sayısı kadar argüman kullanılır. Örneğin: foo(10, 20); Argümanlar ',' operatörü ile birbirlerinden ayrılmaktadır. Argüman olarak herhangi birer ifade (expression) kullanılabilir. Örneğin: foo(a + b - 10, c + d + 20); Anahtar Notlar: Bir fonksiyon çağrılırken parametre parantezinin içerisine yazılan ifadelere argüman (argument), fonksiyonun parametre değişkenlerine kısaca parametre (parameter) 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ıklı bir atama yapılır. Sonra programın akışı fonkisyona geçer. Örneğin: #include void foo(int a, int b) { printf("a = %d, b = %d\n", a, b); } int main() { int x = 10, y = 20; foo(x, y); foo(x + 10, y + 20); foo(100, 200); return 0; 28 } Fonskiyonların parametre değişkenleri faaliyet alanı bakımından sanki ana bloğun başınd tanımlanmış değişkenler gibi işlem görür. Yani fonksiyonların parameyre değişkenleri o fonksiyonda tanınabilmektedir. Aynı faaliyet alanına sahip aynı isimli değişken tanımlanamayacağına göre aşağıdaki bildirim de geçerli değildir: void foo(int a, int b) { long a; /* geçerli değil! */ ... } Fakat: void foo(int a, int b) { ... { long a; // geçerli */ ... } ... } Bir fonksiyon parametreleriyle aldığı değere işlemler uygulayıp sonucu geri dönüş değeri olarak verebilir. Örneğin: #include int add(int a, int b) { return a + b; } int mul(int a, int b) { return a * b; } int main() { int result; result = add(10, 20); printf("%d\n", result); result = mul(10, 20); printf("%d\n", result); return 0; } Bazı Matematiksel Standart C Fonksiyonları C'de temel bazı matemetik işlemleri yapan çeşitli standart C fonksiyonları vardır. Bu fonksiyonları kullanırken dosyası include edilmelidir. 29 - sqrt fonksiyonu double bir parametreye sahiptir, parametresi ile aldığı değerin karekökünü geri dönüş değeri olarak verir. double sqrt(double val); Örneğin: #include #include int main() { double val; double result; printf("Lutfen bir sayi giriniz:"); scanf("%lf", &val); result = sqrt(val); printf("%f\n", result); return 0; } pow fonksiyonu üs almak için kullanılır. Parametrik yapısı şöyledir: double pow(double base, double e); Bu fonksiyon birinci parametresiyle belirtilen değerin ikinci parametresiyle belirtilen kuvvetini alır ve onu geri dönüş değeri olarak verir. Örneğin: #include #include int main() { double result; result = pow(3, 4); printf("%f\n", result); return 0; } log fonksiyonu e tabanına göre log10 fonksiyonu 10 tabanına göre logaritma alır: double log(double val); double log10(double val); Örneğin: #include #include int main() { double result; 30 result = log10(1000); printf("%f\n", result); return 0; } sin, cos, asin, acos, tan, atan fonksiyonları trigonometrik işlemleri yapar. Bu fonksiyonlardaki açılar radyan cinsinden girilmelidir. Örneğin: #include #include int main() { double result; result = sin(3.141592653589793238462643 / 6); printf("%f\n", result); result = cos(3.141592653589793238462643 / 3); printf("%f\n", result); result = sin(3.141592653589793238462643 / 4) / cos(3.141592653589793238462643 / 4); printf("%f\n", result); result = tan(3.141592653589793238462643 / 2); printf("%f\n", result); result = tan(3.141592653589793238462643 / 4); printf("%f\n", result); return 0; } exp fonksiyonu e üzeri işlemi yapmaktadır. Sabitler (Literals) Program içerisinde doğrudan yazılan sayılara sabit denir. C'de yalnızca nesnelerin değil, aynı zamanda sabitlerin de türü vardır. Sabitin türü onun niceliğiyle ve sonuna getirilen eklerle ilgilidir. Kurallar şöyledir: 1) Sayı nokta içermiyorsa ve sayının sonunda hiçbir ek yoksa sabit int, long ve unsigned long türlerinin hangisinin sınırları içerisinde ilk kez kalıyorsa o türdendir. Örneğin: 0 ---> int 100 ---> int long türünün 8 byte int türünün 4 byte olduğunu varsayalım: 123 ---> int 3000000000 ---> long Eğer sayı 16'lık ya da 8'lik sistemde belirtilmişse sabit int, unsigned int, long ve unsigned long türlerinin hangisinin sınırları içerisinde ilk kez kalıyorsa sabit o türdendir. 2) Sayı nokta içermiyorsa ve sayının sonuna küçük harf ya da büyük harf L getirilmişse sabit long ve unsigned long türlerinin hangisinin sınırları içerisinde ilk kez giriyorsa o türdendir. Örneğin: 31 100L ---> long 0L ---> long 3) Sayı nokta içermiyorsa ve sayının sonunda küçük harf ya da büyük harf U varsa sabit unsigned int ve unsigned long türlerinin hangisinin sınırları içerisinde ilk kez kalıyorsa o türdendir. Örneğin: 123U ---> unsigned int 1000u ---> unsigned int 4) Sayı nokta içermiyorsa ve sayının sonunda küçük harf ya da büyük harf UL ya da LU varsa (UL, Ul, uL, LU, Lu, ul, lu) sabit unsigned long türündendir. 100ul ---> unsigned long 1LU ---> unsigned long 5) Sayı nokta içeriyorsa ve sayının sonında hiçbir ek yoksa sabit double türdendir. Örneğin: 123.65 ---> double 0.5 ---> double Pek çok programlama dilinde olduğu gibi C'de de noktanın solunda ya da sağında birşey yoksa sıfır olduğu varsayılır. Örneğin: .10 ---> double 10 ---> int 10. ---> double 6) Sayı nokta içeriyorsa ve sayının sonunda küçük harf ya da büyük harf F varsa sabit float türdendir. Örneğin: 12.34F ---> float 10F ---> geçersiz 10.f ---> float 7) Sayı nokta içeriyorsa ve sonunda küçük harf ya da büyük harf L varsa sabit long double türdendir. Örneğin: 100.23L ---> long double 10.l ---> long double .10L ---> long double 10L ---> long 8) Tek tırnak içerisine bri karakter yerleştirilirse bu bir sayı belirtir. O karakterin karakter tablosundaki sıra numarasını belirtir. (Örneğin tipik olarak ASCII.) Örneğin 'a' bir sabit belirtir. Yani aslında 'a' bir sayıdır. ASCII tablosu kullanılıyorsa 97 anlamındadır. Örneğin: x = 'A' + 1; Burada x'e 66 atanmaktadır. 66 da 'B' nin ASCII kodudur. Anahtar Notlar: ASCII tablosunda önce büyük hrfler, sonra küçük harfler gelir. Büyük harflerle küçük harfler arasında 6 farklı karakter vardır. Bu özellikle 'a' - 'A' farkının 32 etmesi için düşünülmüştür. Anahtar Notlar: UNICODE tablonun ilk 128 elemanı standart ASCII tablosusun aynısıdır. ikinci 128'lik elemanı ASCII Latin-1 code page'i ile aynıdır. 32 C'de tek tırnak içerisine int türünün byte uzunluğu kadar karakter yazılabilir (Örneğin şu andaki yaygın sistemlerde int türü 4 byte uzunluğundadır. Bu nedenle tek tırnak içerisine 4 karakter yazılabilir.) Tek tırnak içerisine bir'den fazla karakter yazılırsa bunun hangi sayı anlamına geleceği derleyicileri yazanların isteğine bırakılmıştır. Yani C'de örneğin 'ab' ifadesi geçerlidir. Fakat bunun hangi sayıyı belirttiği derleyiciden derleyiciye değişebilir. Genellikle tek tırnak içerisinde tek bir karakter yerleştiririz. C'de tek tırnak içerisine yerleştirilen sabitler int türdendir (halbuki C++'ta char türdendir.) Karakter tablolarındaki tüm karakterler görüntülenebilir değildir. Bu tür görüntülenemeyen karakterleri (nonprintable characters) ekrana basmak istediğimzde ekranda birşey göremeyiz. Bazı olaylar gerçekleşir. Tek tırnak içerisinde önce bir ters bölü ve sonra özel bazı karakterler bazı görüntülenemeyen özel karakterleri belirtir. Bunların listesi şöyledir: Karakter Anlamı '\a' Alert (ekrana gönderildiğinde beep sesi çıkar) '\b' Back Space (ekrana gönderildiğinde sanki geri tuşuna basılmış gibi imleç bir geriye gider) '\f' Form Feed (bu karakter yazıcıya gönderildiğinde bir sayfa atar) '\n' New Line (ekrana gönderildiğinde imleç aşağı satırın başına geçer) '\r' Carriage Return (ekrana göndeildiğinde imleç aynı satırın başına geçer) '\t' Tab (ekrana gönderildiğinde imleç bir tab atar) '\v' Vertical Tab (ekrana gönderildiğinde düşey zıplama yapar) Bu ters bölü karakterleri iki tırnak içerisinde tek bir karakter belirtirler. Örneğin: printf("ali\tveli\nselami\tayse\n"); Ters bölü karakterinin kendisi '\\' ile belirtilir. '\' ifadesi geçersizdir. Örneğin: #include int main() { printf("c:\temp\a.dat\n"); printf("c:\\temp\\a.dat\n"); return 0; } Tek tırnak karakterinin karakter sabiti ''' biçiminde yazılamaz. '\'' biçiminde belirtilir. Fakat çift tırnak içerisinde tek tırnak soruna yol açmaz. Örneğin: #include int main() { char ch = '\''; printf("%c\n", ch); printf("Turkiye'nin baskenti Ankara'dir\n"); /* geçerli */ printf("Turkiye\'nin baskenti Ankara\'dir\n"); /* geçerli */ 33 return 0; } İki tırnak karakterine ilişkin karakter sabiti '\”' ile belirtilir. Fakat tek tırnak içerisinde iki tırnak soruna yol açmaz. Ancak iki tırnak içerisinde iki tırnak soruna yol açar. Örneğin: #include int main() { char ch1 = '\"'; char ch2 = '"'; /* geçerli */ /* geçerli */ printf("%c, %c\n", ch1, ch2); printf("\"Ankara\"\n"); /* geçerli */ return 0; } Karakter sabitleri tek tırnak içerisinde önce bir ters bölü sonra oktal digit'ler belirtilerek de yazılabilir. Örneğin '\7' karakteri '\a' karakteri ile ASCII tablosunda aynı değer sahiptir. Örneğin: #include int main() { char ch = '\11'; printf("Ankara%cIzmir\n", ch); return 0; } Karaktre sabitleri tek tırnak içerisinde önce bir ters bölü sonra küçük harf bir x sonra da hex digit'ler belirtilerek de yazılabilir. Örneğin '\x1B' gibi. Örneğin: #include int main() { printf("Ankara\x9Izmir\n"); return 0; } İki tırnak içerisinde oktal ve hex sistemde karakterleri yazarken dikkat etmek gerekir. Çünkü derleyici anlamlı en uzun karakter kümesinden ters bölü karakterlerini oluşturur. Yani örneğin “6Ankara\x91Adana” stringinde ters bölü karakteri '\x9' biçiminde değil '\x91A' biçiminde ele alınır. Fakat “6Ankara\x9\x31\x41\x64\x61na\n" istediğimizi yapabilir. 9) C'de short, unsigned short, char, signed char ve unsigned char türünden karakter sabitleri yoktur. Örneğin: #include int main() { printf("6Ankara\x9\x31\x41\x64\x61na\n"); 34 return 0; } Anahtar Notlar: C'de tek tırnak içerisinde yazılan sabitlere karakter sabitleri denir fakat bunlar int türdendir. Yani örneğin C'de 'a' bir karakter sabitidir fakat int türdendir. Oysa C++'ta tek tırnak içerisindeki karakterler (eğer tek tırnak içerisinde tek karakter varsa) char türdendir. Tamsayıların 10'luk, 8'lik ve 16'lık Sistemde Belirtilmesi C'de bir sayı yazdığımızda onu default olarak 10'luk sistemde yazdığımız anlaşılır. eğer sayıyı 0x ya da 0X ile başlatarak yazarsak sayının 16'lık sistemde yazılmış olduğukabul edilir. Örneğin: int a, b; a = 100; b = 0x64; Eğer sayıyı başına 0 getirerek yazarsak 8'lik sistem anlaşılır. Örneğin: a = 0144; Sabitin kaçlık sistemde yazıldığının sabit türüyle bir ilgisi yoktur. Örneğin 0x64 sabiti yine int türden 0x64L sabiti long türdendir. Ayrıca C90'da float, double ve long double türleri 16'lık ve 8'lik sistemde belirtilemezler. Ancak C99'da 16'lık sistemde belirtilebilirler. Gerçek Sayıların Üstel Biçimde Belirtilmesi float, double ve long double türden sabitler üstel biçimde yazılabilirler. Üstel biçimin genel biçimi şöyledir: [±]<üs>; Örneğin 1e20, 1.2e-50, -2.3e+15 gibi... Sayıyı üstel biçimde belirtmişsek sayı sonuna ek almamışsa double türdendir. Örneği 1E20 sabiti double türdendir. Ancak 1E20F float, 1E20L long double türdendir. Üsetel biçim özellikle çok büyük ve çok küçük sayıları ifad etmek için kullanılır. C90'da ve C99'da ikilik sistemde sayılar belirtilememektedir. Operatörler Bir işleme yol açan ve işlem sonucunda bir değer üretimesini sağlayan atomlara operatör denir. Operatörlerin teknik olarak tanımlamak için onları sınıflandıracağız. Operatörler üç biçimde sınıflandırılabilir: 1) İşlevlerine Göre Sınıflandırma 2) Operand Sayılarına Göre 3) Operatörün Konumuna Göre 1) İşlevlerine Göre Sınıflandırma: Bu sınıflandırma operatörün hangi tür işlem yaptığına yönelik bir sınıflandırmadır. Tipik olarak operatör şu sınıflardan birine ilişkin olabilir: - Aritmetik Operatörler (Arithmetic Operators): Bunlar +, -, * ve / gibi dört işleme yönelik operatörlerdir. 35 - Karşılaştırma Operatörleri (Comparision / Relational Operators): Bunlar karşılaştırma için kullanılan operatörledir. Örneğin >, <, >=, <= gibi. - Mantıksal Operatörler (Logical Operators): Bunlar AND, OR, NOT gibi mantıksal işlem yapan operatörlerdir. - Bit Operatörleri (Bitwise Operators): Bunlar sayıların karşılıklı bitleri üzerinde işlem yapan operatörlerdir. - Adres Operatörleri (Pointer Operators): Bunlar adres işlemi yapan operatörlerdir. Özel Amaçlı Operatörler (Special Purpose Operators): Bunlar çeşitli konulara ilişkin özel işlem yapan operatörlerdir. 2) Operand Sayılarına Göre Sınıflandırma: Operatörün işleme soktuğu ifadelere operand denilmektedir. Örneğin a + b ifadesinde + operatördür, a ve b bunun operandlarıdır. Operatörler tek operand, iki operand ve üç operand alabilir. Tek operand alan operatörlere İngilizce “unary”, iki operand alan operatörlere “binary” ve üç operand alan operatörler “ternary” operatör denilmektedir. Örneğin ! tek operandlı (unary bir operatördür ve !a biçiminde kullanılır. / iki operandlı bir operatördür. 3) Operatörün Konumuna Göre Sınıflandırma: Operatör operandlarının önünde, sonunda ya da arasında bulunabilir. Opeandlarının önüne getirilerek kullanılan operatörlere “önek (prefix) operatörler, arasına getirilerek kullanılan operatörlere “araek (infix) operatörler” ve operandlarının sonuna getirilerek kullanılan operatörlere “sonek (postfix)” operatörler denilmektedir. Bir operatörü teknik olarak tanımlayabilmek için öncelikle yukarıdaki üç sınıflandırma biçiminde de nereye düştüğünün belirtilmesi gerekir. Örneğin “! operatörü tek operandlı önek (unary prefix) mantıksal operatördür” gibi. Ya da “/ operatörü iki operandlı araek (binry indfix) aritmetik operatördür” gibi... Operatörler Arasındaki Öncelik İlişkisi Bir ifadedeki operatörler belli bir sırada seri olarak yapılır. Örneğin: a = b + c * d; İ1: c * d İ2: b + İ1 İ3: a = İ2 Derleyiciler ifadedeki işlemi yapacak makine komutlarını seri olarak üretirler. Run time sırasında işlemci onları sırasıyla çalıştırır. Operatörler arasındaki öncelik ilişkisi “Operatörlerin Öncelik Tablosu” denilen bir tabloyla betimlenmektedir. Operatörlerin Öncelik Tablosunun basit bir biçimi aşağıdaki gibidir: () Soldan-Sağa * / Soldan-Sağa + - Soldan-Sağa = Sağdan-Sola Tablo satırlardan oluşur. Üst satırdaki operatörler alt satırdaki operatörlerden daha yüksek önceliklidir. Aynı satırdaki operatörler eşit önceliklidir. Aynı satırdaki operatörler “Soldan-Sağa” ya da “Sağdan-Sola” eşit öncelikli 36 olabilirler. “Soldan-Sağa” öncelik demek, ifade içerisinde “o satırda solda olan önce yapılır” demektir. “SağdanSola” ise “ifade içerisinde o satırda sağda olan önce yapılır” demektir. Tablonun en yüksek öncelik grubunda fonksiyon çağırma operatörü ve öncelik parantezi vardır. Örneğin: result = foo() + bar() * 2; İ1: foo() İ2: bar() İ3: İ2 * 2 İ4: İ1 + İ3 İ5: result = İ4 Parantezler öncelik kazandırmak için kullanılabilirler. Örneğin: a = (b + c) * d; İ1: b + c İ2: İ1 * d İ3 = a = İ2 *, /, + ve - Operatörleri Bu operatörler iki operandlı araek (binary infix) aritmetik operatörlerdir. Temel dört işlemi yaparlar. % Operatörü İki operandlı araek aritmetik operatördür. Bu operatör soldaki oprandın sağdaki operanda bölümünden elde edilen kalan değerini verir. Öncelik tablosunda * ve / ile aynı gruptadır. () Soldan-Sağa * /% Soldan-Sağa + - Soldan-Sağa = Sağdan-Sola Örneğin: #include int main() { int result; result = 10 % 4 - 1; printf("%d\n", result); return 0; } % operatörünün her iki operandının da tamsayı türlerine ilişkin olması zorunludur. Örneğin 15.2 % 2 ifadesi geçerli değildir. Negatif sayının prozitif sayıya bölümünden elde edilen kalan negatiftir. (Örneğin -10 % 4 ifadesi -2 sonucunu verir). 37 İşaret - ve İşaret + Operatörleri + ve - sembolleri hem toplama ve çıkarma operatörleri için hem de işret - ve işaret + işlemleri için kullanılmaktadır. Aynı sembollere sahip olsalar da bu operatörlerin birbirleriyle hiçbir ilgisi yoktur. Örneğin: a = -3 - 5; Burada soldaki - işaret eksi operatörü, sağdaki ise çıkartma operatörüdür. İşaret + ve işaret - operatörleri tek operandlı önek aritmetik operatörlerdir. Öncelik tablosunun ikinci düzeyinde Sağdan-Sola grupta bulunurlar. () Soldan-Sağa + - Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa = Sağdan-Sola Örneğin: a = -3 - 5; İ1:-3 İ2: İ1 - 5 İ3: a = İ2 Örneğin: a = - - b + 2; İ1: -b İ2: -İ1 İ3: İ2 + 2 İ4: a = İ3 İşaret - operatörü operandının negatif değerini üretir. İşaret + operatörü ise operandıyla aynı değeri üretir. (Yani aslında birşey yapmaz.). Örneğin: #include int main() { int result; result = - - - - - - - - - - - - - - - -3 - 5; printf("%d\n", result); return 0; } Tanımsız, Belirsiz ve Derleyiciye Bağlı Davranışlar C standartlarında bazı kodların tanımsız davranışa yol açacağı (undefined behavior) belirtilmiştir. Tanımsız davranışa yol açan kodlamalar derleme aşamasında bir soruna yol açmazlar. Ancak programın çalışma zamanı 38 sırasında programın çökmesine, yanlış çalışmasına yol açabilirler. Bazen tanımsız davranışa yol açan kodlar hiçbir soruna yol amayabilirler. Programcının böylesi farkında olması ve bunlardan uzak durması gerekir. Yine standartlarda bazı durumlar için belirsiz davranışın (unspecified behavior) oluşacağı söylenmiştir. Belirsiz davranışa yol açan kodlarda birkaç seçenek vardır. Derleyici yazanlar bu seçenklerden birini tercih etmiş durumdadırlar. Ancak hangi seçeneği tercih ettiklerini belirtmek zorunda değillerdir. Derleyici hangi seçeneği seçmiş olursa olsun bu kodun çökmesine yol açmaz. Ancak belirsiz davranışa yol açan kodlar taşınabilirliği bozarlar. Yani bu programlar başka sistemlerde derlendiğinde farklı sonuçlar elde edilebilir. En iyisi belirsiz davranış sonucunda farklı sonuçların oluşacağı tarzda kodlardan kaçınmaktadır. Standartlarda bazı durumlardaki belirlemeler derleyiciyi yazanların isteğine bırakılmıştır (implementation defined behavior). Fakat derleyici yazanlar bunu referans kitaplarında açıkça belirtmek zorundadır. Örneğin tahsis edi,lmemiş bir alan erişilmesi tanımsız davranışa yol açan bir işlemdir. C'de fonksiyon çağrıldığındanda parametre aktarımının soldan sağa mı ya da sağdan sola mı yapılacağı belirsiz davranışa sahiptir. Büyük tamsayı türünden işaretli küçük tamsayı türüne dönüştürme yapıldığında bilgi kaybının nasıl olacağı derleyiciyi yazanların isteğine bırakılmıştır. ++ ve -- Operatörleri Bu operatörler tek operandlı önek ve sonek kullanılabilen operatörlerdir. Yani ++a ya da a++ biçiminde kullanılabilirler. ++ operatörüne artırma (increment), -- operatörüne eksiltme (decrement) operatörü denilmektedir. ++ operatörü “operand içerisindeki değeri 1 artır”, -- operatörü “operand içerisindeki değeri 1 esksilt” anlamına gelir. Örneğin: #include int main() { int a; a = 3; ++a; printf("%d\n", a); /* 4 */ a = 3; --a; printf("%d\n", a); /* 2 */ return 0; } ++ ve -- operatörleri öncelik tablosunun ikinci düzeyinde sağdan sola grupta bulunurlar. () Soldan-Sağa + - ++ -- Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa = Sağdan-Sola Bu operatörlerin önek ve sonek kullanımları arasında farklılık vardır. Her iki kullanımda da artırma ya da eksiltme tablodaki öncelikle yapılır. Önek kullanımda sonraki işleme nesnenin artırılmış ya da eksiltilmiş değeri sokulurken, sonek kullanımda artırılmamış ya da eksiltilmemiş değeri sokulur. Örneğin: 39 #include int main() { 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; } Örneğin: #include int main() { int a, b; a = 3; b = ++a; printf("a = %d, b = %d\n", a, b); /* a = 4, b = 4 */ a = 3; b = a++; printf("a = %d, b = %d\n", a, b); /* a = 4, b = 3 */ return 0; } Örneğin: #include int main() { 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; } Şüphesiz bu operatörler tek başlarına başka bir operatör olmaksızın kullanılıyorsa, bunların önek ve sonek biçimleri arasında bir fark oluşmaz. Yani örneğin: ++a; ile 40 a++; arasında bir fark oluşmaz. C'de bir nesne bir ifadede ++ ya da -- operatörüyle kullanılmışsa artık aynı ifadede bir daha o nesnenin görülmemesi gerekir. Aksi halde tanımsız davranış oluşur. Örneğin aşağıdaki gibi ifadeleri kullanmamalıyız. Burada ne olacağı belli değildir: b = ++a + a; b = ++a + ++a; b = ++b; b = a + a++; ++ ve -- operatörlerinin operandlarının nesne belirtmesi gerekir. Örneğin: ++3; ifadesi geçerli değildir. Ya da örneğin: b = ++(a - 2); iafdesi de geçersizdir. Çünkü a - 2 bir nesne belirtmemektedir. Bu operatörlerden elde edilen ürün nesne belirtmez. Örneğin: b = ++a--; gibi bir ifade geçersizdir. Burada a--'den elde edilen ürün nesne belirtmez. Karşılaştırma Operatörleri C'de 6 karşılaştırma operatörü vardır: <, >, <=, >= ==, != Bu operatörlerin hepsi iki operandlı araek (binary infix) operatörlerdir. Öncelik tablosunda karşılaştırma operatörleri aritmetik operatörlerden daha düşük önceliklidir: () Soldan-Sağa + - ++ -- Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa < > <= >= Soldan-Sağa == != Soldan-Sağa = Sağdan-Sola Bu operatörler önerme doğruysa 1 değerini, yanlışsa 0 değerini üretirler. Bu operatörlerin ürettiği bu değerler int türdendir ve istenirse başka işlemlere sokulabilirler. Örneğin: 41 #include int main() { int a = 3, b; b = (a > 2) + 1; printf("%d\n", b); /* 2 */ return 0; } Karşılaştırma operatörleri kendi aralarında iki öncelik grubunda bulunmaktadır. Örneğin: result = a == b > c; İ1: b > c İ2: a == İ1 İ3: result = İ2 Örneğin: #include int main() { int a = 3, b; b = a++ == 3; printf("a = %d, b = %d\n", a, b); return 0; } Örneğin: #include int main() { int a = 20, b; b = 0 < a < 10; /* dikkat bu ifade a'nın 0 ile 10 arasında olduğuna bakamaz */ printf("a = %d, b = %d\n", a, b); return 0; } Mantıksal Operatörler C'de üç mantıksal operatör vardır: ! (NOT) && (AND) || (OR) ! operatörü öncelik tablosunun ikinci düzeyinde Sağdan-Sola grupta bulunur. && ve || operatörleri karşılaştırma operatörlerinden daha düşük önceliklidir: 42 () 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 Anahtar Notlar: C'nin tüm tek operandlı (unary) operatörleri öncelik tablosunun ikinci düzeyinde Sağdan-Sola grupta bulunmaktadır. ! operatörü tek operandlı önek, && ve || operatörleri iki operandlı araek operatörlerdir. AND, OR ve NOT işlemleri operand olarak Doğru, Yanlış Değerlerini alır. Bu mantıksal işlemler aşağıdaki gibi etki gösterirler: A B A AND B A OR B Doğru Doğru Doğru Doğru Doğru Yanlış Yanlış Doğru Yanlış Doğru Yanlış Doğru Yanlış Yanlış Yanlış Yanlış A NOT A Doğru Yanlış Yanlış Doğru C'de bool türü yoktur. Peki Doğru ve Yanlış'lar nasıl ifade edilmektedir? İşte !, && ve || operatörleri önce operandlarını Doğru ya da Yanlış olarak yorumlarlar. Sıfır dışı değerler (tamsayı da gerçek sayı türünden) C'de Doğru olarak, sıfır değeri Yanlış olarak ele alınır. Bu operatörler yine Doğru sonuç için 1 değerini, yanlış sonuç için 0 değerini üretirler. Örneğin: -2.3 && 10.2 0 || 10 10.2 && 0 0.2 && 0.2 => 1 => 1 => 0 => 1 Tabi genellikle mantıksal operatörler doğrudan sayısal değerlerle değil, karşılaştırma operatörlerinin çıktıklarıyla işleme sokulurlar. Örneğin: result = a > 10 && a < 20; Burada iki koşul da doğruysa && operatörü 1 değerini üretecektir. Aksi halde 0 değeri üretilecektir. Örneğin: 43 #include int main() { int a; int result; printf("a:"); scanf("%d", &a); result = a > 10 && a < 20; printf("%d\n", result); return 0; } Anahtar Notlar: Microsoft'un C derleyicileri belirli bir versiyondan sonra bazı standart C fonksiyonları için “Unsafe” uyarısı vermektedir. Üstelik de proje yaratılırkenm SDL Check disable edilmemişse bu uyarı error'e dönüştürülmektedir. Bu “Unsafe” uyarıları ne anlama gelir? Kısaca bu uyarılarda “bu fonksiyonlar esasen standart C fonksiyonlarıdır fakat tasarımlarında küçük birtakım kusurlar vardır”. Evet her ne kadar Microsoft bunda haklıysa da yine de bu fonksiyon çağırılarını Uyarı olarak değerlendirmesi hoş değildir. Eğer programcı bu uyarı mesajlarından kurtulmak istiyorsa iki yol deniyebilir. Birincisi Proje ayarlarından C/C++/Preprocessor kısmına gelip “Preprocesssor Definitions” listesine _CRT_SECURE_NO_WARNINGS makrosunu eklemektir. İkincisi de Proje ayarlarından C/C++/General tabına gelip Uyarı “düzeylerini (Warning Level)” 2'ye çekmektir. Anahtar Notlar: Eğer proje yaratılırken yanlışlıkla SDL Check çarpısı kaldırılmamışsa bu işlem sonradan da yapılabilir. Bunun için proje ayarlarına gelinir. C/C++/General tabından “SDL Checks” disable edilir. Anahtar Notlar: Linux ortamında C ile program geliştirmek için hiç IDE kullanılmayabilir. Program iyi bir Editör'de (Örneğin Kate) yazılıp, komut satırında aşağıdaki gibi derlenebilir: gcc -o sample sample.c Şöyle çalıştırılabilir: ./sample IDE olarak Linux sistemlerinde şu seçenler dikkate değerdir: - MonoDevelop (Mono-core ve MonoDevlop paketleri kurulmalı) - QtCreator - Eclipse (C/C++ için) - Netbeans (C/C++ için) MonoDevelop çalışma biçimi olarak Visual Studio'ya en fazla benzeyen IDE'dir ve kullanımı çok basittir. Anahtar Notlar: UNIX/Linux sistemlerinde komut satırı çalışması hala en yaygın çalışma biçimidir. Çünkü bu sistemler ağırlıklı olarak server amaçlı kullanılmaktadır (Server kullanım oranı %60-70 civarı, Client kullanım oranı %1-%2 civarındadır.) Linux komutlarının belli bir düzeyde öğrenilmesi tavsiye edilir. Bunun için pek çok kitap vardır. Kısa devre özelliği “eğer işlemler hiç bu özellik olmadan yapılsaydı” ile aynı sonucu oluşturur. Yani kısa devre özelliği aslında öncelik tablosunda belirtilen sırada işlemlerin yapılması durumunda elde edilecek olan değerin daha hızlı elde edilmesini sağlar. && ve || operatörleri aynı ifade içerisinde bulunuyorsa her zaman sol taraftaki operatörün sol tarafı önce yapılır. Duruma göre diğer taraflar yapılmaktadır. Örneğin: result = foo() || bar() && tar(); Burada önce foo yapılır. foo sıfır dışı ise başka hiçbir şey yapılmaz ve sonuç 1 olarak elde edilir. Eğer foo sıfır ise 44 bu durumda bar yapılır. bar da sıfır ise tar yapılmaz. Örneğin: result = foo() && bar() || tar(); Burada önce foo yapılır. foo sıfır ise bar yapılmaz fakat tar yapılır. foo sıfır dışı ise bar yapılır. bar da sıfır dışı ise tar yapılmaz. Atama Operatörü Atama operatörü iki operandlı araek özel amaçlı bir operatördür. Bu operatör sağdaki ifadenin değerini soldaki nesneye yerleştirir. Atama operatörü de bir değer üretmektedir. Eğer atama operatörü ifadenin son operatörü değilse ondan elde edilen değeri diğer operatörlere sokabiliriz. Atama operatörü sol taraftaki nesneye atanmış olan değeri üretir. Atama operatörünün öncelik tablousunda Sağdan-Sola grupta bulunduğuna dikkat ediniz. Örneğin: a = b = 10; İ1: b = 10 => 10 İ2: a = İ1 => 10 Örneğin: a = (b = 10) + 20; İ1: b = 10 İ2: İ1 + 20 İ3: a = İ2 Atama operatörünün sol tarafındaki operand nesne belirtmek zorudadır (yani LValue olmak zorundadır). Örneğin: a + b = c; 10 = 20; /* geçersiz! */ /* geçersiz! */ Aşağıdaki gibi bir ifade de geçerlidir: foo(a = 10); Burada önce a = 10 ifadesinin değeri hesaplanır, sonra bu değer foo fonksiyonunun parametre değişkenine atanır. İşlemli Atama Operatörleri (Compound Assignment Operators) C'de bir grup +=, -=, *=, /=, %=, .... gibi işlemli atama operatörü vardır. a = b ifadesi ile a = a b ifadesi tamamen eşdeğerdir. Örneğin: a += 2; a *= 3; /* a = a + 2; */ /* a = a * 3; */ İşlemli atama operataörlerinin hepsi öncelik tablosunda atama operatörüyle Sağdan-Sola aynı öncelik grubunda bulunmaktadır: () Soldan-Sağa + - ++ -- ! Sağdan-Sola * /% Soldan-Sağa 45 + - Soldan-Sağa < > <= >= Soldan-Sağa == != Soldan-Sağa && Soldan-Sağa || Soldan-Sağa = += -= *= /= %=, ... Sağdan-Sola Örneğin: a *= 2 + 3; İ1: 2 + 3; İ2: a *= İ1; Virgül Operatörü Virgül atomu aynı zamanda iki operandlı araek bir operatör belirtir. Virgül operatörü iki farklı ifadeyi tek ifade gibi ifade etmek için kullanılır. Örneğin: a = 10, b = 20 tek bir ifadedir. Çünkü virgül de bir operatördür. Virgül operatörü özel bir operatördür. Klasik öncelik tablosu kuralına uymaz. Ancak öncelik tablosunda tablonun sonuna yerleştirilmiştir: () 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ünün sağında ne olursa olsun önce onun sol tarafındaki ifade tamamen yapılıp bitirilir. Sonra sağındaki ifade tamamen yapılı bitirilir. Virgül operatörü de bir değer üretir. Virgül operatörünün ürettiği değer sağ tafındaki ifadenin değeridir. Virgülün solundaki ifadenin değer üretmekte bir etkisi yoktır. Örneğin: a = (b = 10, 20 + 30); Burada parantezler olmasaydı virgül operatörünün sol tarafındaki operand a = b = 10 ifadesi olacaktı. Parantezler virgül operatörünü diğer operatörlerden ayrıştırmaktadır. Burada önce parantez içi yapılır. Parantez içerisinde virgül operatörü vardır. Onun da önce sol tarafı sonra sağ tarafı yapılır. Virgül operatörünün hepsinden elde edilen değer sağ tarafındaki ifadenin değeridir. Yani yukarıdaki örnekte a'ya 50 atanacaktır. Bir'den fazla virgül operatörü de kombine edilebilir. Örneğin: x = (foo(), bar(), tar()); 46 İ1: foo(), bar() İ2: İ1, tar() İ3: x = İ2 Her virgülü de virgül operatörü sanmamak gerekir. Örneğin: int a, b, c; Buradaki virgüller ayıraç görevindedir. Örneğin: foo(a, b); Buradaki virgül de argüman ayıracı görevindedir. Ancak ekstra bir parantez virgülü operatör durumuna sokar. Örneğin: foo((a, b), c); Burada fonksiyonun iki parametre değişkeni vardır. Birinci parametre değişkenine b, ikincisine c kopyalanır. Noktalı Virgülün İşlevi Noktalı virgül ifadeleri birbirlerinden ayırarak onların bağımsız bir biçimde ele alınmasını sağlar. Örneğin: a = 10 + 20; b = 30 + 40; Burada a = 10 + 20 ile b = 30 + 40'ın bir ilişkisi yontur. Bunlar iki ayrı ifade belirtirler. İfadeleri ayırmak için kullanılan bu tür atomlara sonlandırıcı (terminator) denilmektedir. Eğer noktalı virgül unutulursa derleyici önceki ifadeyle sonraki ifadeyi tek bir bağımsız ifade olarak ele alır o da sentaks bakımından soruna yol açar. Örneğin: a = 10 + 20 b = 30 + 40; /* noktalı virgül unutulmuş */ Burada derleyiciye göre tek bir ifade vardır o da anlamsızdır. Bazı dillerde sonlandırıcı olarak : bazılarında da \n kullanılabilmektedir. Örneğin BASIC'te aslında \n karakteri (ENTER tuşuna basılınca elde edilen karakter) sonlandırıcı görevindedir. Tabibudurumda her satıra tek bir ifade yazılmak zorundadır. Deyimler (Statements) Bir programdaki çalıştırma birimlerine deyim denir. Imperative dillerin teorisinde program deyimlerin çalışmasıyla çalışır. Deyim deyimi içerebilir. Deyimler 5 gruba ayrılmaktadır: 1) Basit Deyimler (Simple Statements): Bir ifadenin sonuna ; getirilirse artık o atom grubu bir deyim olur. Bu tür deyimlere basit deyimler denir. Yani basit deyimler “ifade;” biçimindeki deyimlerdir. Örneğin: x = 10; foo(); birer basit deyimdir. Görüldüğü gibi ifade kavramı noktalı birgülü içermemektedir. İfadenin sonuna noktalı virgül konulunca o deyim olur. 47 2) Bileşik Deyimler (Compund Statements): C'de bir bloğun içerisine sıfır tane ya da daha fazla deyim yerleştirilirse o bloğun tamamı da bir deyim olur. Bunlara bileşik deyim denilmektedir. Bileşik deyimi diğer deyimleri içeren deyimler gibi (yani büyük kutu gibi) düşünebiliriz. Örneğin: ifade1; { ifade2; { ifade3; ifade4; } ifade5; } ifade6; Burada dışarıdan bakıldığında 3 deyim vardır: İkisi basit biri bileşik deyim. Bileşik deyimin içerisinde 3 tane deyim bulunmaktadır. 3) Kontrol Deyimleri (Control Statements): if gibi, for gibi, while gibi akış üzerinde etkili olan özel deyimlere kontrol deyimleri denilmektedir. Zaten ilerleyen bölümde bu kontrol deyimleri tek tek ele alınıp incelenecektir. 4) Bildirim Deyimleri (Declaration Statements): Bildirim yapmakta kullanılan sentaks yapıları da aslında deyim belirmektedir. Bunlara bildirim deyimleri denir. Örneğin: int a, b; int x; a = 10; b = 20; Burada iki bildirim deyimi, iki de basit deyim vardır. 5) Boş Dyimler (Null Statements): Solunda ifade olmayan noktalı virgüllere boş deyim denilmektedir. Örneğin: x = 10;; Buradaki ilk noktalı virgül ifadeyi deyim yapan sonlandırıcıdır. Fakat ikinci noktalı virgülün solunda bir ifade yoktur. Boş deyimler “hiçbirşey;” gibi düşünülebilir. Boş deyim bir etkiye yol açmasa da yine de bir deyim olarak değerlendirilir. yukarıdki örnekte tolam iki deyim vardır. Her deyim çalıştığında birşeyler olur: 1) Bir basit deyimin çalıştırılması demek o deyimi oluşturan ifadenin yapılması demektir. 2) Bir bileşik deyimin çalıştırılması demek onu oluşturan deyimlerin tek tek çalıştırılması demektir. Örneğin: { ifade1; { ifade2; ifade3; 48 } ifade4; } Burada bu bileşik deyim çalıştırıldığında önce ifade1; basit deyimi çalıştırılır. Sonra içerideki bileşik deyim çalıştırılır. Bu da ifade2; ve ifade3; basit deyimlerinin çalıştırışması anlamına gelir. Sonra da ifade4; basit deyimi çalıştırılır. Özetle bir bileşik deyim çalıştırıldığında onu oluşturan deyimler yukarıdan aşağıya doğru çalıştırılır. Aslında her fonksiyon ana bloğuyla bir bileşik deyim belirtir. Bir fonksiyon çağrıldığında o fonksiyonun ana bloğuna ilişkin bileşik deyim çalıştırılır. O halde her şey main fonksiyonun çağrılmasıyla başlar. Bir C programının çalıştırılması demek main fonksiyonunun çağrılması demektir. main fonksiyonu programı yükleyen işletim sistemi tarafından çağrılır. 3) Bir kontrol deyimi çalıştırıldığında nelerin olacağı izleyen bölümde ele alınmaktadır. 4) Bir bildirim deyimi çalıştırıldığında bildirilen değişkenler için yer ayrılır. 5) Boş deyim karşısında derleyiciler hiçbir şey yapmaz. Kontrol Deyimleri Programın akışı üzerinde etkili olan deyimler kontrol deyimleri denir. Burada C'deki kontrol deyimleri başlıklar haliden ele alınacaktır. if Deyimi if deyiminin genel biçimi şöyledir: if () [ else ] if anahtar sözcüğünden sonra parantezler içerisinde bir ifadenin bulunması zorunludur. if deyimi doğruysa ve yanlışsa kısımlarından oluşur. Doruysa ve yanlışsa kısımlarında tek bir deyim bulunmak zorundadır. Eğer programcı bu kısımlarda birden fazla deyim bulundurmak istiyorsa onu bileşik deyim olarak ifade etmelidir (yani bloklamalıdır). if deyiminin yanlışsa kısmı bulunmak zorunda değildir. if deyiminin tamamı tek bir deyimdir. if deyimi şöyle çalışır: Önce if parantezi içerisindeki ifadenin sayısal değeri hesaplanır. Bu değer sıfır dışı ise if deyiminin doğruysa kısmı, sıfır ise yanlışsa kısmı çalıştırlır. Örneğin: #include int main(void) { int a; printf("Bir sayi giriniz:"); scanf("%d", &a); if (a > 0) printf("pozitif\n"); else printf("negatif ya da sifir\n"); 49 printf("son\n"); return 0; } Örneğin: #include #include int main(void) { double a, b, c; double delta; printf("a:"); scanf("%lf", &a); printf("b:"); scanf("%lf", &b); printf("c:"); scanf("%lf", &c); delta = b * b - 4 * a * c; if (delta < 0) printf("Kok yok!\n"); else { double x1, x2; x1 = (-b + sqrt(delta)) / (2 * a); x2 = (-b - sqrt(delta)) / (2 * a); printf("x1 = %f, x2 = %f\n", x1, x2); } return 0; } İf deyiminin doğruysa kısmı bulunmak zorundadır. Ancak yanlışsa kısmı bulunmak zorunda değildir. Örneğin: if (ifade1) ifade2; ifade3; Burada if deyiminin doğruysa kısmında yalnızca ifade; deyimi vardır. ifade3; deyimi if dışındadır. Örneğin: if (ifade1) { ifade2; ifade3; } ifade4; Burada ifade4; deyimi if dışındadır. Derleyici if deyiminin doğruysa kısmı bittiğinde else anahtar sözcüğünün olup olmadığına bakar. eğer else anahtar sözcüğü yoksa if deyimi bitmiştir. Örneğin: #include int main(void) 50 { int a; printf("Bir sayi giriniz:"); scanf("%d", &a); if (a > 0) printf("pozitif\n"); printf("son\n"); return 0; } if deyiminin yanlışlıkla boş deyim ile kapatılması sık rastalanan bir hatadır. Örneğğin: if (a > 0); /* dikkat yanlışlıkla yerleştirilmiş boş deyim */ printf("pozitif\n"); Boş deyim için bir şey yapılmıyor olsa da boş deyim yine bir deyimdir. Yalnızca yanlışsa kısmı olan bir if olamaz. Fakat bunu aşağıdaki gibi yapay bir biçimde oluşturabiliriz: if (ifade1) ; else ifade2; Tabi bunun yerine soruyu ters çevirmek daha iyi bir tekniktir: if (!ifade1) ifade2; Aşağıdaki if cümlesinde derleyici hatayı nasıl tespit edecektir? if (ifade1) ifade2; ifade3; else ifade4; Bu bize if deyiminin doğruysa kısmında birden fazla deyim olduğu halde bloklama yapılmamış hatası izlenimini vermektedir. Oysa derleyiciye göre ifade’den sonra if deyimi bitmiştir. Dolayısıyla if olmadan kullanılan bir else söz konusudur. Örneğin Microsoft derleyicileri bu durumda şu error mesajını vermektedir: “illegal else without matching if”. İç içe if Deyimleri (Nested if Statements) Bir if deyiminin doğruysa ya da yanlışsa kısmında başka bir ifa deyimi bulunabilir. Örneğin: 51 Bu akış diagramının if cümlesi şöyle yazılabilir: if (ifade1) if (ifade2) { ifade3; ifade4; } else ifade5; else ifade6; İki if için tek bir else varsa else hangi if’e ilişkindir. Örneğin: if (ifade1) if (ifade2) ifade3; else ifade4; Görünüşe bakılırsa burada iki durum da sanki mümkünmüş gibidir. else birinci if’in else kısmı da olabilir (yani ikinci if else’siz olabilir), ikinci if’in else’i de olabilir (yani birinsi if else’sizdir). Bu duruma “dangling else” denilmektedir. Burada else içteki if’e ilişkindir. Deneyimli programcılar bile aşağdaki gibi hatalı kod yazabilemktedir: if (ifade1) if (ifade2) ifede3; else ifade4; Burada else’in gerçekten birinci if’e ilişkin olması isteniyorsa bilinçli bloklama yapılmalıdır: if (ifade1) { if (ifade2) ifede3; } else ifade4; Ayrık Koşullar Bir grup koşuldan herhangi birisi doğruyken diğerlerinin doğru olma olasılığı yoksa bu koşullara ayrık koşullar 52 denilmektedir. Örneğin: a > 0 a < 0 koşulları ayrıktır. Örneğin: a == 0 a > 0 a < 0 koşulları ayrıktır. Örneğin: a == 1 a == 2 a == 3 koşulları ayrıktır. Fakat: a > 0 a > 10 koşulları ayrık değildir. Ayrık koşulların ayrık if’lerle ifade edilmesi kötü bir tekniktir. Örneğin: if (a > 0) printf("pozitif\n"); if (a < 0) printf("negatif\n"); Burada a > 0 ise a < 0 olma olasılığı yoktur. Fakat gereksiz bir biçimde yine bu koşul yapılacaktır. Ayrık koşulların else if’lerle ifade edilmesi gerekir. Örneğin: if (a > 0) printf("pozitif\n"); else if (a < 0) printf("negatif\n"); Burada artık a > 0 ise gereksiz bir biçimde a < 0 işlemi yapılmayacaktır. Bazen else-if durumu bir merdiven haline gelebilir. Örneğin: 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("hic biri\n"); else if merdivenlerinde olasılığı yüksek olanları yukarıya yerleştirmek daha iyi bir tekniktir. 53 Klavyeden Karakter Okumak Klavyeden scanf fonksiyonuyla %c formatıyla karakter okuyabiliriz. Örneğin: #include int main(void) { char ch; printf("bir karakter giriniz:"); scanf("%c", &ch); printf("girilen karakter: %c\n", ch); return 0; } Fakat karakter okumak için getchar isimli standart bir C fonksiyonu da vardır: int getchar(void); Fonksiyon bir tuşa basılıp ENTER tuşuna basılana kadar bekler. Basılan tuşun karakter tablosundaki sıra numarasına geri döner. Geri dönüş değeri aslında bir byte’lık karakterin kod numarasıdır. Tipik olarak char türden bir nesneye atanabilir. Örneğin: #include int main(void) { char ch; ch = getchar(); printf("girilen karakter: %c\n", ch); return 0; } stdin dosyasından okuma yapan fonksiyonlar tamponlu (buffered) bir çalışmaya sahiptir. Bu çalışma -bazı detayları olmakla birlikte- kabaca şöyledir: getchar fonksiyonuyla biz bişeyler girdiğimizde girdiğimiz karakterler ve ilave olarak \n karakteri önce bir tampona aktarılır. Sonra oaradan sırasıyla alınır. (En son ‘\n’ alınacaktır.) Eğer tamponda karakter varsa getchar klavyden birşey istemez. Tampondan alır. Ancak tamponda hiçbir karakter kalmamaışsa getchar klavyeden değer ister. Örneğin: #include int main(void) { char ch; ch = getchar(); printf("girilen karakter: %c\n", ch); ch = getchar(); printf("girilen karakter: %c\n", ch); return 0; } 54 Burada birinci getchar’da biz k karaktereine basıp ENTER tuşuna basmış olalım. Tamponda k\n karakterleri bulunacaktır. Birinci getchar k’yı alacak ikincisi hiç beklemeden ‘\n’ karakterini alacaktır. Sınıf Çalışması: Klavyeden getchar fonksiyonuyla bir karakter okuyunuz. Karakter ‘a’ ise ali, ‘b’ ise burhan, ‘c’ ise cemal ‘d’ ise demir, ‘e’ ise ercan ve bunların dışında bir karakterse hiçbiri yazısını bastırınız. Çözüm: #include int main(void) { char ch; ch = getchar(); if (ch == 'a') printf("ali\n"); else if (ch == 'b') printf("burhan\n"); else if (ch == 'c') printf("cemal\n"); else if (ch == 'd') printf("demir\n"); else if (ch == 'e') printf("ercan\n"); else printf("hicbiri\n"); return 0; } Anahtar Notlar: Standartlar asgariyi belirlemek için oluşturulmuştur. Standartlara uygun bir C derleyicisinin ekstra özellikleri olabilir. Bunlara eklenti (extension) denilmektedir. Eklentiler sentaksa ilişkin olabileceği gibi kütüphaneye ilişkin de olabilir. Eklenti özellikleri kullanırken dikkatli olmalıyız. Çünkü bunlar her derleyicide bulunmak zorunda olmayan özelliklerdir. Bu durumda kodumuzun taşınabilirliği (portability) zarar görür. Ancak bazı eklentiler çok yaygın bir biçimde desteklenmektedir. Hatta bu eklentileri bazı programcılar standart özellik dahi sanabilmektedir. Klavyeden karakter okuyan diğer bir fonksiyon da getch fonksiyonudur. getch standart bir C fonksiyonu değildir. Pek çok C derleyicisind bulunan bir eklenti fonksiyondur. Microsoft C derleyicilerinde ve gcc derleyicilerinde bu fonksiyon bulunmaktadır. Fonksiyonun parametrik yapısı şöyledir: #include int getch(void); Bu fonksiyon ENTER tuşuna gereksinim duymaz ve tuşu basar basmaz alır. Basılan tuş görüntülenmez (biz istersek görüntüleriz). Örneğin: #include #include int main(void) { char ch; printf("bir karakter giriniz:"); ch = getch(); 55 printf("\ngirilen karakter: %c\n", ch); return 0; } Bazen programcılar bu fonksiyonu bir tuşa basılana kadar akışı bekletmek için de kullanmaktadır. Örneğin: #include #include int main(void) { printf("Press any key to continue...\n"); getch(); printf("ok\n"); return 0; } Diğer bir karakter okuyan eklenti fonksiyon getche fonksiyonudur. Bu fonksiyonun getch’tan farkı basıldığı zaman basılan tuşun da görüntülenmesidir. putchar Fonksiyonu putchar standart bir C fonksiyonudur. Bu fonksiyon parametresiyle aldığı sayıya karşılık gelen karakter tablosundaki karakter görüntüsünü ekrana yazdırır. Örneğin: char ch = ‘a’; putchar(ch); Burada ekrana a karakteri çıkar. Yani: putchar(ch); ile: printf(“%c”, ch); aynı etkiyi oluşturur. Örneğin: #include #include int main(void) { char ch; while ((ch = getch()) != 'q') putchar(ch); return 0; } Örneğin: #include 56 int main(void) { char ch; while ((ch = getchar()) != '\n') putchar(ch); putchar('\n'); return 0; } Döngü Deyimleri Bir program parçasının yinelemeli olarak çalıştırılmasını sağlayan kontrol deyimlerine döngü (loop) denir. C’de 2 döngü deyimi vardır: while döngüleri ve for döngüleri. while döngüleri de kendi aralarında “kontrolün başta yapıldığı while döngüleri” ve “kontrolün sonda yapıldığı while döngüleri (do-while döngüleri)” olmak üzere ikiye ayrılır. Kontrolün başta yapıldığı while döngüleri Bu biçimdeki while döngüleri pek çok programlama dilinde benzer biçimde bulunmaktadır. Genel biçimi şöyledir: while () while anahtar sözcüğünden sonra parantezler içerisinde bir ifade bulunmak zorundadır. Döngü içerisinde tek bir deyim vardır. O yinelenecek olan deyimdir. while döngüsü şöyle çalışır: while parantezi içerisindeki ifadenin sayısal değeri hesaplanır. Bu değer sıfır dışı bir değerse doğru kabul edilir ve döngü deyimi çalıştırılıp başa dönülür. Bu ifade sıfır ise while deyimi sonlanır. while döngüleri paranztez içerisindeki ifade sıfır dışı olduğu sürece yinelenen döngülerdir. Örneğin: #include int main(void) { int i = 0; while (i < 10) { printf("%d\n", i); ++i; } 57 return 0; } Örneğin: #include int main(void) { int i = 10; while (i) { printf("%d\n", i); --i; } return 0; } Bir değeri atayıp, atanan değeri karşılaştırmak için parantezler kullanılmalıdır. Örneğin: #include #include int main(void) { char ch; while ((ch = getch()) != 'q') printf("%c", ch); return 0; } while parantezi içerisindeki ifadede virgül operatörü kullanılabilir. Örneğin: #include #include int main(void) { char ch; while (ch = getch(), ch != 'q') putchar(ch); putchar('\n'); return 0; } Burada önce ch = getch() ifadesi yapılır, sonra ch != ‘q’ ifadesi yapılır. Test işlemine ch != ‘q’ ifadesi sokulur. Anahtar Notlar: ENTER tuşuna basıldığında normal olarak “Carriage Return (CR)” karakterinin sayısal değeri elde edilir. CR karakterini ekrana yazdırdığımızda imleç bulunduğu satırın başına geçer. Fakat getchar fonksiyonunda karakter girdikten sonra basılan ENTER tuşu için getchar tampona CR değil “LF (Line Feed)” karakterini yerleştirmektedir. LF karakterini ekrana yazdırmak istediğimizde imleç aşağı satırın başına geçer. getch ve getche fonksiyonlarında ENTER tuşuna bastığımızda CR karakterini elde edriz. ENTER tuşu niyet olarak aşağı satırın başına geçmek amacıyla klavyeye yerleştirilmiş olsa da onun asıl kodu CR’dir. ENTER’a bastığımızda CR yerine LF karakterinin elde edilmesi girdi alan fonksiyonların yaptığı bir şeydir. CR karakteri (13 nolu ASCII karakter) C’de ‘\r’ ile, LF karakteri de (10 numaralı ASCII karakteri) C’de ‘\n’ ile temsil edilir. 58 while döngülerinin de yanlışlıkla boş deyim kapatılması durumuyla karşılaşılmaktadır. Örneğin: #include #include int main(void) { char ch; while (ch = getch(), ch != 'q'); putchar(ch); putchar('\n'); return 0; } Burada artık döngünün içerisinde boş deyim vardır. while parantezinin içerisinde ++ ve -- operatörlerinin kullanılması kafa karıştırabilmektedir. Örneğin: #include int main(void) { int i = 0; while (++i < 10) printf("%d ", i); /* 1'den 9'a kadar */ putchar('\n'); return 0; } while parantezinde sonek bir ++ ya da -- varsa önce artırım ya da eksiltim uygulanır fakat sonraki operatöre nesnenin artırılmamış ya da eksiltilmemiş değeri sokulur. Örneğin: #include int main(void) { int i = 0; while (i++ < 10) printf("%d ", i); /* 1'den 10'a kadar */ putchar('\n'); return 0; } Örneğin: #include int main(void) { int i = 10; while (--i) 59 printf("%d ", i); /* 9'dan 1'e kadar */ putchar('\n'); return 0; } Örneğin: #include int main(void) { int i = 10; while (i--) printf("%d ", i); /* 9'dan 0'a kadar */ putchar('\n'); return 0; } Burada while parantezi içerisindeki ifadeyi while (i-- != 0) biçiminde değerlendirmek gerekir. Yani burada önce i bir eksiltilir fakat teste i’nin eksiltilmemiş değeri sokulur. Başka bir deyişle: while (i--) { //... } ile, while (i-- != 0) { //... } eşdeğer etkiye sahiptir. while ile sonsuz döngü aşağıdaki gibi oluşturulabilir: while (1) { /* ... */ } Kontrolün Sonda Yapıldığı while Döngüleri (do-while Döngüsü) Kontrolün sonda yapıldığı while döngüsünün genel b içimi şöyledir: do while (ifade); while parantezinin sonundaki noktalı virgül boş deyim belirtmez. Bu noktalı virgül sentaksın bir parçasıdır ve orada bulunmak zorundadır. Kontrolün sonda yapıldığı while döngüleri kontrolün başta yapıldığı while döngülerinin ters yüz edilmiş bir biçimidir. Ancak başına bir de do anahtar sözcüğü getirilmiştir. do-while döngülerinde döngü deyimi en az bir kez yapılmaktadır. Örneğin: #include 60 int main(void) { int i; i = 0; do { printf("%d\n", i); ++i; } while (i < 10); /* 0...9 */ return 0; } Kontrolün başta yapıldığı while döngülerine kontrolün sonda yapıldığı while döngülerine göre çok fazla gereksinim duyulur. Fakat bazen kontrolün sonda yapıldığı while döngülerini kullanmak daha anlamlı olabilmektedir. Örneğin: #include int main(void) { char ch; ch = ' '; while (ch != 'e' && ch != 'h') { printf("(e)vet/(h)ayir?"); ch = getch(); printf("%c\n", ch); } if (ch == 'e') printf("evet secildi\n"); else printf("hayir secildi\n"); return 0; } Burada kontrolün sonda yapıldığı while döngüsü daha uygundur: #include #include int main(void) { char ch; do { printf("(e)vet/(h)ayir?"); ch = getch(); printf("%c\n", ch); } while (ch != 'e' && ch != 'h'); if (ch == 'e') printf("evet secildi\n"); else printf("hayir secildi\n"); return 0; } 61 Karakter Test Fonksiyonları C’de başı “is” ile başlayan isxxx biçiminde isimlendirilmiş bir grup standart fonksiyon vardır. Bunlara karakter test fonksiyonları denilmektedir. Bu fonksiyonların hepsi parametre olarak bir karakter alır ve int türden bir geri dönüş değeri verir. Bu fonksiyonlar parametreleriyle aldıkları karakterleri test ederler. Test doğruysa sıfır dışı bir değere yanlışsa sıfır değerine geri dönerler. Bu fonksiyonların listesi şöyledir: isupper (büyük harf mi?) islower (küçük harf mi?) isdigit (‘0’ ile ‘9’ arasındaki karakterlerden biri mi?) isxdigit (hex karakterlerden biri mi?) isalpha (alfebetik karakterlerden biri mi?) isalnum (alfabetik ya da nümerik (alfanümerik) karakterlerden biri mi?) isspace (boşluk karakterlerinden biri mi? (Space, tab, new line, carriage return, vertical tab)) isascii (ASCII tablosunun ilk yarısındaki karakterlerden biri mi?) iscntrl (İlk 32 kontrol karakterinden biri mi?) ispunct (noktalı virgül, nokta vs. gibi karakterlerden biri mi) Bu fonksiyonları kullanırken dosyası include edilmelidir. Anahtar Notlar: Önce atama yapıp sonra atanan değerin karşılaştırılmasını istiyorsak parantez kullanmalıyız. Örneğin: if ((ch = getch()) != ‘q’) { ... } Örneğin: #include #include #include int main(void) { char ch; while ((ch = getch()) != 'q') { printf("%c\n", ch); if (isupper(ch)) printf("buyuk harf\n"); else if (islower(ch)) printf("kucuk harf\n"); else if (isdigit(ch)) printf("digit karakter\n"); else if (isspace(ch)) printf("bosluk karakterlerinden biri\n"); else if (ispunct(ch)) printf("noktalama karakterlerinden biri\n"); else printf("diger bir karakter\n"); } return 0; } Anahtar Notlar: UNIX/Linux sistemlerinde getch ve getche fonksiyonları yalnızca curses kütüphanesinde bulunmaktadır. Bu nedenle o kütüphanenin kurulması gerekir. Benzer biçimde gcc’nin Windows portunda da bu fonksiyonlar bulunmamaktadır. Karakter test fonksiyonlarını biz de yazabiliriz. Örneğin isupper şöyle yazılabilir: 62 #include int myisupper(char ch) { if (ch >= 'A' && ch <= 'Z') return 1; return 0; } int myislower(char ch) { if (ch >= 'a' && ch <= 'z') return 1; return 0; } int myisalpha(char ch) { if (myisupper(ch) || myislower(ch)) return 1; return 0; } int main(void) { char ch; printf("Karakter: "); ch = getchar(); if (myisalpha(ch)) printf("alfabetik\n"); else printf("alfabetik degil\n"); return 0; } Karakter test fonksiyonları ancak karakter 0-127 arasındaysa doğru çalışmaktadır. Bu fonksiyonlar ASCII karkterlerinin ilk 7 bitini dikkate almaktadır. Büyük Harf Küçük Harf Dönüştürmesi Yapan Standart C Fonksiyonları toupper fonksiyonu eğer parametresi ile belirtilen karakter küçük bir karakterse onun büyük karşılığıyla geri döner fakat küçük harf karakter değilse aynısı i,le geri döner. tolower fonksiyonu da benzer biçimde parametresi ile aldığı karakter büyük harf ise onun küçük harf karşılığıyla değilse onun aynısıyle geri dönmektedir. Bu fonksiyonları da kullanmadan önce dosyasının include edilmesi gerekir. Örneğin: #include #include int main(void) { char ch; printf("Karakter giriniz:"); ch = getchar(); ch = toupper(ch); printf("%c\n", ch); 63 return 0; } Anahtar Notlar: Karakter test fonksiyonları ve toupper ile tolower fonksiyonları C90 ekleriyle lokal spesifik özelliğe sahip olmuştur. Yani uygun lokal set edilmişse bunlar o dile göre de çalışabilir. Default lokal İngilizce olduğu için bunların da default davranışı standart İngilizce karakterler dikkate alınarak yapılır. toupper ya da tolower kullanımına tipik bir örnek de şöyle olabilir: #include #include #include int main(void) { char ch; do { printf("(e)vet/(h)ayir?"); ch = tolower(getch()); printf("%c\n", ch); } while (ch != 'e' && ch != 'h'); if (ch == 'e') printf("evet secildi\n"); else printf("hayir secildi\n"); return 0; } for Döngüleri for döngüleri aslında while döngülerinin daha genel bir biçimidir. for döngülerinin genel biçimi şöyledir: for ([ifade1];[ifade2];[ifade3]) for anahtar sözcüğünden sonra parantezler içerisinde iki noktalı virgül bulunmak zorundadır. Bu for döngüsünü üç kısma ayırır. Bu üç kısımda da ifade tanımına uyan herhangi birer ifade kullanılabilir. for döngüsü şöyle çalışır: 64 for döngüsünün birinci kısmındaki ifade döngüye girişte bir kez yapılır. Bir daha da yapılmaz. Döngü ikinci kısımdaki ifade sıfır dışı olduğu sürece yinelenir. İkinci kısımdaki ifade sıfır dışı ise önce döngü deyimi yapılır. Sonra üçüncü kısımdaki ifade yapılır ve yeniden kontrole girer. for döngülerinin en çok kullanılan kalıbı şöyledir: for (ilkdeğer; koşul; artırım) Örneğin: #include int main(void) { int i; for (i = 0; i < 10; ++i) printf("%d\n", i); return 0; } Örneğin: #include int main(void) { int i; for (i = 0; i < 10; ++i) printf("%d ", i); printf("\n"); return 0; } Örneğin: #include int main(void) 65 { int i; for (i = 0; i < 10; i += 2) printf("%d ", i); printf("\n"); return 0; } Örneğin: #include #include int main(void) { double x; for (x = 0; x < 6.28; x += 0.1) printf("Sin(%.3f)=%.3f\n", x, sin(x)); return 0; } Örneğin: #include int main(void) { int i; for (i = 10; i >= 0; --i) printf("%d ", i); printf("\n"); return 0; } Şüphesiz üçüncü kısımda yapılan artırım ya da eksiltimlerin önek ya da sonek olması arasında bir fark yoktur. 1’den 100’e kadar sayıların toplamı şöyle bulunabilir: #include int main(void) { int i, total; total = 0; for (i = 1; i <= 100; ++i) total += i; printf("%d\n", total); return 0; } Bir döngünün döngü deyimi başka bir döngü olabilir. Bu duruma iç içe döngüler (nested loops) denilmektedir. Örneğin: 66 #include int main(void) { int i, k; for (i = 0; i < 3; ++i) for (k = 0; k < 3; ++k) printf("(%d, %d)\n", i, k); return 0; } for döngülerinin de yanlışlıkla boş deyim ile kapatılması durumuyla karşılaşılmaktadır. Örneğin: #include int main(void) { int i; for (i = 0; i < 10; ++i); printf("%d\n", i); /* Dikkat boş deyim */ return 0; } for döngüsünün üç kısmına da ifade tanımına uyan her şey yerleştirilebilir. Örneğin: #include int main(void) { int i; i = 0; for (printf("birinci kisim\n"); i < 3; printf("ucuncu kisim\n")) { printf("dongu deyimi\n"); ++i; } return 0; } for döngüsünün birinci kısmındaki ifade yazılmayabilir. Buradaki ifadeyi yukarı alsak da değişen birşey olmaz. Örneğin: for (i = 0; i < 10; ++i) { ... } ile i = 0; for (; i < 10; ++i) { ... } tamamen işlevsel olarak eşdeğerdir. Örneğin: 67 #include int main(void) { int i; i = 0; for (; i < 10; ++i) printf("%d ", i); printf("\n"); return 0; } for döngüsünün üçüncü kısmı da yazılmayabilir. Aslında üçüncü kısmındaki ifade döngü değiminin sonuna yerleştirilirse değişen bir şey olmaz. Örneğin: for (i = 0; i < 10; ++i) { ... } ile i = 0; for (; i < 10; ) { ... ++i; } işlevsel olarak eşdeğerdir. Örneğin: #include int main(void) { int i; i = 0; for (; i < 10;) { printf("%d ", i); ++i; } printf("\n"); return 0; } Birinci ve üçüncü kısmı olmayan for döngüleri tamamen while döngüleriyle eşdeğerdir. Yani: for (;ifade;) { ... } ile while (ifade) { ... } tamamen eşdeğerdir. 68 Şüphesiz for döngüsü olmasaydı while döngüsünden for elde edebilirdik. Yani: ifade1; while (ifade2) { ifade3; } aslında aşağıdaki for döngüsüyle eşdeğerdir: for (ifade1; ifade2; ifade3) #include int main(void) { int i; i = 0; while (i < 10) { printf("%d\n", i); ++i; } for (i = 0; i < 10; ++i) printf("%d\n", i); return 0; } for döngüsünün ikinci kısmı da boş bırakılabilir. Bu durumda döngü koşulunun her zaman sağlandığı kabul edilir. Yani döngünün ikinci kısmına birşey yazmamakla buraya sıfır dışı bir değer yazmak aynı anlamdadır. Örneğin: #include int main(void) { int i; for (i = 0;; ++i) /* sonsuz döngü (infinite loop) */ printf("%d\n", i); return 0; } for döngüsünün üç kısmında da virgül operatörü zenginlik katmak için kullanılabilir. Örneğin: #include int main(void) { int i, total; for (i = 1, total = 0; i <= 100; total += i, ++i) ; printf("%d\n", total); return 0; } 69 Burada döngünün birinci kısmını i = 1, total = 0 ifadesi oluşturmaktadır. Üçüncü kısmını ise total += i, ++i ifadesi oluşturmaktadır. Anahtar Notlar: Bir problemi kesin çözüme götüren adımlar topluluğuna algoritma (algorithm) denilmektedir. Algoritmaları hız bakımından ya da kaynak kullanımı bakımından karşılaştırabiliriz. Baskın ölçüt hızdır ve default olarak hız akla gelmektedir. Bazen bir problemin algoritması probleminin yapısı gereği çok uzun bilgisayar zamanı alabilmektedir. Bu durumda makul iyi bir çözümle yetinilebilir. Problemi kesin çözüme götürmeyen ancak iyi bir çözüm vaat eden adımlar topluluğuna sezgisel yöntem (heuristic) denilmektedir. Önce C++'a sonra da C99 sokulmuş olan for döngülerinde önemli bir özellik vardır. C++'ta ve C99'da for döngülerinin birinci kısmıunda bildirim yapılabilir. Bu prtaiklik sağlamaktadır. Örneğin: #include int main(void) { for (int i = 0; i < 10; ++i) printf("%d\n", i); return 0; } Birden fazla değişkenin de aynı türden olmak koşuluyla bu biçimde bildirimi yapılabilir. Örneğin: for (int i = 0, k = 100; i + k >= 50; ++i, k -= 2) { ... } Bu biçimde bildirilen değişkenler ancak for döngüsünün içerisinde kullanılabilir. C++ ve C99'un standartlarına göre, for (bildirim; ifade2; ifade3) ile, { bildirim; for (;ifade2; ifade3) } eşdeğerdir. Biz kursumuzda C90 gördüğümüz için bu özelliği kullanmayacağız. Ancak bu özellik pek çok C90 derleyicisinde ekstra bir eklenti olarak bulunmaktadır. break Deyimi break deyiminin genel biçimi şöyledir: break; break deyimi yalnızca döngülerin ya da switch deyiminin içerisinde kullanılılır. Programın akışı break deyimini gördüğünde döngü deyiminin kendisi sonlandırılır, akış sonraki deyimle devam eder. (Yani break adeta döngü dışına goto yapmak gibi etki gösterir.). Örneğin: 70 #include int main(void) { int val; for (;;) { printf("Sayi giriniz:"); scanf("%d", &val); if (!val) break; printf("%d\n", val * val); } return 0; } Asal sayı testi aşağıdaki gibi yapılabilir: #include int isprime(int val) { int i, result; result = 1; for (i = 2; i < val; ++i) if (val % i == 0) { result = 0; break; } return result; } int main(void) { int i; for (i = 2; i < 1000; ++i) if (isprime(i)) printf("%d ", i); printf("\n"); return 0; } Yukarıdaki asallık testi aşağıdaki gibi iyileştirilebilir: #include #include int isprime(int val) { int i, result; double n; if (val % 2 == 0) return val == 2; n = sqrt(val); result = 1; for (i = 3; i <= n; i += 2) if (val % i == 0) { 71 result = 0; break; } return result; } int main(void) { int i; for (i = 2; i < 1000; ++i) if (isprime(i)) printf("%d ", i); printf("\n"); return 0; } İç içe döngülerde break yalnızca kendi döngüsünden çıkar. Örneğin: #include #include int main(void) { int i, k; for (i = 0; i < 10; ++i) for (k = 0; k < 10; ++k) { printf("(%d, %d)\n", i, k); if (getch() == 'q') break; } return 0; } Yukarıdaki örnekte 'q' tuşuna basıldığında her iki döngüden de çıkılması isteniyorsa dış döngü için de ayrıca break kullanılmalıdır. Örneğin: #include #include int main(void) { int i, k; char ch; for (i = 0; i < 10; ++i) { for (k = 0; k < 10; ++k) { printf("(%d, %d)\n", i, k); if ((ch = getch()) == 'q') break; } if (ch == 'q') break; } return 0; } Sınıf Çalışması: Klavyeden int türden bir n değeri alınız ve aşağıdaki deseni çıkartınız: 72 * ** *** **** ***** .... ******....***** (n tane) Çözüm: #include int main(void) { int i, k, n; printf("Bir sayi giriniz:"); scanf("%d", &n); for (i = 1; i <= n; ++i) { for (k = 1; k <= i; ++k) putchar('*'); putchar('\n'); } return 0; } Sınıf Çalışması: Klavyeden int türden bir sayı okuyunuz ve onun asal çarpanlarını yan yana yazdırınız. Örneğin: Sayi giriniz: 28 2 2 7 İpucu: önce sürekli ikiye bölünerek ilerlenir. ikiye bölünmeyince bu sefer üçe bölünerek ilerlenir, bölünmediği zaman dörde bölünerek ilerlenir ve böyle devam ettirilir. Çözüm: #include int main(void) { int n, i; printf("Bir sayi giriniz:"); scanf("%d", &n); i = 2; while (n != 1) { if (n % i == 0) { printf("%d ", i); n /= i; } else ++i; } putchar('\n'); return 0; } 73 continue Deyimi continue deyimi break deyimine göre daha seyrek kullanılır. Genel biçimi şöyledir: continue; Programın akışı continue anahtar sözcüğünü gördüğünde döngünün içindeki deyim sonlandırılır. Böylece yeni bir yenilemeye geçilmiş olur. break döngü deyiminin kendisini sonlandırırken, continue döngü içerisindeki yinelenen deyimi sonlandırır. Örneğin: #include int main(void) { int i; for (i = 0; i < 10; ++i) { if (i % 2 == 0) continue; printf("%d\n", i); } return 0; } Burada ekrana yalnızca tek sayılar basılacaktır. continue deyimi yalnızca döngüler içerisinde kullanılabilir. Sabit İfadeleri (Constant Expressions) Yalnızca sabit ve operatörlerden oluşan ifadelere sabit ifadeleri (constant expressions) denilmektedir. Örneğin: 10 10 + 20 10 + 20 - 30 / 2 birer sabit ifadesidir. Aşağıdaki ifadeler sabit ifadesi değildir: x x + 10 10 - 20 - foo() Sabit ifadelerinin net sayısal değerleri derleme aşamasında belirlenebilir. C'de kimi durumlarda sabit ifadeleri zorunludur. Örneğin global değişkenlere verilen ilkdeğerlerin sabit ifadesi olması gerekir. Örneğin: int x = 10; int y = x + 10; void foo() { int a = 10; int b = a + 10; /* ... */ } /* geçerli */ /* geçersiz! */ /* geçerli, b yerel */ switch Deyimi switch deyimi bir ifadenin çeşitli sayısal değerleri için çeşitli farklı işlemlerin yapılması amacıyla kullanılır. Genel 74 biçimi şöyledir: switch () { case : .... case : .... case : .... [default: .... ] } switch anahtar sözcüğünden sonra derleyici parantezler içerisinde bir ifade bekler. switch deyimi case bölümlerinden oluşur. case anahtar sözcüğünü bir sabit ifadesi ve sonra da ':' atomu izlemek zorundadır. switch deyiminin default bölümü olabilir. switch deyiminin tamamı tek bir deyimdir. switch deyimi şöyle çalışır: Önce switch parantezinin içerisindeki değer hesaplanır. Sonra bu değere eşit olan bir case bölümü var mı diye bakılır. Eğer böyle bir case bölümü varsa akış o case bölümüne aktarılır. eğer böyle case bölümü yoksa fakat switch deyiminin default bölümü varsa akış default bölüme aktarılır. default bölüm de yoksa akış switch deyimine girdiği gibi çıkar. Akış bir case bölümüne aktarıldığında artık diğer case etiketlerinin bir önemi kalmaz. Akış aşağıya doğru switch deyiminin sonuna kadar akar. (Buna "fall through" denilmektedir.). break deyimi döngülerin yanı sıra switch deyimini de kırmak için kullanılmaktadır. Genellikle her case bölümü break ile sonlandırılır. Böylece yalnızca o case bölümü yapılmış olur. Örneğin: #include int main() { int a; printf("Bir sayi giriniz:"); scanf("%d", &a); switch (a) { case 1: printf("bir\n"); break; case 2: printf("iki\n"); break; case 3: printf("uc\n"); break; case 4: printf("dort\n"); break; default: printf("hicbiri\n"); break; } printf("son\n"); return 0; } case bölümlerinin sıralı olması ve default sonda olması zorunlu değildir. Bazen farklı case değerleri için aynı 75 işlemlerin yapılması istenebilir. Bunun tek yolu aşağıdaki gibidir. Daha pratik yolu yoktur: case 1: case 2: ifade; break; Örneğin: #include #include int main() { char ch; for (;;) { printf("CSD>"); ch = getch(); printf("%c\n", ch); if (ch == 'q') break; switch (ch) { case 'r': case 'd': printf("delete command\n"); break; case 'c': printf("copy command\n"); break; case 'm': printf("move command\n"); break; default: printf("bad command!\n"); break; } } return 0; } Aynı değere ilişkin birden fazla case bölümü bulunamaz. Örneğin: case 1 + 1: ... break; case 2: ... break; /* geçersiz! */ Zaten case ifadelerinin sabit ifadesi olma zorunluluğu buradan gelmektedir. Eğer case ifadeleri sabit ifadesi olmasaydı bu durum derleme aşamasında denetlenemezdi. case bölümlerine istenildiği kadar çok deyim yerleştirilebilir. İç içe switch deyimleri söz konusu olabilir. case bölümlerinin hemen switch bloğunun içerisinde bulundurulması zorunlu değildir. Yani case bölümleri daha diplerde de bulunabilir. switch (a) { case 1: 76 /*.... */ if (ifade) { case 2: /* .... */ } break; } switch parantezi içerisindeki ifade tamsayı türlerine ilişkin olmak zorundadır. case ifadeleri de gerçek sayı türünden sabit ifadesi olamaz. Tamsayı türlerine ilişkin olmak zorundadır. Sınıf Çalışması: Klavyeden gün, ay ve yıl için ü.ç değer isteyiniz. Ay bilgisi yazı ile olacak biçimde tarihi gün-ayyıl biçiminde yazdırınız. Örneğin: Gün:10 Ay: 12 Yil: 1994 10-Aralik-1994 Çözüm: #include int main() { int day, month, year; printf("Gun:"); scanf("%d", &day); printf("Ay:"); scanf("%d", &month); printf("Yil:"); scanf("%d", &year); printf("%02d-", day); switch (month) { case 1: printf("Ocak"); break; case 2: printf("Subat"); break; case 3: printf("Mart"); break; case 4: printf("Nisan"); break; case 5: printf("Mayis"); break; case 6: printf("Haziran"); break; case 7: printf("Temmuz"); break; case 8: printf("Agustos"); break; 77 case 9: printf("Eylul"); break; case 10: printf("Ekim"); break; case 11: printf("Kasim"); break; case 12: printf("Aralik"); break; } printf("-%02d\n", year); return 0; } Sayıların printf Fonksiyonuyla Formatlanması printf fonksiyonunda format karakterinden hemen önce bir sayı getirilirse, o sayı kadar alan ayrılır ve o alana sayı sağa dayılı olarak yazdırılır. Eğer o alana sayının sola dayalı olarak yazdırılması isteniyorsa sayının başına karakteri getirilir. Örneğin: #include int main() { double f = 12.3; int a = 123; printf("%-10d%f\n", a, f); return 0; } Eğer format karakterinden önceki sayı, yazdırılacak sayının basamk sayısından küçükse bu durumda o sayının bir önemi kalmaz. Sayının hepsi yazdırılır. Birtakım sayıların bıçakla kesilmiş gibi hizalanması için bu özellik kullanılmaktadır: #include int main() { int i; for (i = 1; i <= 100; ++i) printf("%-10d%d\n", i, i *i); return 0; } eğer format karakterinden önce + kullanılıyorsa (örneğin "%+10d" gibi, "%+d" gibi) sayının işareti her zaman yazdırlır. Örneğin: #include int main() { int i; 78 i = -123; printf("%+d\n", i); i = 123; printf("%+d\n", i); /* -123 */ /* +123 */ return 0; } Format karakterinden öncekki sayının başına 0 getirilirse (örneğin "%010d" gibi) geri kalan boş alan sıfırla doldurulur. Örneğin: day = 7; month = 6; year = 2009; printf("%02d/%02d/%04d", day, month, year); /* 07/06/2009 */ float ve double türleri default olarak noktadan sonra 6 basamak yazdırılır: #include int main() { double d; d = 3.6; printf("%f\n", d); d = 3; printf("%f\n", d); /* 3.600000 */ /* 3.000000 */ return 0; } printf fonksiyonunda "%n.kf " "toplam n tane alan ayır, noktadan sonra k basamak yazdır" anlamına gelir. Eğer sayının tam kısmı için yeterli alan belirtilmemişse sayının hepsi yazdırılır. Şüphesiz noktadan sonraki k basamak için yuvarlama yapılmaktadır. Eğer sayının tam kısmı ile ilgilenilmiyorsa "%.kf" biçiminde format belirtilebilir. Örneğin: printf("%.10f\n", f); /* sayının tam kısmının hepsini yazdır, fakat noktadan sonra 10 basamak yazdır */ Örneğin: #include int main() { double d; d = 12345.6789; printf("%10.2f\n", d); /* 12345.68 */ printf("%4.2f\n", d); /*12345.68 */ d = 12.78; printf("%.0f\n", d); return 0; } goto Deyimi 79 goto deyimi programın akışını belli bir noktaya koşulsuz olarak aktarmak amacıyla kullanılır. Genel biçimi şöyledi: goto ; .... : programın akışı goto anahtar sözcüğünü gördüğünde akış koşulsuz olarak etiket ile belirtilen noktaya aktarılır. Etiket (label) isimlendirme kuralına uygun herhangi bir isim olabilir. Bazı programcılar okunabilirlik için goto etiketlerini büyük harflerle harflendirirler. Eskiden ilk yüksek seviyeli diller makina dillerinin etkisi altındaydı. Bu dillerde neredeyse goto kullanmak zorunluydu. Sonraları goto'lar kodun takip edilebilirlğini bozması nedeniyle dillerden dışlanmaya başladılar. Pek çok deyim goto'suz bloklu bir tasarıma sahip oldu. Bugün goto deyiminin kullanımı birkaç durum dışında tavsiye edilmemektedir. Fakat tipik olarak goto kullanılmasının anlamlığı olduğu birkaç durum vardır. Aşağıdaki örnekte goto ile bir döngü oluşturulmuştur. Kesinlikte goto'lar döngü oluşturmak amacıyla kullanılmamalıdır. Bu nedenle aşağıdaki örnek goto kullanımının anlamlı olduğu bir durum için değil, onun çalışma mekanizmasını açıklamak için verilmiştir: #include int main() { int i = 0; REPEAT: printf("%d\n", i); ++i; if (i < 10) goto REPEAT; return 0; } goto etiketi yalnızca goto işlemi sırasında etkili olur. Onun dışında bu etiketin bir işlevi yoktur. goto etiketinin bulundurulması ona goto yapılmasını zorunla hale getirmez. (Ancak kendisine goto yapılmamış etiketler için derleyiciler uyarı verebilmektedir.) goto etiketlerini bir deyim izlemek zorundaıdr. Örneğin: void foo(void) { .... XX: /* geçersiz! */ } Fakat: void foo(void) { .... XX: /* geçerli */ ; } goto etiketleri fonksiyon faaliyet alanına sahiptir. Yani aynı fonksiyon içerisinde aynı isimli tek bir goto etiketi bulunabilir. 80 goto deyimi ile başka bir fonksiyona atlama yapılamaz. Aynı fonksiyon içerisinde başka bir bölgeye atlama yapılabilir. goto ile iç bir bloğa atlama yapılırken dikkat edilmelidir. Çünkü o bloktaki değişkenler için ilkdeğerleme işlemleri yapılmamış olabilir. Örneğin: if (ifade) goto XX; { int i = 10; XX: .... } Burada atlanan noktada i çöp değere sahip olabilir. Başka bir dyeişle C'de iç bloğa goto yapıldığında o iç bloğun başında bildirilmiş değişkenler çöp değerlerdedir. Örneğin aşağıdaki programı çalıştırdığımızda ekrana çöp değer basılacaktır: #include int main() { goto TEST; { int i = 10; TEST: printf("%d\n", i); } return 0; } Şüphesiz aynı etikete fonksiyonun farklı yerlerinden birden fazla kez goto yapılabilir. goto deyiminin aşağıdaki üç durumda kullanılması tavsiye edilmektedir: 1) İç içe döngülerden ya da döngü içerisindeki switch deyiminden tek hamlede çıkmak için. Örneğin: #include #include int main(void) { int i, k; for (i = 0; i < 10; ++i) { for (k = 0; k < 10; ++k) { printf("(%d, %d)\n", i, k); if (getch() == 'q') goto EXIT; } } EXIT: return 0; } Örneğin: 81 #include #include int main() { char ch; for (;;) { printf("CSD>"); ch = getch(); printf("%c\n", ch); switch (ch) { case 'r': case 'd': printf("delete command\n"); break; case 'c': printf("copy command\n"); break; case 'm': printf("move command\n"); break; case 'q': goto EXIT; default: printf("bad command!\n"); break; } } EXIT: return 0; } 2) goto ters sırada kaynak boşaltımı amacıyla kullanılabilir: 3) Bazı özel durumlarda goto kullanılmazsa algortima çok çetrefil hale gelebilir. Yani goto bazı durumlarda kodu kısaltmakta ve okunabilirliği artırmaktadır. Rastgele (Rassal) Sayı Üretimi Bilgisayarda rassal sayılar aritmetik yöntemlerle üretilirler. Bu biçimde üretilmiş rassal sayılara sahte rassal sayılar (pseudo random numbers) denilmektedir. Sahte rassal sayı üretmek için pek çok yöntem bulunuyor olsa da bunların hemen hepsi bir başlangıç sayısı alınıp, onun üzerinde belirle işlem yapıp yeni sayı elde edilmesi ve aynı işlemlerin bu yeni sayı üzerinde devam ettirilmesi biçiminde elde edilmektedir. Örneğin bir sayıdan başlayıp onun karesini alıp ortadaki n basamağını alırsa bu n basamak rassaldır. Örneğin ilk sayı 123 olsun 123 512 621 856 .... Bu biçimde elde edilen sayılar rassaldır. Tabi burada ilk değer aynı ise her defasında aynı dizilim bulunur. Programın ger çalışmasında farklı bir dizilimin elde edilmesi için bu ilkdeğerin her çalışmada farklı alınması gerekir. C'de rassal sayı üretimi için iki standart fonksiyon vardır: srand ve rand. Fonksiyonların parametrik yapıları şöyledir: 82 #include int rand(void); void srand(unsigned int seed); rand fonksiyonu her çağrıldığında 0 ile RAND_MAX değeri arasında rastgele bir sayıyla geri dönmektedir. RAND_MAX değerinin kaç olacağı derleyicileri yazanların isteğine bırakılmıştır. Bu değer her derleyicide farklı olabilir (pek çok derleyicide 32767 ya da 2147483647'dir.) Elde böyle bir fonksiyon varsa herhangi bir aralıkta rassal sayı elde edilebilir. Örneğin: #include #include int main() { int i, val; for (i = 0; i < 20; ++i) { val = rand() % 10; printf("%d ", val); } printf("\n"); return 0; } Bu program her çalıştıtıldığında aynı dizilimi bize verir. Aynı dizilim bize verilmesi sayıların rassal olmadığı anlamına gelmez. Tabi genellikle program her çalıştığında farklı bir dizilimin verilmesi istenir. srand fonksiyonu rassal sayı üreticisinin kullandığı ilkdeğeri değiştirmek amacıyla kullanılır. Bu değere terminolojide tohum değer (seed value) denilmektedir. Programın her çalışmasında farklı bir dizilimin elde edilmesi için programın her çalışmasında srand'ın farklı bir değerle çağrılması gerekir. İşte bu bilgisayarın içerisindeki saatle sağlanır. İleride ele alınacak olan time fonksiyonu bize bilgisayarın saatine bakarak bize 01/01/1970'ten fonksiyonun çağrıldığı zamana kadar kaç saniye geçtiğini verir. Bu fonksiyon için dosyasının include edilmesi gerekir. Fonksiyon sıfır parametresiyle çağrılmalıdır. Örneğin: #include #include #include int main() { int i, val; srand(time(0)); for (i = 0; i < 10; ++i) { val = rand() % 10; printf("%d ", val); } printf("\n"); return 0; } srand(time(0)) çağrısı programın başında yalnızca bir kez yapılmaktadır. Bazen programcılar yanlışlıkla bu çağrıyı da döngüsünün içerisine yerleştirirler. Bu durum çoğu kez aynı hep aynı değerin elde edilmesine yol açar. srand fonksiyonu hiç çağrılmazsa tohum değer hep aynı sayıdan başlar. Olasılığın görelilik tanımı (büyük sayılar yasası): Yazı tura işlemindeki limit 83 #include #include #include int main() { double head, tail, headRatio, tailRatio; unsigned long i; unsigned long n; srand(time(0)); head = tail = 0; n = 1000000000; for (i = 0; i < n; ++i) if (rand() % 2 == 0) ++head; else ++tail; headRatio = head / n; tailRatio = tail / n; printf("Head Ratio = %f, Tail Ratio = %f\n", headRatio, tailRatio); return 0; } Zarda 6 gelme olasılığı nedir? #include #include #include int main() { double six, sixRatio; unsigned long i; unsigned long n; srand(time(0)); six = 0; n = 1000000000; for (i = 0; i < n; ++i) if (rand() % 6 + 1 == 6) ++six; sixRatio = six / n; printf("Head Ratio = %f\n", sixRatio); return 0; } Sınıf Çalışması: Bir döngü içerisinde getch fonksiyonuyla karakter bekleyiniz. Her tuşa baısldığında "Ali", "Veli", "Selami", "Ayşe", "Fatma isimlerinden birini rastgele yazdırınız. 'q' tuşuna basıldığında program sonlansın. Çözüm: #include #include #include #include 84 int main() { srand(time(0)); while (getch() != 'q') { switch (rand() % 5) { case 0: printf("Ali\n"); break; case 1: printf("Veli\n"); break; case 2: printf("Selami\n"); break; case 3: printf("Ayse\n"); break; case 4: printf("Fatma\n"); break; } } return 0; } Sınıf Çalışması: Her tuşa basıldığında 16 büyük harf karakterden oluşan bir dizilimi ekrana bastırınız (Yalnızca İngilizce karakterler kullanılacaktır.). 'q' tuşuna basıldığında program sonlansın. GDSUSLYERVFXWTER KREYDSBSKAIEIRIT ... Çözüm: #include #include #include #include int main() { int i; srand(time(0)); while (getch() != 'q') { for (i = 0; i < 16; ++i) putchar('A' + rand() % 26); putchar('\n'); } return 0; } Farklı Türlerin Birbirlerine Atanması C'de tüm aritmektik türler birbirlerine atanabilir. Ancak atama işleminde bilgi kaybı söz konusu olabilir. Eğer bilgi kaybı söz konusu oluyorsa, bilginin nerisinin kaybedildiği stadartlarda belirtilmiştir. 85 Bir atamada kaynak ve hedef tür vardır. Örneğin: a = b; Burada kaynak tür b'nin türüdür, hedef tür a'nın türüdür. Standartlarda farklı türlerin birbirlerine atanması bir tür dönüştürme faaliyeti gibi ele alınmıştır. Standartlara göre atama işleminde kaynak türle hedef tür birbirlerinden farklıysa önce kaynak tür hedef türe dönüştürülür, sonra atama yapılır. Yani farklı türlerin birbirlerine atanması aslında farklı türlerin birbirlerine dönüştürülmesiyle aynı anlamdadır. Aşağıda farklı türlerin birbirlerine atanması sırasında ne olacağı tek tek açıklanmıştır (else-if gibi değerlendiriniz) 1) Eğer kaynak türün içerisindeki değer hedef türün sınırları içerisinde kalıyorsa bilgi kaybı söz konsu olmaz. Örneğin: long a = 10; short b; b = a; 2) Eğer kaynak tür bir tamsayı türünden (signed char, unsigned char, signed short, unsigned short, signed int, unsigned int, signed long, unsigned long) hedef tür de işaretsiz bir tamsayı türündense sayının yüksek anlamlı byte'ları atılır, düşük anlamlı byte'ları atanır. Örneğin: #include int main() { long a = 12345678; unsigned short b; b = a; printf("%u\n", b); /* 0xBC614E */ /* 24910 = 0x614E */ return 0; } 3) Eğer kaynak tür bir tamsayı türünden (signed char, unsigned char, signed short, unsigned short, signed int, unsigned int, signed long, unsigned long) hedef tür de işaretli bir tamsayı türündense bilgi kaybının nasıl olacağını (yani nerenin atılacağı) derleyiciyi yazanların isteğine bırakılmıştır (implementation dependent). Ancak derleyicilerin hemen hepsi bu durumda yine sayının yüksek anlamlı byte değerlerini atar. Örneğin: #include int main() { long a = 7654321; short b; b = a; printf("%d\n", b); /* 0x74CBB1 */ /* 0xCBB1 = -13391 */ return 0; } 4) Eğer kaynak ile hedef tür aynı tamsayı türünün işaretli ve işaretsiz biçimlerinden oluşuyorsa sayının bit kalıbı değişmez, yalnızca işaret bitinin anlamı değişir. Örneğin: #include 86 int main() { int a = -1; unsigned int b; b = a; printf("%u\n", b); /* 4294967295 */ return 0; } 5) Eğer kaynak tür küçük işaretli bir tamsayı türü hedef tür de büyük işaretsiz bir tamsayı türü ise bu durumda dönüştürme iki aşamada yürütülür. Önce küçük işaretli tür, 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 int main() { signed char a = -1; unsigned int b; b = a; printf("%u\n", b); /* 4294967295 */ return 0; } 6) Eğer kaynak tür bir gerçek sayı türündense (float, double, long double) hedef tür bir tamsayı türündense sayının noktadan sonraki kısmı atılır, tam kısmı elde edilir. Eğer sayının noktadan sonraki kısmı atıldıktan sonra elde edilen tam kısım hala hedef türün sınırları içerisinde kalmıyorsa tanımsız davranış (undefined behavior) oluşru. Örneğin: #include int main() { double a; int b; a = 3.99; b = a; printf("%d\n", b); /* 3 */ a = -3.99; b = a; printf("%d\n", b); /* -3 */ return 0; } 7) Eğer kaynak tür bir tamsayı türü hedef tür gerçek sayı türüyse ve sayı tam olarak tutulamıyorsa, kaynak tür ile belirtilen değere en yakın büyük ya da en yakın küçük değer elde edilir. Örneğin: #include int main() { long a; float b; 87 a = 123456789; b = a; printf("%.0f\n", b); /* 123456792 */ return 0; } 8) Eğer kaynak tür ve hedef tür de gerçek sayı türündense bu durumda bilgi kaybının niteliğine bakılır. eğer basamaksal bir kayıp (magnitute kaybı) varsa tanımsız davtranış oluşur. Basamksal değil de mantis kaybı varsa yine kaynak tür ile belirtilen değere en yakın büyük ya da en yakın küçük değer elde edilir. İşlem Öncesi Otomatik Tür Dönüştürmeleri Yalnızca değişkenlerin ve sabitlerin değil, her ifadenin de bir türü vardır. Bir operatörün opendaları farklı türlerdense önce derleyici onları aynı türe dönüştürür, sonra işlemi yapar. İşlem sonucunda elde edilen türü bu dönüştürülen ortak tür türündendir. İşlem öncesi otomatik tür dönüştürmesinin özet kuralı "küçük tür büyük türe dönüştürülür, sonuç büyük türünden çıkar" biçimindedir. Örneğin: int a; long b; ... a + b işleminde a long türüne dönüştürülür, ondan sonra toplama yapılır. a + b ifadesinin türü long olur. İşlem öncesi otomatik tür dönüştürmeleri geçici nesne yoluyla yapılmaktadır. Yani dönüştürülmek istenen değer önce dönüştürülecek türden geçici bir nesneye atanır, işleme o sokulur, sonra o nesne yok edilir: long temp = a; temp + b temp yok ediliyor İşlem öncesi otomatik tür dönüştürmelerinin bazı ayrıntıları vardır: 1) Eğer bölme işleminde iki operand da tamsayı türündense sonuç tamsayı türünden çıkar. Bu durumda bölme yapılır, sayının noktadan sonraki kısmı atılır. Örneğin: #include int main() { double a; a = 10 / 4; printf("%f\n", a); /* 2.0 */ return 0; } Fakat örneğin: #include int main() { double a; a = 10 / 4.; 88 printf("%f\n", a); /* 2.5 */ return 0; } 2. Eğer operandlardan her ikisi de int türünden küçükse (örneğin char-char, char-short, short-short gibi) Bu durumda ö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. Bu kurala "int türüne yükseltme kuralı (integral promotion)" denilmektedir. Örneğin: #include int main() { int a; short b = 30000; short c = 30000; a = b + c; /* 60000, sonuç int türden */ printf("%d\n", a); return 0; } Bu kuralın şöyle bir ayrıntısı da vardır: Eğer ilgili sistemde short ile int aynı uzunluktaysa ve operandlardan biri unsigned short türündense dönüştürme int türüne doğru değil unsigned int türüne doğru yapılır. 3) Operandlardan biri tamsayı türünden, diğeri gerçek sayı türündense dönüştürme her zaman gerçek sayı türüne doğru yapılır. Örneğin long ile float işleme sokulursa sonuç float türden çıkar. 4) Operandlar aynı tamsayı türünün işaretli ve işaretsiz biçimine ilişkinse dönüştürme her zaman işaretsiz türe doğru yapılır (örneğin int ile unsigned int işleme sokulsa sonuç unsigned int türünden çıkar.) İşlem öncesi tür dönüştürmeleri aşağıdaki gibi de açıklanabilir: 1. İşleme giren operandlardan en az biri gerçek sayı türlerindense (if-else if şeklinde düşünülmelidir): Eğer operandlardan biri long double türündense diğer operand long double türüne dönüştürülür. Eğer operandlardan bir tanesi double türündense diğer operand double türüne dönüştürülür. Eğer operandlardan bir tanesi float türündense diğer operand float türüne dönüştürülür. Bu tanımlamadan şu kural çıkartılabilir. İşleme giren operandlardan bir tanesi gerçek sayı türünden, diğeri tamsayı türünden ise tamsayı türünden operand o gerçek sayı türüne dönüştürülerek işlem yapılmaktadır. 2. İşleme giren operandlar tam sayı türlerindense: Operandlardan en az bir tanesi short int, unsigned short int, char, signed char, unsigned char türlerinden biri ise öncelikle değerler int türüne dönüştürülür. Buna tamsayıya yükseltme (integer/integral promotion) denilmektedir. Daha sonra algoritma şu şekildedir. Eğer operandlardan bir tanesi unsigned long türündense diğer operanda unsigned long türüne dönüştürülür. Eğer operandlardan bir tanesi signed long türündense diğeri long türüne dönüştürülür. Eğer operandlardan bir tanesi unsigned int türündense diğeri unsigned int türüne dönüştürülür. Tür Dönüştürme Operatörü Bazen bir değişkeni işleme sokarken onun sanki başka tür olarak işleme girmesini isteyebiliriz. Örneğin: 89 int a = 10, b = 4; double c; c = a / b; Burada kırpıolma olacak ve c'ye 2 değeri atanacaktır. Şüphesiz biz a ya da b'den en az birini double olarak bildirip sorunu çözebiliriz. Ancak a ve b'nin int olarak kalması da gerekiyor olabilir. İşte bunun için tür dönüştürme operatörü kullanılmaktadır. Tür dönüştürme operatörünün genel biçimi şöyledir: () operand Buadaki parantez öncelik parantezi değildir, opğeratör görevindedir. Tür dönüştürme operatörü tek operandlı önek bir operatördür. Öncelik tablosununun ikinci düzeyinde sağdan sola grupta bulunur: () 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: a = (long) b * c; İ1: (long)b İ2: İ1 * c İ3: a = İ2 Örneğin: a = (long)(a * b); İ1: a * b İ2: (long)İ1 İ3: a = İ2; Örneğin: #include int main() { int a = 10, b = 4; double c; c = (double)a / b; printf("%f\n", c); 90 return 0; } Örneğin: a = (double)(long) b * c; İ1: (long)b İ2:(double)İ1 İ3:İ2 * c İ4: a = İ3 Tür dönüştürmesi yine geçeici nesne yoluyla yapılmaktadır. Yani derleyici önce dönüştürülmek istenen tür türünden geçici bir nesne yaratır. Dönüştürülmek istenen ifadeyi oraya atar. Sonra işleme onu sokar. En sonunda da geçici nesneyi yok eder. Örneğin: #include int main() { int n; int i, val, total; double avg; printf("Kac sayi gireceksiniz:"); scanf("%d", &n); total = 0; for (i = 1; i <= n; ++i) { printf("%d. Sayiyi giriniz:", i); scanf("%d", &val); total += val; } avg = (double)total / n; /* dikkat! */ printf("Ortalama = %f\n", avg); return 0; } Nesnelerin Ömürleri Bir programda yaratılan nesnelerin hepsi program sonuna kadar bellekte kalmaz. Bazı nesneler programın çalışmasının belli bir aşamasında yaratılır, bir süre faaliyet gösterir, sonra yok edilir. Ömür (duration) bir nesnenin bellekte tutulduğu zaman aralığına denilmektedir. Bir nesne potansiyel en uzun ömür programın çalışma zamanı kadar (run time) olabilir. Bir nesnenin ömrü statik ya da dinamik olabilir. Statik ömür demek ömürün çalışma zamanına eşit olması demektir. Statik ömürlü nesneler program çalışmak üzere belleğe yüklendiğinde yaratılır, program sonlanana kadar bellekte kalır. Dinamik ömürlü nesnelerde ömür programın çalışma zamanından küçüktür. 91 Global Nesnelerin Ömürleri Global nesneler statik ömürlüdür. Yani bunlar programın çalışma zamanı boyunca bellekte yer kaplarlar. Global nesneler belleğin Data ve BSS denilen bölümlerinde tutulmaktadır. Yerel Değişkenlerin Ömürleri Yerel nesneler dinamik ömürlüdür. Programın akışı nesnenin bildirildiği bloğa girdiğinde o blokta bildirilmiş bütün yerel nesneler yaratılır, akış o bloktan çıktığında o blokta bildirilmiş bütün yerel nesneler otomatik yok edilir. Yerel değişkenler belleği "stack" denilen bir bölümünde yaratılmaktadır. Stack'te yaratım ve yokedim çok hızlı yapılır. Parametre Değişkenlerinin Ömürleri Parametre değişkenleri de dinamik ömürlüdür. Fonksiyon çağrıldığında yaratılırlar, fonksiyon çalıştığı sürece bellekte kalırlar. Fonksiyonun çalışması bittiğinde yok edilirler. Parametre değişkenleri de stack denilen bölümde yaratılmaktedır. Önişlemci Kavramı (Preprocessor) Aslında bir C derleyicisi iki modülden oluşamaktadır: "Önişlemci Modülü" ve "Derleme Modülü". Önişlemci modülü kaynak kodu alır, onun üzerinde bazı işlemler uygular. Sonra onu derleme modülüne verir. Asıl derleme işlemi derleme modülü tarafından gerçekleştirilmektedir. Pek çok programlama dilinde böyle bir önişlemci modülü yoktur. C'de '#' karakteri ile başlayan satırlar önişlemciye ilişkindir. Yani önişlemci yalnızca başı '#' ile başlayan satırlarla ilgilenmektedir. '#' atomunu önişlemci komutu denilen bir anahtar sözcük izler. '#' ile bu anahtar sözcük arasında boşluk karakterleriş bırakılabilir. Fakat önişlemci komutları tekbir satırda bulunmak zorundadır. Önişlemci komutu önişlemciye ne yapması gerektiğini anlatır. 20'ye yakın önişlemci komutu vardır. Ancak en çok kullanılanı #include ve #define komutlarıdır. Kursumuzun bu bölümünde bu iki komut ele alınacaktır. Diğerleri kursumuzun sonlarına doğru ele alınacaktır. #include Komutu #include komutun açısal parantezler içerisinde ya da iki tırnak içerisinde bir dosya ismi izlemek zorundadır. Örneğin: 92 #include #include "a.c" #include komutu kaynak kodun tepesinde bulunmak zorunda değildir. Herhangi bir yerd ebulunabilir. Fakat bir satırı sadece kendisi kaplamak zorundadır. Önişlemci #include komutunu gördüğünde dosyayı diskten okur ve onun içindekilerini komutun yazılı olduğu yere yapıştırır. Tabi bunu geçici bir dosya açarak yapmaktadır. Derleme modülüne de bu geçici dosyayı verir. Derleme modülü kodu aldığında artık orada #include görmez. Onun içeriğini görür. #include komutu ile include ettiğimiz dosyanın içerisinde C'ce anlamlı şeyler olmalıdır. Örneğin: /* test.c */ int a; /* sample.c */ #include int main() { #include "test.c" a = 10; printf("%d\n", a); return 0; } Eğer dosya açısal parantezler içerisinde include edilirse bu durumda dosya derleyicinin belirlediği bir dizinde aranır. Derleyici install edilirken başlık dosyaları bir dizine çekilmektedir. Açısal ile dosya ismi belirtilirken o dizine bakılır. C'nin standart başlık dosyalarının açısal parantzeler içerisinde include edilmesi uygundur. eğer dosya ismi iki tırnak içerisnde belirtilirse derleyiciler onu önce bulunulan dizinde (ya da projenin yüklü dizinde) arar eğer orada bulamazlarsa sani açısal parantez ile belirtilmiş gibi derleyicinin belirlediği dizine de bakılır. Bu durumda bizim kendi dosyalarımız iki tırnak içerisinde include etmemiz daha uygundur. Çünkü onlar muhtemelen kendi dizinimizde bulunacaktır. C standartları #include komutunun semantiğini ana hatlarıyla belirtip çoğu şeyi derleyicileri yazanların isteğine bırakmıştır. Yani dosya ismi açısal parantez ile belirtildiğinde nerelere bakılacağı, iki tırnak içerisinde belirtildiğinde nerelere bakılacağı hep derleyicileri yazanların isteğine bırakılmıştır. Pek çok derleyicide include dosyaları aranırken programcının istediği dizinlere de bakılması sağlanmıştır. Örneğin Visual Studio IDE'sinde Projeye ayarlarında "C+C++/General/Additional Include Directories" seçenekleri ile önişlemcinin bizim belirlediğimiz dizilere de bakması sağlanabilir. gcc derleyicilerinde -I seçeneği ile dizin eklenebilmektedir. Örneğin: gcc -I /home/csd/Study/C -o sample sample.c include edilen bir dosyanın içerisinde de #include komutları bulunabilir. Önişlemci bunların hepsini düzgün biçimde açabilmektedir. Örneğin biz pek çok dosyayı "myheaders.h" dosyası içerisinde include etmiş olalım: /* myheaders.h */ #include #include #include Asıl programımızda yalnızca "myheaders.h" dosyasını include edebiliriz: 93 /* sample.c */ #include "myheaders.h" ... Fakat include işlemlerinde döngüsellik olamaz. include edilecek dosyanın ismi ve uzuntası herhangi bir biçimde olabilir. Ancak C'de gelenek uzantının .h biçiminde olmasıdır. #define Komutu #define komutu text editörlerdeki "find and replace" işlemine benzer bir işlem yapmaktadır. Komutun genel biçimi şöyledir: #define STR1 STR2 Örneğin: #define MAX #define MIN 100 100 - 20 #define anahtar sözcüğünden sonra boşluk karakterleri atılır ve ilk boşluksuz yazı kümesi elde edilir. Buna STR1 diyelim. Sonra önişlemci yine boşlukları atar ve satır sonuna kadarki tüm karakterleri elde eder. Buna da STR2 diyelim. Sonra kaynak kodda STR1 gördüğü yere STR2 yazısını yerleştirir. Sonucu derleme modülüne verir. Örneğin: #include #define MAX 100 - 50 int main() { int a; a = MAX * 2; printf("%d\n", a); return 0; } Burada #define komutunda STR1 MAX, STR2 de 100 - 50'dir. Önişlemci MAX atomlarını 100 - 50 ile yer değiştirir. Derleme modülüne kod aşağıdaki gibi verilir: ...stdio.h içeriği... int main() { int a; a = 100 - 50 * 2; printf("%d\n", a); return 0; } Dolayısıyla bu program çalıştırıldığında ekrana 0 basılır. Önişlemci hesp yapmaz. Bir yazıyı başka bir yazıyla yer değiştirir. Örneğin: 94 #include #define MAX (100 - 50) int main() { int a; a = MAX * 2; printf("%d\n", a); return 0; } Burada ekrana 100 basılacaktır. STR1 olarak yalnızca değişken ve anahtar sözcük atom kullanılır. Örneğin: #define + #define 100 #define beep() #define MAX 200 putchar('\a') + /* geçerli değil */ /* geçerli değil */ /* geçerli */ /* geçerli */ Aşağıdaki kod tamamen geçerlidir: #include #define ana main #define tam int #define eger if #define yazf printf #define geridon return tam ana() { tam a = 100; eger(a == 100) yazf("tamam\n"); geridon 0; } Bir yazıyıya karşılık bir sayı karşı düşürülmesi durumunda bu yazıya sembolik sabit (symbolic constans) ya da makro (macro) denilmektedir. Örneğin: #define MAX 100 Buarada MAX bir sembolik sabittir. STR2'de başka makrolar bulundurulabilir. Önişlemci açımı özyinelemeli yapar. Yani açtığı makroyu yeniden açar. Ta ki açılacak bi,rşey kalmayana kadar. Örneğin: #include #define MAX #define MIN 100 (MAX - 50) int main() { int a; 95 a = MIN; printf("%d\n", a); return 0; } Burada MIN yerine önce (MAX - 50) yerleştirilir. Sonra bu da yeniden önişlemciye sokulur. Böylece (100 - 50) elde edilir. Burada #define'ların sırası farklı olsaydı da değişen birşey olmayacaktı. Yani önişlemci #define satırları üzerinde değişiklik yazpmaz. #define önişlemci komutu ile iki tırnak içerisindeki yazılarda değişiklik yapamayız. Örneğin: #include #define MAX 100 int main() { printf("MAX"); /* MAX çıkar */ return 0; } Örneğin: #include #define CITY int main() { printf(CITY); "Ankara" /* Ankara çıkar */ return 0; } Aşağıdaki makro geçerlidir: #define MYHEADER #include MYHEADER #define komutu #include komutunda değişiklik yapabilmektedir. Eğer STR2 yazılmazsa bu durumda STR1 görülen yere boşluk yerleştirilir. Yani STR1 silinir. Örneğin aşağıdaki kodda derleme sorunu oluşmaz: #include #define in int main() { in in in in in return 0; } #define komutu nereye yerleştirilmişse onun aşağısında etkili olur. Örneğin: 96 #include int main() { int i; for (i = 0; i < SIZE; ++i) { /* ... */ } #define SIZE /* geçersiz! */ 100 for (i = 0; i < SIZE; ++i) { /* ... */ } return 0; } #define komutunda yerel, global gibi bir kavram yoktur. Komut nereye yazılmışsa dosyanın sonuna kadarki bölgede geçerli olur. Önişlemci #include komutu ile bir başlık dosyasını açtığında o dosyanın içini de önişleme sokar. Yani oradaki #define'lar, #include'lar da etki gösterir. Böylece biz birtakım sembolik sabit tanımlamalarını bir başlık dosyasında yapabiliriz. Onu include ettiğimizde o makrolar etkili olur. C standartlarına göre bir makro sabit ikinci kez farklı bir biçimde define edilirse tanımsız davranış oluşur. Örneğin: #define MAX ... #define MAX 100 200 Burada pek çok derleyici ikinci komuta kadar birincisini etkin yapar, ikincikomuttan sonra ikincisinin etkin olduğu düşünür. Fakat aslında derleyicinin burada ne yapacağı belli değildir. Bundan kaçınmak gerekir. Tabi makronun aynı biçimde birden fazla kez tanımlanması soruna yol açmaz. Ayrıca #define komutunun parametreli bir kullanımı da vardır. Bu konı kursumuzun sonlarına doğru ele alınacaktır. Sembolik sabitler geleneksel olarak büyük harflerle isimlendirilmektedir. Bu onların program içerisinde ayrımsanmasını kolaylaştırmaktadır. #define Komutuna Neden Gereksinim Duyulmaktadır? #define komutu kaynak kod üzerinde bazı değişiklikler yapılmasına olanak sağlar. Fakat kurusumuzun bu noktasında bu tür değişikliklerden nasıl fayda sağlanacağı açıklanmayacaktır. #define komutu okunabilirliği artırmak için sıkça kullanılır. Örneğin bir personel takip programında aşağdaki gibi bir satır bıuşunuyor olsun: if (n > 732) { ... } Bu mu daha onunabilirdir (readable), yoksa aşağıdaki mi? #define PERSONEL_SAYISI ... 732 if (n > PERSONEL_SAYISI) { ... 97 } İşte programımızda çeşitli sayıları yazısal biçimde ifade edersek okunabilirliği artırmış oluruz. #define komutu kod üzerinde değişiklikleri daha az zahmetle yapmamızı sağlar. Örneğin bir programın pek çok yerinde 100 sayısı kullanılıyor olsun. Biz bunu 200 ile yer değiştirmek isteyelim. Eğer 100'ü define edersek bunu tek yerden yapabiliriz: #include #define SIZE 100 int main() { int i; for (i = 0; i < SIZE; ++i) { /* ... */ } for (i = 0; i < SIZE; ++i) { /* ... */ } /* ... */ return 0; } Fonksiyon Prototipleri Derleme işleminin bir yönü vardır ve bu yön yukarıdan aşağıya doğrudur. Derleyici çağrılan bir fonksiyonu gördüğünde çağrılama noktasına kadar bu fonksiyonun geri dönüş değerinin türünü tespit etmek zorundadır. Eğer çağrılan fonksiyon çağıran fonksiyonun daha yukarısında tanımlanmışsa çağrılam noktasına kadar derleyici bu tespit yapmış olur. Fakat eğer çağrılan fonksiyon çağıran fonksiyonun daha altında tanımlanmışsa bu durumda derleyici çağrılma noktasına kadar fonksiyonun geri dönüş değerinin türünü tespit edememiştir. Eğer derleyici çağrılma noktasına kadar fonksiyonun geri dönüş değerinin türünü tespit edememişse bu durumda fonksiyonun int geri dönüş değerine sahip olduğunu varsayarak kodu üretir. Eğer fonksiyonunun daha sonra başka bir geri dönüş değerine sahip olduğunu derleyici görürse geri dönüp ürettiği kodu düzeltmez. Bu durum error oluşturur. Fakat derleyici daha sonra fonksiyonun int geri dönüş değerine sahip olarak tanımlandığı görürse sorun oluşmaz. Örneğin: #include int main() { foo(); /* geçersiz */ return 0; } void foo(void) { /* ... */ } Fakat örneğin: 98 #include int main() { foo(); /* geçerli */ return 0; } int foo(void) { /* ... */ } Örneğin: #include int main() { double result; result = div(10, 2.5); /* error! */ return 0; } double div(double a, double b) { return a / b; } İşte derleyiciye bir fonksiyonun geri dönüş değeri ve parametreleri hakkında bilgi veren bildirimlere "fonksiyon prototipleri" denilmektedir. Eğer çağrılan fonksiyon çağıran fonksiyonun daha aşağısında tanımlanmışsa biz çağırma işleminin yukarısına fonksiyon prototipini yerleştirmeliyiz. Prototip bildiriminin genel biçimi şöyledir: [geri dönüş değerinin türü] ([parametre bildirimi]); Örneğin: double divide(); double divide(double a, double b); divide(void); geçerli prototip bildirimleridir. Prototipte parametre bildirimi yapmak iyi bir tekniktir. Çünkü derleyici bu durumda çağrılma ifadesinde parametre kontrolü ve uygun tür dönüştürmesini yapar. Prototip bildiriminde parametre değişkenlerinin yalnızca türleri belirtilebilir. Örneğin: double divide(double, double); Fakat parametre değişkenlerinin isimlerini yazmak okunabilirliği artırır. Örneğin aşağıdakilerden hangisi daha okunabilirdir? double usal(double, double); double usal(double taban, double us); Parametre değişkenlerinin isimlerinin hiçbir önemi yoktur. Hatta istenirse bazı parametre değişkenlerine isim verili bazılarına verilmeyebilir. Tabi iyi teknik tüm parametre değişkenlkerinin isimlendirilmesidir. 99 Protoipteki geri dönüş değeri ve parametre türleriyle tanımlamadakilerin tam olarak aynı olması gerekir. Aksi halde kod geçerli olmaz. Örneğin: #include double divide(float x, double y); int main() { double result; result = divide(10, 2.5); printf("%f\n", result); return 0; } double divide(double a, double b) { return a / b; } /* geçersiz! */ Fakat parametre değişkenlerinin isimlerinin aynı olması zorunluluğu yoktur. Fonksiyon tanımlamasının ilk satırı alınıp kopyalanır ve sonuna noktalı virgül getirilirse prototip elde edilir. Örneğin: double divide(double a, double b) { return a / b; } Bu fonksiyonun prototipi şöyledir: double divide(double a, double b); Aynı protipi birden fazla kez bildirmek sorun oluşturmaz. Örneğin: double div(double, double); double div(double a, double b); double div(); Bunların hepsi bir arada bulunabilir. Küçük olmayan projelerde ve kodlarda fonksiyonlar nerede tanımlamış olursa olsun onların prototiplerini kaynak kodun tepesine yerleştirmek ya da bir başlık dosyasına yerleştirip onu include etmek iyi bir tekniktir. Eğer biz fonkisyonun prototini ya da tanımlamasını daha yukarıda oluşturmuşsak bu durumda çağrılma ifadesinde derleyici argümanları sayıca ve türce kontrol eder. Örneğin: #include void foo(int a, int b); int main() { foo(100); /* geçersiz! */ return 0; } 100 void foo(int a, int b) { /* ... */ } Prototip bir bildirimdir, tanımlama değildir. Yani prototip oluşturduğunuzda biz fonksiyonu yazmış (tanımlamış) olmayız. Yalnızca derleyiciye bir ön bildirimde bulunmuş oluruz. Prototip bildiriminde parametre parantezinini boş bırakılmasıyla oraya void yazılması farklı anlamlara gelmektedir. eğer parametre parantezi boş bırakılırsa bu durum derleyicinin çağrılam ifadelerinde argüman kontrolü yapmayacağı anlamına gelir. Örneğin: void foo(); Böyle bir bildirimde biz foo'yu istediğimiz kadar çok argümanla çağırabiliriz. Derleyici bir kontrol uygulamaz. Ancak parametre parantezinin içine void yazılırsa bu durum fonksiyonun parametre almadığı anlamına gelir. Örneğin: void foo(void); Biz artık foo'yu argümanla çağıramayız. Anahtar Notlar: Aslınd abu durum un tarihsel bazı gerekçeleri vardır. Eskiden C'de fonksiyon prototipleri yoktu. Zaten parametre parantezlerinin içi boş bırakılmak zorundydı. Sonra C'ye prototi kavramı eklendiği zaman eski kodlar geçerli olsun diye bu semantik muhafaza edildi. Eskiden yapılan parametre parantezinin içinin boş bırakıldığı bildirimlere prototip değil "fonklsiyon bildirimi" deniliyordu. Fonksiyon tanımlamasında parametre parantezinin içinin boş bırakılmasıyla void yazılması arasında bir farklılık yoktur. Her iki durum da fonksiyonun parametreye sahip olmadığı anlamına gelir. Yani: void foo() { ... } ile, void foo(void) { ... } tanımlamaları tamamen eşdeğerdir. Fonksiyon prototip bildirimeleri nerede yapılamlıdır? Protip bildirimleri yerel ya da global düzeyde yapılabilir. Yine eğer yerel düzeyde yapılacaksa C90'da blokların başlarında yapılması zorunludur (Tabi C99 ve C++'ta blokların herhangi bir yerinde yapılabilir.) Eğer prototip bildirimi global düzeyde yapılırsa tüm dosya bundan etkilenir. Yerel düzeyde yapılrsa yalnızca o bloktaki kullanım bundan etkilenir. Yani başka bloklarda sanki bildirim hiç yapılmamış gibi etki gösterir. Örneğin: #include void bar(void) { void foo(int a, int b); 101 foo(10, 20); /* prototip etki gösterir */ } int main() { foo(10, 20); /* geçersiz! sanki protip yokmuş gibi etki gösterir*/ return 0; } void foo(int a, int b) { /* ... */ } Fakat hemen her zaman fonksiyon prototip bildirimi glob al düzeyde yapılmaktadır. Derleyici bir fonksiyonun prototipini ya da tanımlamasını görmeden onun çağrıldığı görürse sanki onun prototipi aşağıdaki gibi yazılmış varsayar: int some_function(); Bir fonksiyonun prototip bildirimin yaılmış olması onu çağırmayı zorunlu hale getirmez. Standart C Fonksiyonlarının Prototipleri Kaynak programımızda olmayan bir fonksiyonu çağırsak bile derleme aşamasından başarıyla geçilir. Eğer prototip yoksa derleyici fonksiyonun geri dönüş değerinin türünü int varsayar. Tabi bu durumda argüman kontrolü yapmaz. Eğer prototip varsa derleyici geri dönüş değerinin türünü anlar. Her iki durumda da derleme işlemi başarıyla geçilir. Fakat derleyici object kod içerisine linker için bir mesaj yazar. Mesajda "sevgili linker, ben bu fonksiyonu bulamadım. Sen onu diğer objectg modüllerde ve kütüphane dosyalarında ara. Bulursan oradan al. Bulamazsan seninde elinden birşey gelmez. Ne yapalım...". Bu mesajı okuyan linker fonksiyonu diğer modüllerde ve kütüphanelerde arar. Aslında link işlemi tek bir object modülle yapılmak zorunda değildir. Birden fazla object modül ve kütüphane dosyaları birlikte link edilip tek bir exe dosya elde edilebilir. Uzantısı Windows'ta .lib ve .dll olan, UNIX/Linux sistemlerinde .a ve .so olan dosyalara kütüphane dosyaları denir. Kütüphaneler statik ve dinamik olmak üzere ikiye ayrılmaktadır. Uzantısı Windows'ta .lib, UNIX/Linux'ta .a olan dosyalara statik kütüphane dosyaları, uzantısı Windows'ta .dll, UNIX/Linux'ta .so olan dosyalara dinamik kütüphane dosyaları denilmektedir. Kütüphanelerin içerisinde derlenmiş halde fonksiyonlar bulunmaktadır. Eğer bir fonksiyon linker tarafından statik kütüphanelerde bulunursa oradan çekilir ve .exe dosyanın içerisine yazılır. Eğer fonksiyon dinamik kütüphane içerisinde bulunursa linker fonksiyonu oradan çekip exe dosyaya yazmaz. Linker exe dosya içerisine işletim sistemi için şöyle bir nor yazmaktadır: "Sevgili işletim sistemi bu programı çalıştırırken bu programın kullandığı dinamik kütüphaneleri de bul onları da belleğe yükle ki benim 102 fonksiyonlarım çalışsın". Görüldüğü gibi statik link işleminde exe dosya zaten her şeyi içermektedir. Programı artık başka bir makinaya götürürken statik kütüphane dosyasını götürmememize gerek yoktur. Ancak dinamik link işleminde biz exe dosyayla birlikte onun kullandığı dinamik kütüphane dosyalarını da hedef makinaya taşımamaız gerekir. C'nin standart fonksiyonları da kütüphaneler içerisinde derlenmiş bir biçimde bulunmaktadır. Dolayısıyla bunlar linker tarafından ele alınırlar. C derleyicileri standart C fonksiyonlarının farkında değildir. Yani printf fonksiyonuna bizim foo fonksiyonuna yaptığından farklı bir muamele uygulamaz. Biz printf fonksiyonunu çağırdığımızda derleyici onu kaynak dosyada bulamayacağı için linker'a havale eder. Linker da onu baktığı kütüphane dosyalarında bulur. Aslında linker de standart C fonksiyonlarının farkında değildir. O yalnızca belirlenen kütüphanelere bakmaktadır. Standart C fonksiyonlarının yalnızca biz programcılar standart olduğunun farkındayızdır. Tabi onların kütüphane içerinde bulunduğundan emin oluruz. Standart C fonksiyonlarını derleyici tanımadığına göre onların geri dönüş değerlerinin türlerini ve parametrik yapılarını bilmez. Onlar çağrıldığında onların prototipini görmezse geri dönüş değerlerini int kabul eder. O halde standart C fonksiyonarının da prototiplerinin bulundurulması gerekir. Örneğin aşağıdaki programda sqrt fonksiyonun prototipi bulundurulmadığı için program başarılı derlenip link edilmiştir. Fakat yaznlış çalışır: #include int main() { double result; result = sqrt(10); printf("%f\n", result); return 0; } Halbuki prototip eklersek doğru çalışacaktır: #include double sqrt(double); int main() { double result; result = sqrt(10); printf("%f\n", result); return 0; } Buradan çıkan sonuç "standart fonksiyonlarının da prototiplerinin bulundurulması" gerektiğidir. İşte standart C fonksiyonlarının prototipleri gruplanarak çeşitli başlık dosyalarına yerleştirilmiştir. Biz onları elle yazmak yerine o dosyaları include edebiliriz. Örneğin bütün matematiksel fonksiyonların prototipleri , bütün klavye, ekran ve dosya fonksiyonlarının prototipleri , genel amaçlı fonksiyonların prototipleri dosyası içerisindedir. Başlık dosyaalarında fonksiyonların tanımlamaları yoktur. Yalnızca prototipleri vardır. Fonksiyonlar derlenmiş bir biçimde kütüphanede bulunmaktadır. Adres Kavramı CPU'nun doğrudan bağlı olduğu ana bellek (yani RAM) byte'lardan oluşmaktadır. Her byte da 8 bitten oluşur. Belleğin her bir byte'ına ilk byte 0 olmak üzere bir numara verilmiştir. Buna ilgili byte'ın fiziksel adresi denir. 103 Bayte'ların fiziksel adresleri geleneksel olarak 16'lık sistemde gösterilmektedir. Örneğin: Bellketeki nesnelerin de adresleri vardır. Çünkü onlar da yaşam süresi içerisinde RAM'de bir yerlerde bulunmaktadır. Örneğin: Her nesnenin bir fiziksel adresi vardır. Bir byte'tan büyük olan nesnelerin fiziksel adresleri onların en düşük adres numaralarıyla belirtilir. Örneğin b'nin fiziksel adresi 001B0101'dir, c'nin 001B00103'tür. Şüphesiz yerel nesneler dinamik ömürlü olduğu için, yerel nesneler için ayrılan alan onların ömürleri bittiğinde artık başka yerel nesneler için kullanılabilir. Yazılımsal adres bilgisi iki bileşnli bir bilgidir. Adresin sayısal ve tür bileşenleri vardır: Adresin sayısal bileşeni nesnenin bellekteki fiziksel adres numarasını belirtir. Tür bileşeni ise o fiziksel adresteki nesnenin türünü ifade eder. Örneğib: 104 Bundan sonra kursumuzda "adres" denildiğinde yazılımsal adres kavramı anlaşılmalıdır. (Donanımsal ya da fiziksel adres kavramı tek bileşenlidir ve yalnızca sayı belirtir.) C'de adresler tamamen ayrı bir tür belirtmektedir. Nasıl int, long, double gibi türler varsa adresleri tutabilen türler de vardır. C'de adres türleri (pointer types) adresin tür bileşeni ile ifade edilir. Adres türleri C'de adresin tür bileşeni ile ifade edilir. Yani örneğin bu türe "adres" denmez de "int türden adres (pointer to int)", "long türden adres" denir. C'de nasıl int türden, long türden, double türden sabit kavramı varsa adres sabiti kavramı da vardır. T türünden adres sabiti tür dönüştürme operatörü ile oluşturulur. Genel biçimi şöyledir: ( *) Adresin sayısal bileşeni fiziksel adres numarası belirtir. Tür bileşeni ise o fiziksel adresteki bilginin türünü belirtmektedir. Örneğin: (int *)0x001B1010 Bu ifade int türden adres sabiti belirtir. Örneğin: (double *)0x001C2000 Bu ifade double türden adres sabiti anlamına gelir. Genel biçimdeki yıldız adres anlamına gelir. Adres sabitlerini yazarken tür bileşeninin 16'lık sistemde belirtilmesi zorunlu değildir. Fakat gelenek bu yöndedir. C'de nasıl int türünü tutan, long türünü tutan, double türünü tutan nesneler bildirilebiliyorsa adres bilgilerini tutan da nesneler bildirilebilir. Bunlara gösterici (pointer) denir. Diziler (Arrays) Elemanları aynı türden olan ve bellekte ardışıl bir biçimde bulunan veri yapılarına dizi denir (bu tanım ezberlenmelidir). Aralarında fiziksel ya da mantıksal ilişki bulunan birden fazla nesneni oluşturduğu topluluğa veri yapısı (data structure) denilmektedir. Dizi bir veri yapısıdır. Çünkü dizi dediğimizde birden fazla eleman (özel olarak bir eleman da olabilir) söz konusu edilir. Dizi elemanlarının hepsi aynı türdendir. Dizi elemanları bellekte ardışıl bir biçimde tutulur. Yani bir elelmandan sonra hiç boşluk olmadan diğer elemana geçilir. Halbuki örneğin: int a, b; 105 gibi bir bildirimde a ve b'nin bellekte ardışıl tutulacağının bir garantisi yoktur. Dizi bildiriminin genel biçimi şöyledir: <[]>; Örneğin: int a[10]; long b[20]; char c[5]; gibi. Küme parantezi içerisindeki uzunluk belirten ifadenin tamsayı türlerine ilişkin sabit ifadesi olması zorunludur. Örneğin: int a[3 + 2]; int n = 5; int b[n]; /* geçerli */ /* geçerli değil */ Derleyicinin dizi uzunluğunu daha derleme aşamasında bilmesi gerekir. Bu nedenle dizi unluklarının sabit ifadesi olması zorunlu tutulmuştur. Bir dizinin ismi o dizinin tamamını temsil eder. Örneğin: int a[10]; bildiriminde a 10 elemanlı bir dizi nesnesidir. Dizi türleri sembolik olarak önce tür sonra bir köşeli parantez içerisinde uzunluk ifadesiyle temsil edilmektedir. Örneğin yukarıda a'nın türü int[10] biçiminde ifade edilir. int[10] "10 elemanlı int türden dizi" anlamına gelir. Örneğin: char s[20]; Burada s, char[20] türündendir. Dizi bildirimi ile normal nesne bildirimi birlikte yapılabilir. Örneğin: int a[10], b, c[5]; Bu bildirimin eşdeğeri şöyledir: int a[10]; int b; int c[5]; Dizi Elemanlarına Erişim ve [] Operatörü Dizi elemanlarına [] operatörüyle erişilir. Köşeli parantezlerin içerisine tamsayı türlerine ilişkin bir indeks ifadesi yazılmak zorundadır. Bu ifade sabit ifadesi olmak zorunda değildir. Dizinin ilk elemanı sıfırıncı ideks'li elemanıdır. Bu durumda son eleman (n elemanlı bir dizide) n - 1'inci indeksteki eleman olur. Örneğin: int a[10]; a[3] ifadesi int türdendir. a ifadesi ise int[10] türündendir. 106 Örneğin: #include int main(void) { int a[10]; int i; for (i = 0; i < 10; ++i) a[i] = i * i; for (i = 0; i < 10; ++i) printf("%d ", a[i]); printf("\n"); return 0; } Dizi Elemanlarına İlkdeğer Verilmesi Bir dizinin elemanlarına değer atanmamışsa elemanların içerisinde ne vardır? Dizi yerel ise dizinin tüm elemanlarında çöp değer, globalsa sıfır bulunur. Dizi elemanlarına ilkdeğer küme parantezleri ile verilir. Küme parantezlerinin içerisinde değerler ',' atomu ile ayrılarak yazılır. Örneğin: int a[3] = {1, 2, 3}; Verilen ilkdeğerler dizi elemanlarına sırasıyla yerleştirilmektedir: Dizinin uzunluğundan fazla elemanına değer veremeyiz. Örneğin: int a[3] = {1, 2, 3, 4}; /* geçersiz! */ Fakat dizinin az sayıda elemanına ilkdeğer verebiliriz. Bu durumda geri kalan elemanlar dizi ister yerel yerel olsun, ister global olsun derleyici tarafından sıfırlanır. Örneğin: int a[5] = {1, 2 }; /* Dizinin 2, 3 ve 4'üncü indeksli elemanları sıfırdır */ İlkdeğer verirken küme parantezlerinin içi boş bırakılamaz. Örneğin: int a[] = {}; Örneğin yerel bir dizinin tüm elemanlarını sıfırlamanın en kolay şöyledir: int a[10] = {0}; 107 C'de (C90 ve C99 ve C11'de) küme parantezleri içerisinde verilenm ilkdeğerlerin sabit ifadesi olması zorunludur (ancak C++'ta sabit ifadesi olmayabilir). Örneğin: int n = 10; int a[3] = {5, n, 15}; /* geçersiz! */ Bazı derleyiciler bir eklenti olarak küme parantezleri içerisinde sabit ifadesi olmayan ilkdeğer ifadelerini kabul edebilmektedir. Fakat bu standart bir özellik değildir. Diziye ilkdeğer verilirken küme parantezlerinin içi boş bırakılabilir. Bu durumda derleyici verilen ilkdeğerleri sayar ve dizinin o uzunlukta açılmış olduğunu kabul eder. Örneğin: int a[] = {1, 2, 3}; /* geçerli */ Burada a dizisi 3 elemanlıdır. Tabi diziye ilkdeğer verilmiyorsa uzunluk belirtmek zorunludur. Örneğin: int a[]; /* geçersiz! */ C'de virgüllerle ayrılmış listelerde genel olarak (ileride başka konularda da karşılaşılacak) son elemandna sonra ',' atomu bulundurulabilir. Bu yasak değildir. Örneğin: int a[3] = {1, 2, 3, }; int b[] = {1, 2, 3, }; /* geçerli */ /* geçerli */ C90'da dizinin istediğimiz bazı elemanlarına ilkdeğer veremeyiz. Böyle bir sentaks yoktur. Ancak C99'da bu aşağıdaki sentaksla mümkündür: int a[10] = {10, [4] = 100, 200, [9] = 300}; C99'a göre burada dizinin sıfırıncı indeksli elemanında 10, 4'üncü indeksil elemanında 100, 5',nci indeksli elemanında 200 ve 9'uncu indeksli elemanında 300 vardır. Diğer elemanlarda sıfır bulunur. Bu özellik bazı C90 derleyicilerinde bir eklenti (extension) olarak desteklenmektedir. Bir Dizi İçerisindeki En Büyük (ya da En Küçük) elemanın Bulunması Algoritması Bu problemin tek bir algoritmik çözümü vardır. Dizinin ilk elemanı enbüyük kabul edilip bir nesnede saklanır. Sonra geri kalan tüm elemanlara bakılır, daha büyüğü varsa nesne içerisindeki değer yer değiştirilir. Örneğin: #include #define SIZE 10 int main(void) { int a[SIZE] = { 10, 4, 6, 34, 32, -34, 39, 21, 9, 22 }; int i, max; max = a[0]; for (i = 1; i < SIZE; ++i) if (a[i] > max) max = a[i]; printf("Max = %d\n", max); 108 return 0; } Sınıf Çalışması: Yukarıdaki dizide en büyük ve en küçük eleman arasındaki farkı ekrana yazdırınız Çözüm: #include #define SIZE 10 int main(void) { int a[SIZE] = { 10, 4, 6, 34, 32, -34, 39, 21, 9, 22 }; int i, max, min; min = max = a[0]; for (i = 1; i < SIZE; ++i) if (a[i] > max) max = a[i]; else if (a[i] < min) min = a[i]; printf("Degisim Araligi = %d\n", max - min); return 0; } Dizilere İlişkin Çeşitli Algoritmik Örnekler Bir dizinin eleman toplamaı ve ortalaması şöyle bulunabilir: #include #define SIZE 5 int main(void) { int a[SIZE]; int i, total; double avg; for (i = 0; i < SIZE; ++i) { printf("%d. indeksli elemani giriniz:", i); scanf("%d", &a[i]); } total = 0; for (i = 0; i < SIZE; ++i) total += a[i]; avg = (double)total / SIZE; printf("%f\n", avg); return 0; } Sınıf Çalışması: int türden bir dizi içerisindeki en fazla yinelenen değeri bulunuz (mod değeri). Test için aşağıdaki diziyi kullanınınz: int a[SIZE] = { 3, 7, 3, 6, 6, 8, 7, 8, 6, 4 }; 109 İpucu: İç içe iki döngü kullanılabilir. Her değerin kaç tane tekrarlandığı bulunur ve bir değişkende saklanır, daha fazlası varsa yer değiştirilir. (Dizinin k'ıncı elemanından kaç tane olduğuna bakmak için baştan başlamaya gerek yoktur. k'ıncı indeksten başlamak yeterlidir. Çünkü zaten k'ıncı indekstenn önce o elemandan varsa biz onun sayısını bulmuş durumda oluruz). Çözüm: #include #define SIZE 10 int main(void) { int a[SIZE] = { 3, 7, 3, 6, 6, 8, 7, 8, 6, 4 }; int i, k; int count, max_count, max_count_val; max_count = 0; for (i = 0; i < SIZE; ++i) { count = 1; for (k = i + 1; k < SIZE; ++k) if (a[i] == a[k]) ++count; if (count > max_count) { max_count = count; max_count_val = a[i]; } } printf("Mod = %d\n", max_count_val); return 0; } Sınıf Çalışması: int türden bir diziyi ters yüz ediniz ve elemanları yazdırınız Çözüm: #include #define SIZE 10 int main(void) { int a[SIZE] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int i, temp; for (i = 0; i < SIZE / 2; ++i) { temp = a[SIZE - i - 1]; a[SIZE - i - 1] = a[i]; a[i] = temp; } for (i = 0; i < SIZE; ++i) printf("%d ", a[i]); printf("\n"); return 0; } Dizilerin sıraya dizilmesine İngilizce "sorting" denilmektedir ve 20'den fazla sıraya dizme yöntemi vardır. En basit yöntemlerden biri kabarcık sıraalaması (bubble sort) yöntemidir. Bu yöntemde dizinin yan yana elemanları 110 karşılaştırılır, duruma göre yer değiştirilir. Tabi bu işlem bir kez yapılmaz. Bu işlem bir kez yapıldığında en büyük eleman sona gider. Örneğin: Bu işlemi ikinci yaptığımızda sona kadar değil, bir önceye kadar gitmemiz yeterli olur. Böylece iç içe iki döngüyle algoritma gerçekleştirilebilir: #include #define SIZE 10 int main(void) { int a[SIZE] = { 10, 23, -5, 34, 77, 100, 32, 87, 22, 44}; int i, k; int temp; for (i = 0; i < SIZE - 1; ++i) for (k = 0; k < SIZE - 1 - i; ++k) { if (a[k] > a[k + 1]) { temp = a[k]; a[k] = a[k + 1]; a[k + 1] = temp; } } for (i = 0; i < SIZE; ++i) printf("%d ", a[i]); printf("\n"); return 0; } Seçerek Sıralama (Selection Sort) yönetminde dizinin en küçük elemanı bulunur, ilk elemanla yer değiştirilir. Örneğin: Sonra dizi daraltılarak aynı işlem onun için de yapılır: 111 Algoritmada iki döngü bulunur. Dıştaki döngü diziyi daraltmakta kullanılır. İçteki döngü en küçük elemanı bulur: #include #define SIZE 10 int main(void) { int a[SIZE] = { 10, 23, -5, 34, 77, 100, 32, 87, 22, 44}; int i, k; int min, minIndex; for (i = 0; i < SIZE - 1; ++i) { min = a[i]; minIndex = i; for (k = i + 1; k < SIZE; ++k) if (a[k] < min) { min = a[k]; minIndex = k; } a[minIndex] = a[i]; a[i] = min; } for (i = 0; i < SIZE; ++i) printf("%d ", a[i]); printf("\n"); return 0; } Yazıların char Türden Dizilerde Saklanması Aslında yazı dediğimiz şey karakterlerden oluşan bir dizidir. Bilindiği gibi karakterler de aslında karakter tablosunda birer sayı belirtir. Bir yazının karakter numaralarını bir dizide saklarsak yazıyı saklamış oluruz. Programa dili ne olursa olsun yazılar arka planda hep böyle saklanmaktadır. Şüphesiz yazıları saklamak için en iyi tür char türüdür. char türü her sistemde 1 byte uzunluğundadır. Genellikle yazılar char türden dizilerin başından başlarlar fakat onların sonuna kadar devam etmezler. Yani genellikle yazılar yerleştirildikleri diziden küçük olurlar. Bu durumda yazının sonunu belirlemek gerekir. İşte C'de bunun için null karakter kullanılmaktadır. null karakter '\0' ile temsil edilir. Karakter, tablonun sıfır numaralı karakteridir ve sayısal değeri de sıfırdır. null karakterin bir görüntü karşılığı yoktur, zaten tabloya böyle bir amaç için yerleştirilmiştir. C'de bir char türden bir diziye yazı yerleştireceksek yazının sonuna (dizinin sonuna değil) '\0' karakteri koymalıyız. Çünkü C'de herkes tarafından yapılan böyle bir anlaşma vardır. Bu durumda n elemanlı charf türden bir diziye en 112 fazla n - 1 elemanlı bir yazı yerleştirilebilir. Örneğin: #include int main(void) { char s[10]; int i; s[0] = 'a'; s[1] = 'l'; s[2] = 'i'; s[3] = '\0'; for (i = 0; s[i] != '\0'; ++i) putchar(s[i]); putchar('\n'); return 0; } Aynı işlem ilşkdeğer verilerek de yapılabilirdi: #include int main(void) { char s[10] = { 'a', 'l', 'i', '\0' }; int i; for (i = 0; s[i] != '\0'; ++i) putchar(s[i]); putchar('\n'); return 0; } Burada diziye ilk değer verildiği için derleyici gerei kalan elemanları sıfırlar. Null karakter zaten sıfır olduğu için aşağıdaki iki bildirim eşdeğer olur: char s[10] = { 'a', 'l', 'i', '\0' }; char s[10] = { 'a', 'l', 'i'}; char türden bir dizide null karakter görene kadar ilerlemek için şu kalıp kullanılabilir: for (i = 0; s[i] != '\0'; ++i) { ... } gets Fonksiyonu gets standart bir C fonksiyonudur. Aşağıdaki gibi kullanılır: gets(); gets fonksiyonu kalvyeden bir yazı girilip ENTER tuşuna basılana kadar bekler. Girilen yazının karakterlerini tek tek verdiğimiz char türden diziye yerleştirir, yazının sonuna '\0' karakterini de ekler. gets dizinin diğer elemanlarına hiç dokunmaz. Örneğin: 113 #include int main(void) { char s[100]; int i; gets(s); for (i = 0; s[i] != '\0'; ++i) putchar(s[i]); putchar('\n'); return 0; } puts Fonksiyonu puts fonksiyonu bir dizinin ismini (yani adresini) parametre olarak alır '\0' görene kadar tüm karakterleri yan yana basar, en sonunda imleci aşağı satırın başına geçirerek orada bırakır. Örneğin: #include int main(void) { char s[100]; int i; gets(s); puts(s); return 0; } Sınıf Çalışması: Klavyeden gets fonksiyonuyla bir diziye bir yazı giriniz. Girilen yazının kaç karakterli olduğunu (yani yazının uzunluğunu) yazdırınız. Çözüm: #include int main(void) { char s[100]; int i; printf("Yazi giriniz:"); gets(s); for (i = 0; s[i] != '\0'; ++i) ; printf("%d\n", i); return 0; } Sınıf Çalışması: Klavyeden gets fonksiyonuyla bir diziye bir yazı giriniz. Girilen yazıyı tersten yazdırınız. Çözüm: 114 #include int main(void) { char s[100]; int i; printf("Yazi giriniz:"); gets(s); for (i = 0; s[i] != '\0'; ++i) ; for (--i; i >= 0; --i) putchar(s[i]); putchar('\n'); return 0; } Bir dizideki yazıyı büyük harfe dönüştüren örnek: #include #include int main(void) { char s[100]; int i; printf("Yazi giriniz:"); gets(s); for (i = 0; s[i] != '\0'; ++i) s[i] = toupper(s[i]); puts(s); return 0; } Sınıf Çalışması: char türden bir dizi ve bir de char türden değişken bildiriniz. gets fonksiyonu ile klavyeden diziye okuma yapınız. Sonra da getchar fonksiyonu ile char nesneye okuma yapınız. Yazı içerisinde o karakterden kaç tane olduğunu yazdırınız. Çözüm: #include int main(void) { char s[100]; char ch; int i, count; printf("Yazi giriniz:"); gets(s); printf("Bir karakter giriniz:"); ch = getchar(); for (count = 0, i = 0; s[i] != '\0'; ++i) if (s[i] == ch) ++count; printf("%d\n", count); 115 return 0; } printf Fonksiyonu İle Yazıların Yazdırılması printf fonksiyonunda %s format karakterine karşılık char türden bir dizi ismi (aslında adres) gelmelidir. Bu durumda printf null karakter görene kadar karakterleri yazar. Örneğin: #include int main(void) { char s[100]; printf("Yazi giriniz:"); gets(s); printf("Girilen yazi: %s\n", s); return 0; } Yani: puts(s); ile, printf("%s\n", s); eşdeğerdir. char Türden Dizilere İki Tırnak İfadesi ile İlkdeğer Vermek C'de char türden bir diziye iki tırnak ile ilkdeğer verilebilir. Örneğin: char s[10] = "ankara"; Burada yazının karakterleri tek tek diziye yerleştirilir, sonuna null karakter eklenir. Tabi dizinin geri akalan elemanları da yine sıfırlanacaktır. Örneğin: #include int main(void) { char s[100] = "ankara"; printf("%s\n", s); return 0; } İki tırnak ile ilkdeğer verilirken dizi uzunluğu yine belirtilmeyebilir. Örneğin: char s[] = "ankara"; Burada derleyici null karakteri de hesaba katar. Yani dizi 7 uzunlukta açılmış olacaktır. Aşağıdaki ilkdeğer verme geçersizdir: 116 char s[4] = "ankara"; /* geçersiz! */ Burada verilen ilkdeğerler dizi uzunluğundan fazladır. Aşağıdaki durum bir istisnadır: char s[6] = "ankara"; /* geçerli, fakat null karakter eklenmeyecek */ Standartlara göre iki tırnak içerisindeki karakterlerin sayısı tam olarak dizi uzunluğu kadarsa bu durum geçerlidir. Ancak '\0' karakter derleyici tarafından bu istisnai durumda diziye eklenmez. İki tırnağın içi boş olabilir. Bu durumda yalnızca null karakter ataması yapılır. Örneğin: char s[] = ""; /* geçerli */ Dizi tek elemanlıdır ve o elemanda da '\0' karakter vardır. Standartlara göre '\0' karakteri kesinlikle hangi karakter dönüştürme tablosu kullanılıyor olursa olsun o tablonun sıfır numaralı karakteridir. Yani null karakterin sayısal değeri sıfırdır. Başka bir deyişle: s[n] = 0; ile s[n] = '\0'; tamamen aynı etkiyi yapar. Eğer char dizisi yazısal amaçlı kullanılıyorsa '\0' kullanmak iyi bir tekniktir. char türden diziye iki tırnakla daha sonra değer atayamazyız. Yalnızca ilkdeğer olarak bunu yapabiliriz. Örneğin: char s[10]; s = "ankara"; /* geçersiz! */ Koşul Operatörü (Conditional Operator) Koşul operatörü C'nin üç operandlı (ternary) tek operatörüdür. Operatör "? :" temsil edilir. Genel kullanımı şöyledir: op1 ? op2 : op3 Burada op1, op2 ve op3 birer ifadedir. Koşul operatörü if deyimi gibi çalışan bir operatördür. Operatörün çalışması şöyledir: Önce soru işaretinin solundaki ifade (op1) yapılır. Bu ifade sıfır dışı bir değerse yalnızca soru işareti ile iki nokta üstü üste arasındaki ifade (op2) yapılır, sıfırsa iki nokta üst üstenin sağındaki ifade yapılır. Koşul operatörü bir operatör olduğu için bir değer üretir. Yani bu değer bir nesneye atanabilir, başka işlemlere sokulabilir. Koşul operatörünün ürettiği değer duruma göre op2 ya da op3 ifadelerinin değeridir. Örneğin: result = val > 0 ? 100 + 200 : 300 + 400; Burada val > 0 ise yalnızca 100 + 200 ifadesi yapılır ve koşul operatöründen bu değer elde edilir. Değilse 300 + 400 yaılır ve koşul operatöründen bu değer elde edilir. Burada yapılan aşağıdaki ile eşdeğerdir: if (val > 0) result = 100 + 200; else result = 300 + 400; 117 Örneğin: #include int main(void) { int val; int result; printf("Bir sayi giriniz:"); scanf("%d", &val); result = val > 0 ? 100 + 200 : 300 + 400; printf("%d\n", result); return 0; } Koşul operatörü elde edilen değerin bir nesneye atanacağı ya da b aşka bir ifadede kullanılacağı durumlarda tercih edilmelidir. Aşağıdaki örnek koşul operatörünün kötü bir kullanımına işaret eder: a > 0 ? foo() : bar(); Burada koşul operatörünün kullanılması kötü bir tekniktir. Koşul operatörünün üç durumda kullanılması tavsiye edilir: 1) Bir karşılaştırma sonucunda elde edilen değerin bir nesneye atanması durumunda. Örneğin: max = a > b ? a : b; Örneğin: for (i = 1900; i < year; ++i) tdays += isleap(i) ? 366 : 365; Burada isleap(i) sıfır dışı ise tdays'e 366 değilse 365 eklenmektedir. Aşağıda belli bir tarihin hangi güne karşı geldiğini bulan programı görüyorsunuz: #include int isleap(int year) { return year % 4 == 0 && year % 100 != 0 || year % 400 == 0; } int totaldays(int day, int month, int year) { int i; long tdays = 0; int montab[12] = { 31, 28, 31, 30, 31, 30, 31, 31, 30, 31, 30, 31 }; for (i = 1900; i < year; ++i) tdays += isleap(i) ? 366 : 365; montab[1] = isleap(year) ? 29 : 28; for (i = 0; i < month - 1; ++i) tdays += montab[i]; tdays += day; 118 return tdays; } void printday(int day, int month, int year) { long tdays; tdays = totaldays(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) { printday(10, 9, 1993); return 0; } 2) Koşul operatörü fonksiyon çağrılarında argüman olarak kullanılabilir. Böylece elde edilen değer fonksiyonun parametre değişkenine atanmış olur. Örneğin: foo(val % 2 == 0 ? 100 : 200); Bunun if eşdeğeri şöyledir: if (val % 2 == 0) foo(100); else foo(200); Örneğin: #include int main(void) { int val; printf("Bir sayi giriniz:"); 119 scanf("%d", &val); printf(val % 2 == 0 ? "Cift\n" : "Tek\n"); return 0; } 3) Koşul operatörü return deyiminde de kullanılabilir. Örneğin: return a % 2 == 0 ? 100 : 200; Bu ifadenin eşdeğer if karşılığı şöyledir: if (a % 2 == 0) return 100; else return 200; Koşul operatörü özel bir operatördür. Öncelik tablosundaki klasik kurallara tam uymaz. Ancak öncelik tablosunda hemen atama ve işlemli atama operatörlerinin yukarısında sağdan-sola grupta bulunur: () 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ünün operandları şöyle ayrıştırılır: Soru işaretinden sola doğru gidilir. Koşul operatöründen daha düşük öncelikli operatör görülene kadarki ifade koşul operatörünün birinci operandını oluşturmaktadır. Soru işareti ile iki nokta üst üsete arasındaki ifade koşul operatörünün ikinci operandını oluşturur. İki nokta üst üstenin sağındaki ifade koşul operatörünün üçüncü operandını oluşturmaktadır. Örneğin: Parantezler koşul operatörünün opearanlarını ayrıştırmakta kullanılabilir. Örneğin: a = (b > 0 ? 100 + 200 : 300) + 400; Burada artık üç operatör vardır: Atama, koşul ve +. Yani + 400 koşul operatörünün dışındadır. b > 0 koşulu sağlansa da sağlanmasa da 400 ile toplama yapılacaktır. 120 Koşul operatörü de iç içe kullanılabilir. Örneğin: result = a > b ? a > c ? a : c : b > c ? b : c; Burada üç sayının en büyüğü bulunmaktadır. Tabi bu ifade karmaşık gözüktüğü için parantezler gerekmiyor olsa da sadelk oluşturmak için kullanılabilir. Örneğin: result = a > b ? (a > c ? a : c) : (b > c ? b : c); sizeof Operatörü sizeof bir derleme zamanı (compile time) operatörüdür. Parantezlerinden dolayı acemi C programcıları onu fonksiyon sanabilmektedir. Operatörler derleme aşamasında derleyici tarafından kod üretilmesi için ele alınırlar. Oysa fonksiyonlar programın çalışma zamanı sırasında akışın oraya gitmesiyle işlem görürler. sizeof operatörünün kullanım biçimleri şöyledir: sizeof sizeof() sizeof sizeof operatörünün operandı bir ifade ise, derleyici o ifadeyi yapmaz. Yalnızca o ifadenin türüne bakar. sizeof ifadenin türünün o sistemde kaç byte yer kapladığını belirtir. Derleyici sizeof operatörünü gördüğünde derleme aşamasında oraya sabir yerleştirir. Yani sanki sizeof yerine biz bir sabit yazmışız gibi işlem görür. sizeof operatörü tek operandlı önek bir operatördür ve öncelik tablosunun ikinci düzeyinde sağdan sola grupta bulunur: () Soldan-Sağa + - ++ -- ! sizeof Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa ... ... Dolayısıyla, sizeof a + b ifadesinde a'nın sizeof'u b ile toplanır. Fakat: sizeof (a + b) iafdesinde a + b'nin sizeof'u elde edilmektedir. Programcılar genellikle sizeof operatörünü hep parantezlerle kullanmaktadır. Örneğin: #include int main(void) { int n; n = sizeof 1.2 + 5; printf("%d\n", n); /* 13 */ 121 n = sizeof (1.2 + 5); /* 8 */ return 0; } sizeof operatörünü gören derleyici onun yerine o ifadenin türünün kaç byte olduğuna ilişkin sabit bir değer yerleştiri. Yerleştirdiği sabit size_t türündendir (tipik olarak unsigned int). n = sizeof 1.2 + 5; ifadesi derleyici tarafından şu biçime dönüştürülür: n = 8u + 5; Şüphesiz sizeof operatörünün operandı fonksiyon çağrısıysa fonksiyon işletilmez, onun geri dönüş değerinin türüne bakılır, o değer elde edilir: #include double foo(void) { printf("Im am foo\n"); return 1500.2; } int main(void) { int n; n = sizeof(foo()); printf("%d\n", n); return 0; } Burada ekrana foo yazısı basılmaz, yalnızca 8 yazısı basılır. void değerin sizeof'u yoktur. Dolayısıyla sizeof operatörünün operandı void türden olamaz. sizeof operatörünün operandı doğrudan bir tür ismi olabilir. Bu durumda parantezler kullanılmak zorundadır. Örneğin: #include int main(void) { int n; n = sizeof(int); printf("%d\n", n); /* 4 */ return 0; } sizeof operatörünün operandı bir dizi ismi olabilir. Bu durumda sizeof o dizinin bellekte kapladığı byte miktarını bize verir. Örneğin: #include 122 int main(void) { int a[4] = { 1, 2 }; char s[] = "ali"; printf("%u\n", sizeof a); printf("%u\n", sizeof s); /* 16 */ /* 4 */ return 0; } Pekiyi sizeof operatörüne neden gereksinim duyulmaktadır? Bilindiği gibi C'de char dışındaki tüm türlerin uzunlukları derleyiciye bağlı olarak değişebilmektedir. Kodun taşınabilirliğini sağlamak için bazı durumlarda sizeof operatörünü kullanmak zorunda kalabiliriz. sizeof operatörüne olan gereksinim ileriki konularda açıkça görülecektir. Göstericiler (Pointers) Adres bilgilerinin saklanması için kullanılan nesnelere gösterici (pointer) denir. Bir adres bilgisi C'de int, long türlerde tutulamaz. Ancak gösterici denilen türlerde tutulabilir. Benzer biçimde göstericiler de adi birer int, long türleri tutamazlar. Ancak adres tıtarlar. Gösterici bildiriminin genel biçimi şöyledir: *; Örneğin: int *p; long *t; double *d; Buradaki * adres türünü temsil eder. Çarpma gibi bir anlama gelmez. Bir gösterici her türden adres bilgisini tutamaz. Yalnızca tür bileşeni belirli olan olan adres bilgilerini tutar. Genel olarak: T *p; gibi bir bildirimde p göstericisi tür bileşeni T olan adresleri tutabilir. Örneğin: *pi; int Burada pi int türünden göstericidir. int türden gösterici demek, tür bileşeni int olan adresleri tutan gösterici demektir. Anahtar Notlar: İngilizce T türünden gösterici "pointer to T" biçiminde yazılıp okunmaktadır. Gösterici bildiriminde * farklı bir atomdur. Dolayısıyla öncesinde sonrasında birden fazla boşluk karakteri bulunabilir. Örneğin: int * pi; /* geçerli */ Bazı programcılar * atomunu türe yapıştırmaktadır: int* pi; /* geçerli */ 123 Fakat gelenek (zaten daha mantıksal olduğu için) * atomunun gösterici ismine yapıştırılmasıdır: int *pi; * atomu dekleratöre ilişkindir, türe ilişkin değildir. Örneğin aşağıdaki bildirim geçerlidir: int *pi, a; Burada pi int türden bir göstericidir fakat a, adi bir int nesnedir. Örneğin: int *pi, *a; Burada hem pi hem de a int türden göstericidir: int a, b[10], *c; Burada a adi bir int, b 10 elemanlı bir int dizi, c ise inttürdne bir göstericidir. Bir göstericiye aynı türden bir adres bilgisi yerleştirilir: int *pi; pi = (long *) 0x1FC0; pi = 0x1FC0; /* geçersiz! tür yanlış*/ /* geçersiz! adi bir int */ Benzer biçimde biz nümerik türlere adres bilgisi atayamazyız. Örneğin: int a; a = (int *) 0x1FC0; /* geçersiz! */ Dizi İsimlerinin Otomatik Adreslere Dönüştürülmesi C'de bir dizinin ismi dizinin taamamını temsil eder. Dizi isimleri işleme sokulduğunda otomatik olarak derleyici tarafından adrese dönüştürülür. Yani dizi isimleri aslında adres belirtmektedir. Dizi isimleriyle belirtilen adresin tür bileşeni dizinin aynı türden, sayısal bileşeni dizinin bellekteki başlangıcına ilişkin fiziksel adres numjarasından oluşur: Burada a ismini işleme soktuğumuzda otomatik olarak bu dizinin başlangıç adresine ilişkin adres sabiti gibi işleme girer. Medemki dizi isimleri adres belirtmektedir, o halde bir dizi ismini biz aynı türden bir göstericiye atayabiliriz. Örneğin: 124 int *pi; int a[3]; pi = a; /* geçerli */ Örneğin: int *pi; char s[10]; pi = s; /* geçersiz! s char türden adres belirtir */ Örneğin: int a[10]; int b; b = a; /* Geçersiz! a bir adres bilgisidir ve göstericiye yerleştirilebilir */ Bir göstericiye bir adres bilgisi atandığında aslında gösterici yalnızca adresin sayısal bileşenini tutar. (Tabi bu durum göstericiye adi bir sayı atıyabileceğemiz anlamına gelmiyor). Bir göstericiye bir adres adres bilgisi atandığında o göstericinin o adresi gösterdiği söylenir. Örneğin: Burada pi göstericisi a dizisinin başlangıç adresini göstermektedir. & (Address Of) Operatörü & sembolü C'de "address of" isimli bir adres operatörü belirtir. & tek operandlı önek bir operatördür. Öncelik tablosunun ikinci düzeyinde sağdan sola grupta bulunmaktadır: () Soldan-Sağa + - ++ -- ! sizeof & Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa ... ... & operatörü operand olarak bir nesne alır. Operatör bize nesnenin bellekteki yerleşim adresini verir. & operatörü ile çekilen adresin sayısal bileşeni operand olan nesnenin bellekteki fiziksel adres numarasıdır. Tür bileşeni ise 125 nesnenin türüyle aynı türdendir. Örneğin: Dizi isimleri zaten adres belirtmektedir. Bu nedenle yanlışlıkla dizi isimlerine & uygulamamak gerekir. Fakat adi nesnelerin adreslerini almak için & operatörü kullanmak gerekir. & operatörüyle elde ettiğimiz adresler ancak aynı türdne bir göstericiye atanbilir. int a; int *pi; char *pc; pi = &a; pc = &a; /* geçerli */ /* geçersiz! */ & operatörünün operandı bir nesne olabilir. Nesne belirtmeyen bir ifade olamaz. Çünkü yalnızca nesnelerin adresleri vardır. Örneğin: #define MAX 100 int *pi; pi = &100; pi = &MAX; /* geçersiz! */ /* geçersiz! */ * (Indirection) Operatörü * tek operandlı önek bir adres operatörüdür. * operatörünün operandı bir adres bilgisi olmak zorundadır. * operatörü operandı olan adresteki nesneye erişmekte kullanılır. * operatörüyle elde edilen nesnenin türü operanda olan adresin türüyle aynı türdendir. Örneğin: pi göstericini kullandığımızda onun içerisindeki adres bilgisini kullanıyor oluruz. Ancak *pi ifadesi pi adresindeki nesneyi belirtir. Bu da şekilde görüldüğü gibi a'dır. Burada *pi ifadesi int türdendir. Çünkü pi adresi int türdendir. Örneğin: #include 126 int main(void) { int a = 100; int *pi; pi = &a; printf("%d\n", *pi); *pi = 150; printf("%d\n", a); /* 100 */ /* 150 */ return 0; } Örneğin: #include int main(void) { char ch; char *pc; pc = &ch; *pc = 'x'; putchar(ch); return 0; } Örneğin: #include int main(void) { int a[3] = { 10, 20, 30 }; int *pi; pi = a; *pi = 100; printf("%d\n", a[0]); return 0; } Buradada yapılan işlemleri şekilsel olarak şöyle ifade edilebilir: p bir adres belirtiyor olsun. Aşağıdaki gibi bir atamadan bellekteki kaç byte etkilenir: 127 *p = 0; İşte p eğer char türdense p adresinden itibaren 1 byte etkilenir. p int türdense 4 byte, double türdense 8 byte etkilenecektir. Aşğıdaki gibi bir gösterici bildirimi yapılmış olsun: int *pi; Buaradan iki şey anlaşılır: 1) Burada pi int * türündendir. (pi'yi kapatıp sola baktımızda int * görüyoruz). 2) pi göstericisini * operatörü ile kullanırsak int bir nesne elde ederiz (*pi'yi kapatıp sola baktığımızda int görüyoruz). C'de adres türleri sembolik olarak T * ile ifade edilir. Örneğin: char *pc; Burada pc'nin türü char * biçimindedir. *pc ise char türdendir. * operatörü öncelik tablosunun ikinci düzeyinde sağdan sola grupta bulunur: () Soldan-Sağa + - ++ -- ! sizeof & * Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa ... ... Örneğin: a = *pi * 50; Burada: İ1: *pi İ2: İ1 * 50 İ3: a = İ2 * operatörünün operandı gösterici olmak zorunda değildir. Adres belirten herhangi bir ifade olabilir. Yani * operatörünün operandı şunlar olabilir. 128 1) Bir gösterici (*pi gibi) 2) Bir adres sabiti. Örneğin: *(int *) 0x1FC0 = 100; 3) & operatörü elde edilmiş bir adres (*&a gibi) 4) Bir dizi ismi olabilir. Örneğin: int a[] = {1, 2, 3}; *a = 100; /* geçerli */ 5) String ifadeleri (henüz görmedik) & ve * operatörlerinin sağdan sola aynı grupta olduğuna dikkat ediniz. Örneğin: int a = 100; *&a = 200; printf("%d\n", a); Burada: İ1: &a İ2: *İ1 İ3: İ2 = 200 Örneğin: #include int main(void) { int a = 100; *&a = 200; printf("%d\n", a); return 0; } Örneğin: #include int main(void) { int a[3] = { 10, 20, 30 }; int i; *a = 100; for (i = 0; i < 3; ++i) printf("%d\n", a[i]); return 0; } 129 Adreslerin Artırılması ve Eksiltilmesi Adres bilgileri tamsayı türlerine ilişkin değerlerle toplama ve çıkartma işlemine sokulabilir. Yani, p bir adres bilgisi n de bir tamsayı türünden olmak üzere p + n ve p - n ifadeleri geçerlidir (Ancak n - p ifadesi geçerli değildir). Bir adxres bilgisine bir tamsayıyı topladığımızda ya da bir adres bilgisinden bir tamsayı çıkarttığımızda elde edilen ürün yine aynı türden bir adres bilgisi olur. Örneğin pi int türünden bir adres bilgisi (int *) olsun: pi + 1 ifadesi int türden bir adres bilgisi belirtir. C'de bir adres bilgisi 1 artırıldığında ya da 1 eksiltildiğinde adresin sayısal bileşeni o adresin türünün uzunluğu kadar artar ya da eksilir. Yani örneğin int türden bir göstericiyi 1 artırdığımızda içindeki adres 32 bit sistemlerde 4 artacaktır. char türden bir gösterici bir adtırdığımızda içindeki adres 1 artacaktır. Bu durumda dizi elemanları ardışıl olduğuna göre biz dizinin başlangıç adresini aynı türden bir göstericiye yerleştirip sonra o göstericiyi artırarak dizinin tüm elemanlarına erişebiliriz: Örneğin: #include int main(void) { int a[3] = { 10, 20, 30 }; int *pi; int i; pi = a; printf("%d\n", *pi); ++pi; printf("%d\n", *pi); ++pi; printf("%d\n", *pi); /* 10 */ /* 20 */ /* 30 */ return 0; } * operatörünün opeandı adres türünden olmak zorudadır. Örneğin: 130 int a = 0x1Fc0; *a = 10; /* geçersiz! */ char türden bir dizinin adresini char türden bir göstericiye atadıktan sonra göstericiyi artıra artıra yazının tüm karakterlerine erişebiliriz. Örneğin: #include int main(void) { char s[] = "ankara"; char *pc; pc = s; while (*pc != '\0') { putchar(*pc); ++pc; } putchar('\n'); return 0; } Köşeli parantez (Index) Operatörü Köşeli parantez operatörü tek operandlı sonek bir operatördür. Köşeli parantezler içerisinde tamsayı türlerine iliikin bir ifade bulunmak zorundadır. Köşeli parantez operatörü öncelik tablosunun tepe grubunda (..) operatörü ile soldan sağa aynı gruptadır: () [] Soldan-Sağa + - ++ -- ! sizeof & * Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa ... ... Köşeli parantez operatörünün operandı bir adres bilgisi olmak zorundadır. p bir adres belirten ifade olmak üzere: p[n] ile *(p + n) aynı anlamdadır. p[n] ifadesi p adresinden n ilerideki nesneyi temsil eder. Dizi isimleri dizilerin başlangıç adresini belirttiği için dizi elemanlarına köşeli parantez operatörü ile erişilebilmektedir. Örneğin: int a[3] = {10, 20, 30}; a[2] ifadesi a adresinden 2 ilerinin (yani 2 * sizeof(*a) kadar byte ilerinin) içeriği anlamına gelir. Yani a[2] ile *(a + 2) aynı anlamdadır. Pekiyi p[0] ne anlama gelir? Bu ifade p adresinden 0 ilerinin içeriği anlamına gelir. *(p + 0) ile ve dolayısyla *p ile aynı anlamdadır. Biz köşeli parantez operatörünü göstericilerle, dizi isimleriyle ve diğer adres belirten ifadelerle kullanabiliriz. Örneğin: 131 #include int main(void) { char s[] = "ankara"; char *pc; int i; pc = s; for (i = 0; pc[i] != '\0'; ++i) putchar(pc[i]); putchar('\n'); return 0; } Köşeli parantez içerisindeki ifade negatif bir değer de belirtebilir. Örneğin: #include int main(void) { int a[3] = { 10, 20, 30 }; int *pi; pi = a + 2; printf("%d\n", pi[-2]); /* 10 */ return 0; } Göstericilere İlkdeğer Verilmesi Göstericilere de bildirim sırasında '=' atomu ile ilkdeğer verebiliriz. Örneğin: int *pi = (int *)0x1FC0; Burada adres p'ye atanmaktadır. Bildirimdeki yıldız operatör görevinde değildir. Tür belirtmek için kullanılmaktadır. Başka bir örnek: int a = 123; int *pi = &a; Örneğin: #include int main(void) { int a[3] = { 10, 20, 30 }; int *pi = a; // p'ye dizinin başlangıç adresi atanıyor int i; for (i = 0; i < 3; ++i) printf("%d ", pi[i]); printf("\n"); return 0; } 132 Fonksiyon Parametresi Olarak Göstericilerin Kullanılması Bir fonksiyonun parametresi bir gösterici olabilir. Örneğin: void foo(int *pi) { ... } Bir fonksiyonun parametresi bir gösterici ise biz o fonksiyonu aynı türden bir adres bilgisi ile çağırmalıyız. Örneğin: int a = 10; ... foo(a); foo(&a); // geçersiz! // geçerli Örneğin: #include void foo(int *pi) { *pi = 200; } int main(void) { int a = 123; foo(&a); printf("%d\n", a); return 0; } Burada foo fonksiyonuna a'nın içindeki değer değil, a'nın adresi geçirilmiştir. Böylece fonksiyon içerisinde *pi dediğimizde aslında bu ifade main fonksiyonundaki a'ya erişir. C'de bir fonksiyonun başka bir fonksiyonun yerel değişkenini değiştirebilmesi için onun adresini alması gerekir. Bunun için de fonksiyonun parametre değişkeninin gösterici olması gerekir. (Aslında tüm dillerde böyledir. Çünkü makinanın çalışma prensibinde durum böyledir.) İki yerel nesnenin içerisindeki değerleri değiştiren swap isimli bir fonksiyon yazmak isteyelim. Acaba bu fonksiyonu aşağıdaki gibi yazabilir miyiz? 133 #include void swap(int a, int b) { int temp = a; a = b; b = temp; } int main(void) { int x = 10, y = 20; swap(x, y); printf("x = %d, y = %d\n", x, y); return 0; } Yanıt hayır! Çünkü burada swap x ve y'nin içindekilerini değiştirmiyor, parametre değişkeni olan a ve b'nin içindekileri değiştiriyor. Bunun sağlanabilmesi için yerel nesnelerin adreslerinin swap fonksiyonuna aktarılması gerekir. Bu durumda swap fonksiyonunun paremetre değişkenleri birer gösterici olmalıdır: #include void swap(int *a, int *b) { int temp = *a; *a = *b; *b = temp; } int main(void) { int x = 10, y = 20; swap(&x, &y); printf("x = %d, y = %d\n", x, y); return 0; } Dizilerin Fonksiyonlara Parametre Yoluyla Aktarılması Dizi elemanları bellekte ardışıl bir biçimde tuutlduğuna göre biz bir diziyi yalnızca onun başlangıç adresi yoluyla fonksiyona aktarabiliriz. Böylece dizinin başlangıç adresini alan fonksiyon * ya da [ ] operatörü ile dizinin her elemanına erişebilir. Dizilerin fonksiyonlara parametre yoluyla aktarılmasında mecburen göstericilerden faydalanılmaktadır. #include void foo(int *pi) { int k; for (k = 0; k < 10; ++k) { printf("%d ", *pi); ++pi; } printf("\n"); } 134 void bar(int *pi) { int k; for (k = 0; k < 10; ++k) printf("%d ", pi[k]); printf("\n"); } int main(void) { int a[10] = { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }; foo(a); bar(a); return 0; } Yukarıdaki fonksiyonlar dizinin uzunluğununun 10 olduğunu biliyor durumdalar. Biz bu fonksiyonlara ancak 10 elemanlı dizilerin başlangıç adreslerini yollayabiliriz. eğer dizilerle çalışan fonksiyonların daha genel olmasını istiyorsak o fonksiyonlara ayrıca dizinin uzunluğunu geçirmemiz gerekir. Örneğin: #include void foo(int *pi, int size) { int k; for (k = 0; k < size; ++k) { printf("%d ", *pi); ++pi; } printf("\n"); } void bar(int *pi, int size) { int k; for (k = 0; k < size; ++k) printf("%d ", pi[k]); printf("\n"); } int main(void) { int a[10] = { 10, 20, 30, 40, 50, 60, 70, 80, 90, 100 }; int b[5] = { 1, 2, 3, 4, 5 }; foo(a, 10); bar(b, 5); return 0; } Örneğin: #include int getmax(int *pi, int size) { int max = pi[0]; int i; 135 for (i = 1; i < size; ++i) if (pi[i] > max) max = pi[i]; return max; } int main(void) { int a[5] = { 23, -4, 21, 34, 32 }; int max; max = getmax(a, sizeof(a)/sizeof(*a)); printf("%d\n", max); max = getmax(a, 3); printf("%d\n", max); return 0; } Burada getmax int bir dizinin en büyük elemanına geri dönmektedir. double bir dizinin standart sapmasını hesaplayan fonksiyon şöyle yazılabilir: #include #include double getavg(double *pd, int size) { double total; int i; total = 0; for (i = 0; i < size; ++i) total += pd[i]; return total / size; } double get_stddev(double *pd, int size) { double avg, total; int i; avg = getavg(pd, size); total = 0; for (i = 0; i < size; ++i) total += pow(pd[i] - avg, 2); return sqrt(total / (size - 1)); } int main(void) { double a[5] = { 1, 1, 1, 1, 2 }; double result; result = get_stddev(a, 5); printf("%f\n", result); return 0; 136 } Örneğin bir diziyi kabarcık sıralaması (bubble sort) yöntemiyle sıraya dizen fonksiyon şöyle yazılabilir: #include void bsort(int *pi, int size) { int i, k; for (i = 0; i < size - 1; ++i) for (k = 0; k < size - 1 - i; ++k) if (pi[k] > pi[k + 1]) { int temp = pi[k]; pi[k] = pi[k + 1]; pi[k + 1] = temp; } } void disp_array(int *pi, int size) { int i; for (i = 0; i < size; ++i) printf("%d ", pi[i]); printf("\n"); } int main(void) { int a[] = { 4, 8, 2, 21, 43, 10, -5, 87, 9, 68 }; bsort(a, 10); disp_array(a, 10); return 0; } Sınıf Çalışması: 10 elemanlı int türden bir dizi tanımlayınız. Bu dizinin elemanlarını ters yüz eden aşağıdaki prototipe sahip fonksiyonu yazarak test ediniz: void rev_array(int *pi, int size); Çözüm: #include void rev_array(int *pi, int size) { int i, temp; for (i = 0; i < size / 2; ++i) { temp = pi[i]; pi[i] = pi[size - i - 1]; pi[size - i - 1] = temp; } } void disp_array(int *pi, int size) { int i; for (i = 0; i < size; ++i) 137 printf("%d ", pi[i]); printf("\n"); } int main(void) { int a[] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; rev_array(a, 10); disp_array(a, 10); return 0; } Gösterici Parametrelerinin Alternatif Biçimi C'de bir fonksiyonun parametre değişkeni bir gösterici ise bu alternatif olarak dizi sentaksıyla da belirtilebilmektedir. Bu alternatif sentaksta köşeli parantezlerin içi boş bırakılabilir. Fakat köşeli parantezlerin içerisine bir uzunluk yazılsa bile bunun hiçbir özel anlamı yoktur. Yani köşeli parantezlerin içinin boş bırakılmasıyla, onun içerisine bir uzunluk yazılması arasında bir fark yoktur. Örneğin: void foo(int *pi) { ... } bildirimi ile aşağıdaki tamamen eşdeğerdir: void foo(int pi[]) { ... } bununla da aşağıdaki eşdeğerdir: void foo(int pi[10]) { ... } Tabii bu eşdeğerlik yalnızca fonksiyon parametresi söz konusu olduğunda geçerlidir. Yani: int pi[]; int pi[10]; /* geçersiz! Dizi uzunluğu belirtilmemiş! */ /* pi gösterici değil 10 elemanlı bir dizi */ Kaynak Kodun Okunabilirliği ve Anlaşılabilirliği Kaynak kodun bakıldığında (ileride bizim tarafımızdan ya da başkaları tarafından) kolay anlaşılması önemlidir. Buna genel olarak okunabilirlik (readabilty) denilmektedir. Okunabilirliği sağlamak için bazı temel öğelere dikkat etmek gerekir: 1) Kaynak kodda bazı kritik noktalara açıklama (yorum) yerleştirilebilir. Örneğin: if (delta >= ... } 0) { / * kök var mı? */ 138 Çok fazla açıklama bilgisi de okunabilirliği azaltmaktadır. Uygun yerlere, çok uzun olmayan ve genellikle soru cümleleri biçiminde açıklama yapmak uygun olur. 2) Değişkenlere anlamlı ve telaffuz edilebilir isimler verilmelidir. İsimlendirme Türkçe olabilir. Ancak şu da unutulmamalıdır ki, maalesef uluslararası düzeyde herkes İngilizce isimlendirme beklemektedir. Fakat kodumuzu örneğin Internet ortamında uluslararası paylaymayacaksak isimlendirmeleri Türkçe yapabiliriz. Örneğin döngü değişkenleri için i, j, k, l, m, n gelenekseldir (Fortran zamanlarından gelme bir gelenek). Tabi bazen n, count gibi isimler kullanılabilir. 3) Global değişkenlerin özel bir önekle (örneğin g_xxx biçiminde) başlatılması uygun olabilir. Örneğin g_count gibi. 4) Değişken isimlendirmesinde harflendirme (capitalization) önemlidir. Harflendirme bir sözcük uzun olan isimlerin yazılış biçimlerini belirlemek anlamına gelir. Tipik olarak programda dillerinde üç harflendirme tarzı vardır: a) Pascal Tarzı Harflendirme (Pascal casting): Burada her sözcüğün ilk harfi büyük yazılır (bu tarz Pascal dilinde çok kullanıldığı için Pascal tarzı denilmektedir). Örneğin: CreateWindow GetWindowText NumberOfStudents ... b) Klasik C Tarzı (C Style): Bu tarzda isimlerde büyük harf kullanılmaz. Sözcükleri ayırmak için alt tire kullanılır. Örneğin: create_window get_window_text number_of_students Bu tarz UNIX/Linux sistemlerinde C programlama dilinde baskın olarak kullanılmaktadır. c) Deve Notasyonu (Camel casting): Bu tarzda ilk sözcüğün tamamı küçük harflerle sonraki sözcüklerin yalnızca ilk harfleri büyük harflerle belirtilir. C++ QT framework'ünde, Java'da bu biçim tercih edilmektedir. Biz kursumuzda global değişkenleri g_ ile başlatarak harflendireceğiz. Fonksiyon isimlerini Klasik C Tarzında, yerel ve parametre değişkenlerini deve notasyonunda harflendireceğiz. Windows altında C programcıları fonksiyon isimlerini genel olarak Pascal tarzı harflendirmektedir. 5) Okunabilirliği sağlamada en önemli unsurlardan biri de kaynak kodun genel düzenidir. Bunun için birkaç tarz kullanılmaktadır. Biz kursumuzda Ritchie/Kernighan tarzını kullanıyoruz. Bu tarzın anahtar noktaları şunlardır: a) Fonksiyonlar en soldaki sütuna dayalı olarak tanımlanırlar. Fonkisyon tanımlamaları arasında bir satır boşluk bırakılır. b) Bildirim satırlarından sonra bir satır boşluk bırakılır. Örneğin: 139 c) Üst üste birden fazla SPACE kullanılmaz. Bir SPACE yetmezse TAB kullanılır. Üst üste birdeb fazla TAB kullanılabilir. d) Blok içleri bir TAB içeriden yazılır. Örneğin: e) İki operandlı operatörlerle operandlar arasında bir SPACE boşluk bırakılır. Ancak tek operandlı operatörlerle operand arasında boşluk bırakılmaz. (İstisna olarak iki operandlı nokta ve ok operatörleriyle operandlar arasında boşluk bırakılmaz.) Örneğin: f) Anahtar sözcüklerden sonra 1 SPACE boşluk bırakılır. Ancak fonksiyon isimlerinden sonra bırakılmaz. Örneğin: 140 g) Yazıdaki paragraf duygusu programlamada boş satır bırakılarak verilir. Yani konudan konuya geçerken bir satır boşluk bırakılmalıdır. h) if deyiminin yazımı şöyledir: Bloksuz biçim: Bloklu biçim: else-if Merdiveni: 141 i) for deyimi şöyle yazılır: Bloksuz biçim: Bloklu biçim: j) while deyimi şöyle yazılır: Bloksuz biçim: Bloklu biçim: k) switch deyimi şöyle yazılır: 142 l) Sembolik sabitleri yazarken STR1'ler ve STR'ler alta alta gelmelidir. Bunun için yeterli tablama yapılabilir. Örneğin: m) goto etiketleri büyük harflerle oluşturulmalıdır ve bir TAB geride bulunmalıdır. Örneğin: n) Virgül atomundan önce boşluk bırakılmaz, sonra bir SPACE boşluk bırakılır. Örneğin: Yazıların Fonksiyonlara Parametre Yoluyla Aktarılması Yazıları fonksiyonlara parametre yoluyla aktarmak için yalnızca onların başlangıç adreslerinin fonksiyona 143 geçirilmesi yeterlidir. Ayrıca onların uzunluklarının fonksiyonlara aktarılması gereksizdir. Çünkü yazıların sonunda null karakter olduğu için yazının başlangıç adresini alan fonksiyon null karakter görene kadar ilerleyerek yazının tüm karakterlerini elde edebilir. Tabi bu durumda fonksiyonun parametre değişkeni char türden gösterici olmalıdır. null karakter görene kadar yazının tüm karakterlerini dolaşmak için iki kalıp kullanılabilir: 1) while (*str != '\0') { /* ... */ ++str; } 2) for (i = 0; str[i] != '\0'; ++i) { /* ... */ } Örneğin aslında puts fonksiyonu bizden char türden bir adres alıp null karakter görene kadar karakterleri ekrana yazdırmaktadır. Aynı fonksiyonu biz de şöyle yazabiliriz: #include void myputs(char *str) { while (*str != '\0') { putchar(*str); ++str; } putchar('\n'); } int main(void) { char s[] = "ankara"; myputs(s); puts(s); myputs(s + 2); puts(s + 2); return 0; } Sınıf çalışması: Başlangıç adresiyle verilen bir yazıyı tersten yazdıran putrev fonksiyonu yazınız: void putsrev(char *str); Çözüm: #include void putsrev(char *str) { int i; for (i = 0; str[i] != '\0'; ++i) ; for (--i; i >= 0; --i) putchar(str[i]); 144 putchar('\n'); } int main(void) { char s[] = "ankara"; putsrev(s); return 0; } Sınıf Çalışması: Bir yazı içerisinde belli bir karakterden kaç tane olduğunu bulan ve o sayıya geri dönen aşağıdaki prototipe sahip sonksiyonu yazınız: int getch_count(char *str, char ch); Çözüm: #include int getch_count(char *str, char ch) { int count = 0; while (*str != '\0') { if (*str == ch) ++count; ++str; } return count; } int main(void) { char s[] = "ankara"; int result; result = getch_count(s, 'a'); printf("%d\n", result); return 0; } Fonksiyonların Geri Dönüş Değerlerinin Adres Olması Durumu Bir fonksiyonun geri dönüş değeri bir adres bilgisi olabilir. Bu durumda T bir tür belirtmek üzere geri dönüş değeri T * biçiminde belirtilmelidir. Örneğin: int *foo(void) { /* ... */ } Burada foo fonksiyonu int türden bir adrese geri döner. Tabi böyle bir fonksiyonun geri dönüş değeri aynı türden bir göstericiye atanmalıdır. Örneğin: int *pi; ... 145 pi = foo(); Örneğin bir dizideki en büyük elemanın adresiyle geri dönen getmax_addr isimli bir fonksiyonu yazacak olalım: #include int *getmax_addr(int *pi, int size) { int *pmax = &pi[0]; int i; for (i = 1; i < size; ++i) if (pi[i] > *pmax) pmax = &pi[i]; return pmax; } int main(void) { int a[10] = { 34, 21, 87, 45, 32, 67, 93, 22, 9, 2 }; int *pmax; pmax = getmax_addr(a, 10); printf("%d\n", *pmax); return 0; } Yukarıdaki örnekte dizinin en büyük elemanını değiştirmek isteyelim: #include int *getmax_addr(int *pi, int size) { int *pmax = &pi[0]; int i; for (i = 1; i < size; ++i) if (pi[i] > *pmax) pmax = &pi[i]; return pmax; } void disp_array(int *pi, int size) { int i; for (i = 0; i < size; ++i) printf("%d ", pi[i]) ; printf("\n"); } int main(void) { int a[10] = { 34, 21, 87, 45, 32, 67, 93, 22, 9, 2 }; int *pmax; disp_array(a, 10); *getmax_addr(a, 10) = 1000; disp_array(a, 10); return 0; 146 } Anahtar Notlar: C'de bir adresin sayısal bileşeni printf fonksiyonunda %p format karakteriyle yazdırılır. Örneğin: char s[10]; printf("%p\n", s); C'de Standart String Fonksiyonları C'de ismi str ile başlayan strxxx biçiminde bir grup standart fonksiyon vardır. Bu fonksiyonlara string fonksiyonları denir. Bu fonksiyonlar bir yazının başlangıç adresini parametre olarak alırlar, onunla ilgili faydalı işlemler yaparlar. Bu fonksiyonların prototipleri dosyası içerisinde bulunmaktadır. strlen fonksiyonu strlen fonksiyonu bir yazının karakter uzunluğunu bulmak için kullanılmaktadır: unsigned int strlen(char *str); Fonksiyon parametre olarak char türden bir yazının başlangıç adresini alır, geri dönüş değeri olarak bu yazının karakter uzunluğunu verir. Anahtar Notlar: strlen fonksiyonun orijinal prototipi şöyledir: size_t strlen(const char *str); size_t türü ve const göstericiler ileride ele alınacaktır. Örneğin: #include #include int main(void) { char s[100]; unsigned n; printf("Bir yazi giriniz:"); gets(s); n = strlen(s); printf("%u\n", n); return 0; } strlen fonksiyonu aşağıdaki gibi yazılabilir: #include #include unsigned mystrlen(char *str) { int i; for (i = 0; str[i] != '\0'; ++i) ; 147 return i; } unsigned mystrlen2(char *str) { int count = 0; while (*str != '\0') { ++count; ++str; } return count; } int main(void) { char s[100]; unsigned n; printf("Bir yazi giriniz:"); gets(s); n = mystrlen(s); printf("%u\n", n); n = mystrlen2(s); printf("%u\n", n); return 0; } strcpy Fonksiyonu Bu fonksiyon char türden bir dizi içerisindeki yazıyı başka bir diziye kopyalamakta kullanılır. Prototipi şöyledir: char *strcpy(char *dest, char *source); Fonksiyon ikinmci parametresiyle belirtilen adresten başlayarak null karakter görene kadar (null karakter de dahil) tüm karakterleri birinci parametresi ile başlayan adresten itibaren kopyalar. Birinci parametresiyle verilen adresin aynısına geri döner. Uygulamada geri dönüş değerine ciddi bir biçimde gereksinim duyulmamaktadır. Örneğin: #include #include int main(void) { char s[32] = "ankara"; char d[32]; strcpy(d, s); puts(d); return 0; } Örneğin: 148 #include #include int main(void) { char s[32] = "ankara"; char d[32]; strcpy(d, s + 2); puts(d); /* kara */ return 0; } strcpy fonksiyonu aşağıdaki gibi yazılabilir: #include #include char *mystrcpy(char *dest, char *source) { int i; for (i = 0; (dest[i] = source[i]) != '\0'; ++i) ; return dest; } char *mystrcpy2(char *dest, char *source) { char *temp = dest; while ((*dest = *source) != '\0') { ++dest; ++source; } return temp; } int main(void) { char s[32] = "ankara"; char d[32]; mystrcpy(d, s); puts(d); mystrcpy2(d, s); puts(d); return 0; } strcat Fonksiyonu Bu fonksiyon bir yazının sonuna başka bir yazıyı eklemek için kullanılır. Prototipi şöyledir: char *strcat(char *dest, char *source); Fonksiyon ikinci parametresiyle belirtilen adresten başlayarak null karakter görene kadar tüm karakterleri (null karakter de dahil) birinci parametresiyle belirtilen yazının sonuna null karakter ezerek kopyalar. Fonksiyon birinci 149 parametre ile belirtilen adresin aynısına geri döner. Örneğin: #include #include int main(void) { char s[32] = "ankara"; char d[32] = "izmir"; strcat(d, s); printf("%s\n", d); return 0; } strcat fonksiyonu şöyle yazılabilir: #include #include char *mystrcat(char *dest, char *source) { int i, k; for (i = 0; dest[i] != '\0'; ++i) ; for (k = 0; (dest[k + i] = source[k]) != '\0'; ++k) ; return dest; } char *mystrcat2(char *dest, char *source) { char *temp = dest; while (*dest != '\0') ++dest; while ((*dest = *source) != '\0') { ++dest; ++source; } return temp; } int main(void) { char s[32] = "ankara"; char d[32] = "izmir"; mystrcat(d, s); printf("%s\n", d); mystrcat2(d, s); printf("%s\n", d); return 0; /* izmirankaraankara*/ } Sınıf Çalışması: 1000 elemanlık char türden s imli bir dizi ve bunun yanı sıra 32 elemanlık k isimli bir dizi 150 tanımlayınız. Bir döngü içerisinde gets fonksiyonuyla k dizisine okuma yapınız. Oradan da okunanları s dizisinin sonuna ekleyiniz. Ta ki gets fonksiyonunda kullanıcı hiçbirşey girmeyip yalnızca ENTER tuşuna basana kadar. Döngüden çıkınca s dizisini yazdırınız. Çözüm: #include #include int main(void) { char s[1000]; char k[32]; s[0] = '\0'; for (;;) { printf("Isim giriniz:"); gets(k); if (*k == '\0') break; strcat(s, k); } puts(s); return 0; } strcmp Fonksiyonu strcmp fonksiyonu iki yazıyı karşılaştırmak amacıyla kullanılır. Karşılaştırma şöyle yapılır: Yazıların karakterleri eşit olduğu sürece devam edilir. İlk eşi,t olmayan karaktere gelindiğinde o karakterlerin hangisi karakter tablosunda daha büyük numaraya sahipse o yazı o yazıdan büyüktür. ASCII tablosu için şöyle örnekler verebiliriz: - "ali" yazısı "Ali" yazısından büyüktür. - "ali" yazısı "aliye" yazısından küçüktür. - "almanya" yazısı "ali" yazısından büyüktür. strcmp fonksiyonunun prototipi şöyledir: int strcmp(char *s1, char *s2); Fonksiyon birinci yazı ikinci yazıdan büyükse pozitif herhangi bir değer, küçükse negatif herhangi bir değer ve eşitse sıfır değerine geri döner. Örneğin: #include #include int main(void) { char s[64]; char passwd[] = "maviay"; printf("Enter password:"); gets(s); if (!strcmp(s, passwd)) printf("Ok\n"); 151 else printf("Invalid password\n"); return 0; } Fonksiyonu şöyle yazabiliriz: #include #include int mystrcmp(char *s1, char *s2) { while (*s1 == *s2) { if (*s1 == '\0') break; ++s1; ++s2; } return *s1 - *s2; } int main(void) { char s[64]; char passwd[] = "maviay"; printf("Enter password:"); gets(s); if (!mystrcmp(s, passwd)) printf("Ok\n"); else printf("Invalid password\n"); return 0; } strcmp fonksiyonun büyük harf küçük harf duyarlılığı olmadan karşılaştırma yapan stricmp isimli bir versiyonu da vardır. Ancak stricmp standart bir fonksiyon değildir. Fakat Microsoft derleyicilerinde, Borland derleyicilerinde ve gcc derleyicilerinde bir eklenti biçiminde bulunmaktadır. stricmp fonksiyonu şöyle yazılabilir: #include #include int mystricmp(char *s1, char *s2) { while (tolower(*s1) == tolower(*s2)) { if (*s1 == '\0') break; ++s1; ++s2; } return tolower(*s1) - tolower(*s2); } int main(void) { char s[64]; char passwd[] = "maviay"; printf("Enter password:"); 152 gets(s); if (!mystricmp(s, passwd)) printf("Ok\n"); else printf("Invalid password\n"); return 0; } NULL Adres Kavramı (NULL Pointer) NULL adres derleyici tarafından seçilmiş bir sayısal bileşene sahip olan özel bir adrestir. NULL adres C'de geçerli bir adres kabul edilmez. Başarısızlığı anlatmak için kullanılır. NULL adresin sayısal değeri standartlara göre derleyiciden derleyiciye değişebilir. Yaygın derleyicilerin hepsinde NULL adres 0 sayısal bileşenine sahip (yani belleğin tepesini belirten) adrestir. Fakat NULL adres standartlara göre farklı sistemlerde farklı biçimde olabilir. C'de 0 değerini veren tamsayı türlerine ilişkin sabit ifadeleri NULL adres sabiti olarak kullanılır. Örneğin: 5-5 0 1-1 gibi ifadeler aynı zamanda NULL adres sabiti anlamına gelmektedir. Biz bir göstericiye bu değerleri atadığımızda derleyici o sistemde hangi sayısal bileşen NULL adres belirtiyorsa göstericiye onu atar. Yani buradaki 0, 0 adresini temsil etmez. O sistemdeki NULL adresi temsil eder. Örneğin: int *pi = 0; Burada pi göstericisine int bir sıfır atanmıyor. O sistemde NULL adres neyse o değer atanıyor. NULL adres sabitini (yani sıfır değerini) her türden göstericiye atayabiliriz. Bu durumda o göstericinin içerisinde "NULL adres" bulunur. Örneğin: char *pc = 0; pc'nin içerisinde NULL adres vardır. NULL adres sabitinin türü yoktur. NULL adres sabiti (yani sıfır sayısı) her türden göstericiye atanabilir. Yukarıdaki örnekte pc göstericisi char türündendir. Fakat içerisinde NULL adres vardır. Örneğin bir sistgemde NULL adresin sayısal bileşeni 0xFFFF olsun. Biz bu sistemde göstericiye NULL adres atayabilmek için, göstericiye 0xFFFF atayamayız. Yine düz sıfır atarız. Bu düz sıfır zaten o sistemdeki NULL adres anlamına gelir. Örneğin: char *pc = 0; Burada pc'ye sıfır adresi atanmıyor, o sistemdeki NULL adres olan 0xFFFF sayısal bileşenine sahip olan adres atanıyor. Anahtar Notlar: Standartlara göre NULL adres sabiti aynı zamanda sıfır değerinin void * türüne dönüşsütülmüş biçimi de olabilir. Yani (void *)0 da NULL adres anlamına gelir. Bir adresin NULL olup olmadığı == ve != operatörleriyle öğrenilebilir. Örneğin: if (pi == 0) { 153 ... } Burada pi'nin içerisindeki adresin sayısal bileşeninin sıfır olup olmadığına bakılmamaktadır. O sistemdeki NULL adres olup olmadığına bakılmaktadır. Örneğin ilgili sistemde NULL adres 0xFFFF sayısal değerine ilişkin olsun. Ve pi'in içerisinde NULL adres olduğunu varsayalım: if (pi == 0) { ... } Burada if deyimi doğrudan sapar. if parantezinin içerisinde yalnızca bir adres ifadesi varsa bu adresin NULL adres olup olmadığına bakılır. Eğer adres NULL adres ise if deyimi yanlıştan sapar, NULL adres değilse doğrudan sapar. Örneğin: if (pi) { ... } else { ... } Bu örnekte ilgili sistemde NULL adresin 0xFFFF olduğunu düşünelim. Yukarıdaki if deyimi yanlıştan sapacaktır. Çünkü bu özel durumda adresin sayısal bileşeninin sıfır olup olmadığına değil, NULL adres olup olmadığına bakılmaktadır. Benzer biçimde C'de ! operatörünün operandı bir adres bilgisiyse bu operatör adres NULL adres ise 1 değerini, NULL adres değilse 0 değerini üretir. * ya da köşeli parantez operatörleriyle NULL adresin içeriği elde edilmeye çalışılırsa bu durum tanımsız davranışa (undefined behavior) yol açar. C'de NULL adres sabiti daha okunabilir ifade edilsin diye NULL isimli bir sembolik sabitle temsil edilmiştir: #define NULL 0 Programcılar genellikle NULL adres sabiti için düz sıfır kullanmak yerine NULL sembolik sabitini kullanırlar. Örneğin: if (pi == NULL) { ... } NULL sembolik sabiti v e pek çok başlık dosyasında bulunmaktadır. NULL sembolik sabitini int türden bir sıfır olarak değil, NULL adres sabiti olarak kullanmalıyız. strchr Fonksiyonu Bu fonksiyon bir yazı içerisinde bir karakteri aramak için kullanılır. Eğer karakter yazı içerisinde bulunursa fonksiyon karakterin ilk bulunduğu yerin adresiyle geri döner. Eğer bulunamazsa NULL adresle geri döner. Bu fonksiyon null karakteri de arayabilmektedir. char *strchr(char *str, char ch); 154 Örneğin: #include #include int main(void) { char s[] = "ankara"; char *str; str = strchr(s, 'k'); if (str == NULL) printf("karakter yok!..\n"); else printf("Buldu:%s\n", str); return 0; } strchr fonksiyonu şöyle yazılabilir: #include char *mystrchr(char *str, char ch) { while (*str != '\0') { if (*str == ch) break; ++str; } if (*str == '\0' && ch != '\0') return NULL; return str; } int main(void) { char s[] = "ankara"; char *str; str = mystrchr(s, 'k'); if (str == NULL) printf("karakter yok!..\n"); else printf("Buldu:%s\n", str); return 0; } strrchr Fonksiyonu Fonksiyon tamamen strchr fonksiyonu gibidir. Ancak ilk bulunan karakterin değil son bulunan karakterin adresiyle geri döner. Örneğin: #include #include int main(void) { char s[] = "izmir"; char *str; 155 str = strrchr(s, 'i'); if (str == NULL) printf("karakter yok!..\n"); else printf("Buldu:%s\n", str); return 0; } strrchr fonksiyonu şöyle yazılabilir: #include #include char *mystrrchr(char *str, char ch) { char *result = NULL; while (*str != '\0') { if (*str == ch) result = str; ++str; } if (ch == '\0') return str; return result; } int main(void) { char s[] = "izmir"; char *str; str = mystrrchr(s, 'i'); if (str == NULL) printf("karakter yok!..\n"); else printf("Buldu:%s\n", str); return 0; } strncpy Fonksiyonu Bu fonksiyon bir yazının ilk n karakterini başka bir diziye kopyalar. Prototipi şöyledir: char *strncpy(char *dest, char *source, unsigned n); Bu fonksiyonda eğer n değeri strlen(source) değerinden küçük ya da eşitse fonksiyon null karakteri hedef diziye eklemez. Örneğin: #include #include int main(void) { char s[] = "izmir"; char d[32] = "ankara"; 156 strncpy(d, s, 3); puts(d); /* izmara */ return 0; } Eğer n değeri strlen(source) değerinden büyükse ya da eşitse hedefe null karakter de kopyalanır. Üstelik geri kalan miktar kadar null karakter kopyalanır. Örneğin: #include #include int main(void) { char s[] = "izmir"; char d[32] = "ankara"; strncpy(d, s, 30); puts(d); /* izmir çıkacak fakat 24 tane null karakter d'ye eklenecek */ return 0; } Fonksiyon yine strcpy de olduğu gibi, kopyalamanın yapıldığı hedef adrese geri döner. strncpy fonksiyonu şöyle yazılabilir: #include #include char *mystrncpy(char *dest, char *source, unsigned n) { char *temp = dest; while (n-- > 0) { *dest = *source; if (*source != '\0') ++source; ++dest; } return temp; } int main(void) { char s[] = "izmir"; char d[32] = "ankara"; mystrncpy(d, s, 30); puts(d); /* izmir çıkacak fakat 24 tane null karakter d'ye eklenecek */ return 0; } strncat Fonksiyonu Bu fonksiyon bir yazının sonuna başka bir yazının ilk n karakterini ekler. Prototipi şöyledir: char *strncat(char *dest, char *source, unsigned n); 157 Fonksiyon her zaman null karakteri ekler. Ancak n > strlen(source) ise null karakteri ekleyip işlemini sonlandırır. (Yani strcpy'de olduğu gibi geri kalan miktar kadar null karakter eklemez.) Başka bir deyişle eğer n > strlen(source) ise fonksiyon tamamen strcat gibi çalışır. Fonksiyon yine birinci parametresiyle belirtilen adresin aynısına geri döner. Örneğin: #include #include int main(void) { char s[] = "izmir"; char d[32] = "ankara"; strncat(d, s, 3); puts(d); /* ankaraizm */ return 0; } Örneğin: #include #include int main(void) { char s[] = "izmir"; char d[32] = "ankara"; strncat(d, s, 100); puts(d); /* ankaraizmir çıkar fakat dizi taşması olmaz */ return 0; } strncat fonksiyonu şöyle yazılabilir: #include #include char mystrncat(char *dest, char *source, unsigned n) { char *temp = dest; while (*dest != '\0') ++dest; while ((*dest = *source) != '\0' && n > 0) { ++dest; ++source; --n; } /* if (n == 0) */ *dest = '\0'; return temp; } int main(void) { char s[] = "izmir"; char d[32] = "ankara"; 158 mystrncat(d, s, 50); puts(d); /* ankaraizmir çıkar fakat dizi taşması olmaz */ return 0; } strncmp Fonksiyonu Bu fonksiyon iki yazının ilk n karakterinmi karşılaştırmakta kullanılmaktadır. Fonksiyonun prototipi şöyledir: int strncmp(char *s1, char *s2, unsigned n); Fonksiyonun üçüncü parametresi ilk kaç karakterin karşılaştırılacağını belirtir. n sayısı büyükse iki yazıdan hangisinde null karakter görülürse işlem biter. Fonksiyonun geri dönüş değeri yine strcmp fonksiyonunda olduğu gibidir. Örneğin: #include #include int main(void) { char passwd[] = "maviay"; char s[64]; printf("Enter password:"); gets(s); if (!strncmp(passwd, s, strlen(s))) printf("Ok\n"); else printf("invalid password\n"); return 0; } Adres Operatörleriyle ++ ve -- Operatörlerinin Birlikte Kullanılması 1) * Operatörüyle Kullanım a) ++*p Kullanımı: Burada *p bir artırılır. Yani bu ifade *p = *p + 1 ile eşdeğerdir. #include int main(void) { int a = 10; int *pi = &a; ++*pi; printf("%d\n", a); /* 11 */ return 0; } b) *++p Durumu: Burada önce p bir artırılır sonra artırılmış adresin içeriğine erişilir. Örneğin: 159 #include int main(void) { int a[2] = { 10, 20 }; int *pi = a; *++pi = 30; printf("%d\n", a[1]); /* 30 */ return 0; } c) *p++ Durumu: Burada p bir artırılır ancak ++ sonek durumunda olduğu için artırılmış adresin içeriğine erişilir. Örneğin: #include char *mystrcpy(char *dest, char *source) { char *temp = dest; while ((*dest++ = *source++) != '\0') ; return temp; } int main(void) { char s[] = "ankara"; char d[32]; mystrcpy(d, s); puts(d); /* ankara */ return 0; } Görüldüğü gibi strcpy işlemi aşağıdaki gibi kompakt yazılabilmektedir: while ((*dest++ = *source++) != '\0') ; Burada her defasında *source değeri *dest değerine atanır ve source ile dest bir artılmış olur. 2) [] Operatörü İle Kullanım a) ++p[n] Durumu: Burada p[n] bir artırılır. Yani bu ifade p[n] = p[n] + 1 ile eşdeğerdir. b) p[n]++ Durumu: Burada p[n] bir artırılır ancak sonraki operatöre p[n]'in artırılmamış değeri sokulur. c) p[++n] Durumu: Burada n bir artırılır ve artırılmış indeksteki elemaana erişilir. d) p[n++] Durumu: Burada n bir artırılır fakat p[n] sonraki işleme sokulur. Örneğin: 3) & Operatörü İle Kullanım 160 & adres operatörü ile ++ ve -- operatörleri birlikte kullanılamazlar. Çünkü & operatörünün ürettiği değer nesne değildir. Örneğin: int a; ++&a; /* geçerli değil */ ++ ve -- operatörleri nesne üretmezler. Yani ++a işleminde a bir artırılır, fakat ürün olarak a elde edilmez. a artırılmış değeri elde edilir. Benzer biçimde sonrek durumunda aynı şey söz konusudur. Örneğin: a++ = b; /* geçerli değil */ Dolayısıyla &++a ve &a++ ifadeleri de anlamsızdır. Ancak nesnelerin adresleri alınabilir. Farklı Türden Adresin Bir Göstericiye Atanması C'de bir göstericiye farklı türden bir adres atanamaz. Örneğin: char s[] = "ankara"; int *pi; pi = s; /* geçersiz */ Anahtar Notlar: Yukarıdaki atama C'de geçersizdir. Fakat bilindiği gibi geçersiz bir progrtamı C derleyicileri bir mesaj veremek koşuluyla derleyebilir. Yani bir derleyicinin yukarıdaki kodu derlemesi onun insafına kalmıştır. Fakat bir göstericiye farklı türden bir adres tür dönüştürme operatörüyle atanabilir. Örneğin: char s[] = "ankara"; int *pi; pi = (int *)s; /* geçerli */ Örneğin: #include int main(void) { char s[] = "aaaa"; int *pi; pi = (int *)s; printf("%d\n", *pi); /* buradaki sayı kaçtır? */ return 0; } Bir adresi başka türden bir göstericiye tür dönüştürme operatöryle atamak çoğuı zaman anlamsızdır. Örneğin yukarıdaki kodda artık *pi ifadesi dört tane 'a' karakterinin oluşturduğu int değer olarak yorumlanır. Böyle bir şeyi neden yapmak isteyebilriz? Bazı durumlarda böylesi işlemlere gereksinim duyulabilmektedir. Adres Olmayan Bir Bilginin Bir Göstericiye Atanması Bazen elimizde bir tamsayı değeri vardır. Bu değeri sayısal bileşen anlamında bir göstericiye yerleştirmek 161 isteyebiliririz. Örneğin: int *pi; int a = 0x1FC0; pi = a; /* geçersiz! */ Bu tür atamalar da tür dönüştürme operatörüyle yapılabilmektedir: int *pi; int a = 0x1FC0; pi = (int *)a; /* geçerli! */ Örneğin: int *pi; pi = 0x1FC0; pi = (int *)0x1FC0; /* geçersiz! */ /* geçerli */ Dizi İsimleriyle Göstericilerin Karşılaştırılması Dizi isimleriyle göstericiler bazen kullanım bakımından yeni öğreneleri tereddütte bırakabilmektedir. Aralarındaki benzerlikler ve farklılıklar şöyledir: Açıklamalarda aşağıdaki iki bildirimi göz önünde bulundurunuz: int a[10]; int *pi; - Dizi isimleri de göstericiler de birer adres belirtir. Yani ikisini de kullandığımızda biz adres bilgisi kullanmış oluruz. Fakat göstericiler bir nesne belirtir. Yani onun içerisine bir adres bilgisi atayabiliriz. Ancak dizi isimleri o dizilerin başlangıç adreslerini belirtir fakat nesne belirtmez. Biz bir dizi ismine birşey atayamayız. Örneğin: char s[10]; char *pc; s = pc; pc = s; /* geçersiz! */ /* geçerli */ - Dizi isimleri dizilerin başlangıç adresi belirtir. Yani onların belirttiği adreste tahsis edilmiş bir alan vardır. Halbuki göstericilerin içerisindeki adresler değiştirilebilir. Göstericiler bellekte başka yerleri gösterebilir hale getirilebilir. Gösterici Hataları Bir gösterici bellekte potansiyel olarak her yeri gösterebilir. Fakat bir göstericiyi kullanarak * ya da [] operatörleriyle bellekte bizim için tahsis edilmemiş (yani bize ait olmayan) alanlara erişmek normal bir durum değildir. Bu durum C'de tanımsız davranışa (undefined behavior) yol açmaktadır. elimiz bir gösterici olsun biz parmağımızla başkasına ait bir araziyi gösterebiliriz. Bu yasak değildir. Fakat oraya erişemeyiz (yani giremeyiz). Girersek başımıza ne geleceği belli değildir. Tabi biz kendi arazimizi göstererek oraya girebiliriz. İşte göstericiler 162 de tıpkı böyledir. Biz göstericilerle kendi tanımladığımız yani bizim için ayrılan nesnelere erişmeliyiz. Peki bir yerin bizim için ayrıldığını nasıl anlarız? İşte tanımlama yoluyla oluşturduğumuz nesneler bize tahsis edilmiştir. (Yani onların tapusu bizdedir.) Biz o alanlara erişebiliriz. Örneğin: int a; char s[100]; Burada &a'dan itibaren 4 byte, s'ten itibaren 100 byte bize ayrılmıştır. Biz bu alanları istediğimiz gibi kullanabiliriz. Anahtar Notlar: Bir fonksiyona biz bir adres geçirelim. Fonksiyon onun tahsis edilmiş bir adres olup olmadığını anlayamaz. Bir adresin tahsis edilmiş olup olmadığını anlamanın C'de resmi bir yolu yoktur. Bizim için tahsis edilmemiş alanlara erişme işlemine "gösterici hatası" denilmektedir. Gösterici hataları maalesef deneyimli programcılar tarafından bile yanlışlıkla yapılabilmektedir. Gösterici hatalarının tipik ortaya çıkış biçimi şunlardır: 1) İlkdeğer Verilmemiş Göstericilerin Yol Açtığı Gösterici Hataları: Bir göstericinin içerisinde rastgele bir değer varsa onu * ya da [] parantez operatörleriyle kullanamayız. Örneğin: #include int main(void) { int *pi; *pi = 100; /* 100 rastgele bir yere atanıyor */ return 0; } Örneğin: #include int main(void) { char *s; gets(s); /* Gösterici hatası! gets klavyeden girilen karakterleri nereye yerleştiriyor? */ return 0; } 2) Dizi Taşmalarından Doğan Gösterici Hataları: Bir dizi için tam o kadar yer ayrılmıştır. Dizinin gerisi de ötesi de bizim tahsis edilmemiştir. Oralara erişmek gösterici hatalarına yol açar. Örneğin: int a[10]; int i; ... for (i = 0; i <= 10; ++i) a[i] = 0; /* dikkat a[10] bizim için tahsis edilmemiş */ Örneğin: char s[] = "ankara"; char d[] = "istanbul"; strcat(d, s); /* dikkat d dizisi taşıyor! */ 163 Örneğin: char s[10]; gets(s); Burada kullanıcı en fazla 9 karakter girmelidir. Yoksa dizi taşar. 3) Ömrü Biten Nesnelerin Yol Açtığı Gösterici Hataları: Bir fonksiyon yerel bir nesnenin ya da dizinin adresi ile geri dönmemelidir. Çünkü fonksiyon bitince o fonksiyonun yerel değişkenleri boşaltılır. Dolayısıyla fonksiyonun bize verdiği adres artık tahsis edilmiş bir alanın adresi olmaz. Örneğin: #include char *getname(void) { char name[128]; printf("Adi Soyadi:"); gets(name); return name; } int main(void) { char *s; s = getname(); printf("%s\n", s); return 0; } Burada getname fonksiyonu geri döndüğünde artık name isimli dizi yok edilecektir. Dolayısıyla main'de s göstericisine atanan adres tahsis edilmemiş bir alanın adresi olacaktır. Yukarıdaki fonksiyonda name global yapılırsa sorun kalmaz. Ya da fonksiyon şöyle düzenlenebilir: #include void getname(char *s) { printf("Adi Soyadi:"); gets(s); } int main(void) { char s[128]; getname(s); printf("%s\n", s); return 0; } void Adresler ve void Göstericiler C'de void türden bir değişken tanımlanamaz. Ancak gösterici tanımlanabilir. Örneğin: 164 void a; void *pv; /* geçersiz! */ /* geçerli */ void göstericiler türsüz göstericilerdir. void göstericiler * ya da [] operatörleriyle kullanılamazlar. Çünkü bu operatörler eriştiği yerdeki nesnenin türünü bilmek zorundadır. void göstericiler ya da void adresler artırılamazlar ve eksiltilemezler. Çünkü derleyici bu işlemlerde göstericinin sayısal bileşenini kaç artırıp kaç eksilteceğini bilememektedir. Peki void göstericiler ne işe yarar? void bir göstericiye herhangi türden bir adres doğrudan atanabilir. Tür dönüştürme operatörüne hiç gerek yoktur. Örneğin: void *pv; char s[10]; int a[10]; pv = s; pv = a; /* geçerli */ /* geçerli */ Benzer biçimde void bir adres de herhangi bir türden göstericiye atanabilir. Örneğin: void *pv; int *pi; char s[10]; pv = s; ... pi = pv; /* geçerli */ /* geçerli */ void göstericilere neden gereksinim duyulduğunu açıklayabilmek için memcpy fonksiyonu iyi bir örnek olabilir. memcpy fonksiyonu bir adresten bir adrese koşulsuz n byte kopyalamaktadır. Bu fonksiyon strcpy fonksiyonunu andırmakla birlikte ondan farklıdır. memcpy null karaktere bakmaz. Koşulsuz n byte kopyalar. Örneğin herhangi bir türden diziye aynı türden başka bir diziye memcpy ile kopyalayabiliriz. C'de başı memxxx biçiminde başlayan fonksiyonların prototipleri de içerisindedir. #include #include int main(void) { int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int b[10]; int i; memcpy(b, a, 10 * sizeof(int)); for (i = 0; i < 10; ++i) printf("%d ", b[i]); printf("\n"); return 0; } Biz memcpy fonksiyonuyla herhangi türden iki diziyi kopyalayabiliriz: #include #include int main(void) { double a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; 165 double b[10]; int i; memcpy(b, a, 10 * sizeof(double)); for (i = 0; i < 10; ++i) printf("%f ", b[i]); printf("\n"); return 0; } İşte memcpy gibi bir fonksiyonun bizden her türlü adresi kabul edebilmesi için onun parametrelerinin void gösterici olması gerekir. Gerçekten de memcpy fonksiyonunun prototipi şöyledir: void *memcpy(void *dest, void *source, unsigned n); Fonksiyon ikinci parametresiyle belirtilen adresten başlayarak birinci parametresiyle belirtilen adrese koşulsuz n byte kopyalar. Birinci parametresiyle belirtilen adresin aynısına geri döner. Peki biz memcpy gibi bir fonksiyonu nasıl yazabiliriz? void göstericiler artırılıp azaltılamadığına göre onların türü belirli bir göstericiye atanması gerekir. Örneğin: #include #include void *mymemcpy(void *dest, void *source, unsigned n) { char *pcdest = dest; char *pcsource = source; while (n-- > 0) *pcdest++ = *pcsource++; return dest; } int main(void) { double a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; double b[10]; int i; mymemcpy(b, a, 10 * sizeof(double)); for (i = 0; i < 10; ++i) printf("%f ", b[i]); printf("\n"); return 0; } memcpy fonksiyonuyla yazı kopyalaması da yapabiliriz. Örneğin: strcpy(d, s); ile memcpy(d, s, strlen(s) + 1); 166 işlevsel olarak eşdeğerdir. Örneğin: #include #include int main(void) { char s[30] = "ankara"; char d[30]; strcpy(d, s); memcpy(d, s, strlen(s) + 1); return 0; } memset isimli fonksiyon bir adresten başlayarak koşulsuz n byte'a belli bir değeri atar. Yani n tane byte'ı belli bir değerle doldurur. Örneğin: #include #include int main(void) { int a[10]; int i; memset(a, 0, 10 * sizeof(int)); for (i = 0; i < 10; ++i) printf("%d ", a[i]); printf("\n"); return 0; } Örneğin: #include #include int main(void) { char s[200]; memset(s, 'a', 199); s[199] = '\0'; printf("%s\n", s); return 0; } memset fonksiyonunun prototipi şöyledir: void *memset(void *dest, int val, unsigned n); Fonksiyonun birinci parametresi doldurulacak yerin adresini, ikinci parametresi doldurulacak değeri ve üçüncü parametresi kaç adet değerin doldurulacağını belirtir. memset fonksiyonunu şöyle yazabiliriz: #include #include void *mymemset(void *dest, int val, unsigned n) 167 { unsigned char *pcdest = dest; while (n-- > 0) *pcdest++ = ch; return dest; } int main(void) { char s[200]; mymemset(s, 'a', 199); s[199] = '\0'; printf("%s\n", s); return 0; } memset fonksiyonunun strset isimli bir kardeşi vardır. strset char türden bir diziyi null karakter görene kadar belli bir karakterle doldurur: char *strset(char *dest, char ch); Örneğin: #include #include int main(void) { char s[] = "ankara"; strset(s, 'x'); puts(s); return 0; } Anahtar Notlar: C'de strxxx biçiminde str ile başlayan fonksiyonlar char * türünden göstericileri parametre olarak alır ve null karakter görene kadar işlem yapar. Fakat memxxx biçiminde mem ile başlayan fonksiyonlar void * türünden parametre alır ve koşulsuz n byte için işlem yapar. strcpy --- memcpy strset --- memset strchr --- memchr strset fonksiyonu yerine memset kullanılabilir. Yani: strset(s, ch); ile, memset(s, ch, strlen(s)); işlevsel olarak eşdeğerdir. Yukarıda da belirtildiği gibi C'de void bir göstericiye her türden adres doğrudan atanabilir. Benzer biçimde void bir adres de her türlü göstericiye atanabilir. Ancak void adresin herhangi bir göstericiye atanması bazılarınca 168 eleştirilmektedir. Çünkü bu sayede aslında yasak olan birşeyi yapabilir duruma gelmekteyiz (yani hülle yapılabilir): char *pc; int *pi; pi = pc; /* yasak */ Fakat: char *pc; int *pi; void *pv; pv = pc; pi = pv; /* geçerli */ /* geçerli */ İşte C++'ta eleştirilen bu durum düzeltilmiştir. Şöyle ki: C++'ta yine void göstericiye her türden adres doğrudan atanabilir. Fakat void bir adres tür dönüştürmesi yoluyla başka türden bir göstericiye atanabilir. Böylece yukarıdaki kod C'de geçerli olduğu halde C++'ta geçerli değildir. Bazı C programcıları böylesi kodların her iki dilde de geçerli olmasını sağlamak için void adresleri atarken tür dönüştürmesi yaparlar. Örneğin: pi = (int *)pv; /* Hem C'de hem de C++'ta geçerli */ Özetle C'de void göstericiler bir adresin yalnızca sayısal bileşenini tutmak için ve tür dönüştürmesine programcıyı zorlamamak için kullanılmaktadır. Eğer memcpy fonksiyonunun parametreleri char * türünden olsaydı. O zaman onu biz şöyle kullanmak zorunda kalırdık: int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; int b[10]; int i; memcpy((char *)b, (char *)a, 10 * sizeof(int)); void göstericiler C'ye 80'li yılların ikinci yarısında katılmıştır. Anahtar Notlar: Kursun bu noktasında koşul operatörü anlatılmıştır. Fakat notlarda "Diziler" konusundan sonraya eklenmiştir. Göstericilerin Uzunlukları Bir gösterici kaç byte uzunluktadır? Göstericilerin uzunlukları onların türleriyle ilgili değildir. Çünkü göstericilerin türleri onların gösterdikleri yerdeki nesnenin uzunluğuyla ilişkilidir. Bir göstericinin uzunluğu o sistemin adres alanına bağlıdır. Örneğin teorik olarak 32 bit işlemcilere 4GB RAM, 64 bit işlemcilere 16EB RAM takılabilir. Bu durumda 32 bit sistemlerdeki göstericiler 4 byte, 64 bit sistemlerdeki göstericiler 8 byte yer kaplarlar. Biz kursumuzda şekilleri genel olarak 32 bit mikroişlemcilere göre çiziyoruz. Yani örneğin sizeof(char *) ile, sizeof(double *) aynı değerlerdedir. String İfadeleri C'de derleyici ne zaman iki tırnak içerisinde bir yazı görse önce o yazıyı char türden bir dizinin içerisine yerleştirir, sonuna null karakteri ekler ve iki tırnak yerine o dizinin başlangıç adresini insert eder. Yani C'de iki tırnak ifadeleri char türnden adres belirtmektedir. Böylece C'de iki tırnak ifdadeleri doğrudan char türdnen bir göstericiye atanmalıdır. Örneğin: #include 169 int main(void) { char *str; str = "ankara"; printf("%s\n", str); return 0; } Eşdeğeri aşağıdaki gibi düşünülebilir: #include static char compiler_generated_name[] = "ankara"; int main(void) { char *str; str = compiler_generated_name; printf("%s\n", str); return 0; } Bu durumda örneğin biz char * parametreli bir fonksiyonu string ifadesiyle çağırabiliriz. Örneğin: #include int main(void) { unsigned n; n = strlen("ankara"); printf("%u\n", n); puts("ankara"); return 0; } Örneğin char türden bir diziye sonradan (ilkdeğer vererek değil) bir yazı yerleştirecek olsak bunu karakter karakter yerleştirmek pek uygun olmaz. Bunun en pratik ve doğru yolu strcpy fonksiyonunu kullanmaktadır: #include #include int main(void) { char s[32]; strcpy(s, "ankara"); puts(s); return 0; } char türden bir dizi ismine iki tırnak ifadesini atayamayız. Çünkü dizi isimleri nesne belirtmez. Göstericler nesne belirtir. Örneğin: char s[32]; 170 char *ps; s = "ankara"; ps = "ankara"; /* geçersiz! */ /* geçerli */ C'de ist,sna olarak char türden bir diziye iki tırnak ifadesi ile ilkdeğer verilirken kullanılan iki tırnak bir adres belirtmemektedir. Bu iki tırnak içindeki karakterleri tek tek diziye yerleştir ve sonuna null karakter ekle anlamına gelir. Yani: char s[32] = "ankara"; bildiriminde bu iki tırnak ifadesi için derleyici bir adres insert etmeyecektir. Bu istisna durum aşağıdakiyle eşdeğerdir: char s[32] = {'a', 'n', 'k', 'a', 'r', 'a', '\0'}; Bunun dışındaki iki tırnak ifadelerinin hepsi birer string belirtir ve bunlar için derleyici birer adres insert eder. Örneğin: char *ps = "ankara"; Burada "ankara" bir adres belirtmektedir. C'de iki tırnak ifadeleri ile oluşturulan string'ler güncellenmeye çalışılmamalıdır. String karakterlerinin değiştirilmesi "tanımsız davranışa (undefined behavior)" yol açar. Örneğin: #include int main(void) { char *str = "ankara"; char s[] = "ankara"; *str = 'x'; *s = 'x'; /* undefined behavior */ /* normal */ return 0; } Örneğin: #include #include int main(void) { char s[] = "ankara"; char *str = "ankara"; *strchr(s, 'k') = 'x'; puts(s); *strchr(str, 'k') = 'x'; puts(s); /* undefined behavior */ return 0; } 171 String'ler statik ömürlü nesnelerdir. Yani hangi fonksiyonda oluşturulmuş olursa olsunlar programın başından sonuna kadar bellekte kalırlar. Dolayısıyla bir fonksiyonun bir string'in başlangıç adresine geri dönmesi gösterici hatası değildir. Örneğin: #include int main(void) { char *str; str = getstr(); printf("%s\n", str); return 0; } Aşağıdaki örnekte iki tırnak istisna olan duruma ilişkindir. Dolayısıyla burada dizinin adresiyle geri dönülmektedir. O da fonksiyon bittiğinde yok edilir. #include char *getstr(void) { char s[] = "ankara"; return s; } int main(void) { char *str; str = getstr(); /* dikkat! gösterici hatası */ printf("%s\n", str); return 0; } Özdeş string'ler için aynı yerin mi kullanılacağı ya da farklı yerler mi ayrılacağı C'de belirsiz (unspecied) davranış olarak ele alınmaktadır. Yani derleyici bu ikisinden birini yapabilir. Bu durum derleyicileri yazanların isteğine bırakılmıştır. Örneğin: #include int main(void) { char *s = "ankara"; char *k = "ankara"; printf(s == k ? "Evet\n" : "Hayir\n"); return 0; } Burada derleyici bir tane "ankara" yazısı yerleştirip s ve k'ya onu atayabilir. Ya da iki farklı "ankara" yazısı yerleştirip s ve k'ya farklı adresler atayabilir. Bazı derleyiciler de derleyici ayarlarından programcı bunu değiştirebilmektedir. Boş bir string oluşturulabilir. Bu durumda derleyici diziye yalnızca null karakteri yerleştirir. Örneğin: 172 char *s = ""; /* geçerli */ String'ler tek bir satır üzerine yazılmak zorundadır. Örneğin: s = "this is a test"; Böyle bir yazım geçersizdir. Peki ya string bir satıra sığmıyorsa ne olacaktır? C'de yan yana iki string atomu (yani aralarında boşluk karakterlerinden başka bir karakter olmayan) derleme aşamasında derleyici tarafından birleştirilmektedir. Örneğin: #include int main(void) { char *s; s = "this is a" " test"; printf("%s\n", s); return 0; } Yukarıdaki yazım tamamen geçerlidir. Anahtar Notlar: C'de tek bir tes bölü ve hemen arkasından ENTER ('\n') karakteri "aşağıdaki satırı bu starırla birleştir" anlamına gelir. Örneğin: #include int main(void) { int count; cou\ nt = 10; printf("%d\n", count); return 0; } Yukarıdaki örnekte cou ve aşağısındaki satır birleştirilmiş ve count = 10 haline gelmiştir. Bu işlemin mümkün olması için '\'den sonra hemen '\n' karakterinin gelmesi gerekir. Bu özelliği biz string'ler için de kullanabiliriz. Örneğin: #include int main(void) { char *s; s = "ankara,\ izmir"; printf("%s\n", s); return 0; } String ifadeleri içerisinde '\' karakterleri '\' anlamına gelmez. Bunlar yanındak, karakterlerle başka bir karakter anlamına gelir. Dolayısıyla '\' karakterinin kendisi '\\' biçiminde ifade edilmelidir. Örneğin: #include 173 int main(void) { char *path = "c:\\windows\\temp"; printf("%s\n", path); return 0; } Adreslerin Karşılaştırılması C'de adresler altı karşılaştırma operatörüyle de karşılaştırılabilir. Örnğin: int *p, *q; if (p > q) { ... } Ancak karşılaştırılan adreslerin aynı türden olması ya da birinin void türden olması gerekir. Fakat C'de rastgeele iki adresin karşılaştırılması "tanımsız davranış" olarak değerlendirilir. İki adresin karşılaştırılabilmesi için bunların aynı dizinin ya da yapının elemanları olması gerekir. Örneğin: #include void putsrev(char *s) { char *end = s; while (*end != '\0') ++end; --end; while (end >= s) { putchar(*end); --end; } putchar('\n'); } int main(void) { char *s = "ankara"; putsrev(s); return 0; } Gösterici Dizileri (Array of Pointers) Her elemanı bir gösterici olan dizilere gösterici dizileri denir. Gösterici dizileri T bir tür belirtmek üzere, T * [uzunluk]; biçiminde bildirilir. Örneğin: int *a[10]; Bu dizini her elemanı int türden bir göstericidir. Yani dizinin her elemanı int * türündendir. Örneğin: 174 #include int main(void) { int x = 10, y = 20, z = 30; int *a[3]; int i; a[0] = &x; a[1] = &y; a[2] = &z; for (i = 0; i < 3; ++i) printf("%d\n", *a[i]); return 0; } C'de en fazla karşımıza çıkan char türden göstericileridir. Yazıların başlangıç adresleri char türden bir gösterici dizisine yerleştirilirse dizi yazıları tutar hale gelir. Örneğin: #include int main(void) { char *names[5]; int i; names[0] = "ali"; names[1] = "veli"; names[2] = "selami"; names[3] = "ayse"; names[4] = "fatma"; for (i = 0; i < 5; ++i) printf("%s\n", names[i]); return 0; } 175 Dizilere küme parantezleri içeriisnde ilkdeğer verebildiğimize göre aşağıdaki ilkdeğerleme de geçelidir: #include int main(void) { char *names[5] = { "ali", "veli", "selami", "ayse", "fatma" }; int i; for (i = 0; i < 5; ++i) printf("%s\n", names[i]); return 0; } Bu dizinin sonuna NULL adres (Null karakter değil) yerleştirirsek, NULL adres görene kadar işlem yapabiliriz: #include int main(void) { char *names[] = { "ali", "veli", "selami", "siracettin", "ayse", "fatma", NULL }; int i; for (i = 0; names[i] != NULL; ++i) printf("%s\n", names[i]); return 0; } Sınıf Çalışması: char türdne bir gösterici dizisinin içerisine birtakım isimlerin adresleri yerleştirimiştir. Gösterici dizisinin sonunda NULL adres vardır. char türden geniş bir dizi açınız, bu isimleri aralarına ',' karakteri koyarak tek bir dizide (tabi sonu null karakter ile bitmeli) birleştiriniz. Sonra da bu diziyi puts ile yazdırınız. Çözüm: #include #include int main(void) { char *names[] = { "ali", "veli", "selami", "siracettin", "ayse", "fatma", NULL }; char str[100]; int i; str[0] = '\0'; for (i = 0; names[i] != NULL; ++i) { if (i != 0) strcat(str, ", "); 176 strcat(str, names[i]); } puts(str); return 0; } sprintf Fonksiyonu sprintf fonksiyonu kullanım bakımından tamamen printf gibidir. Ancak ekrana yazmak yerine yazılacakları char türden bir dizinin içerisine yazar. Fonksiyonun birinci parametresi char türden dizinin adresidir. Fonksiyonun prototipi şöyledir: int sprintf(char *s, char *format, ...); Fonksiyonun birinci parametresi dizinin başlangıç adresini alır. Diğer parametreleri printf ile aynıdır. Örneğin: #include int main(void) { char s[100]; int a = 10; double b = 12.345; sprintf(s, "a = %d, b = %f\n", a, b); puts(s); return 0; } Sınıf Çalışması: Üç basamklı int türden bir sayıyı yazı biçiminde adresi aldığı char türdne bir dizinin içerisine yerleştiren num_to_text isimli fonksiyonu yazınız. char *num_to_text(int val, char *s); Fonksiyon birinci parametresiyle belirtilen değeri yazıya dönüştürerek ikinci parametresi ile aldığı diziye yerleştirir. Fonksiyon ikinci parametresiyle aldığı adresin aynısına geri döner. char s[100]; num_to_text(983, s); puts(s); /* dokuz yüz seksen üç */ Çözüm: #include #include char *num_to_text(unsigned long val, char *s); int main(void) { char s[100]; unsigned long val; for (;;) { printf("Sayi:"); scanf("%lu", &val); 177 if (val == 123) break; num_to_text(val, s); puts(s); /* dokuz yüz seksen üç */ } return 0; } char *num_to_text(unsigned long val, char *s) { 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; *s = '\0'; hundred = val / 100; ten = val % 100 / 10; one = val % 10; if (!val) { strcat(s, "sifir"); return; } if (hundred) { if (hundred != 1) { strcat(s, ones[hundred - 1]); strcat(s, " "); } strcat(s, "yuz"); } if (ten) { if (hundred) strcat(s, " "); strcat(s, tens[ten - 1]); } if (one) { if (ten) strcat(s, " "); strcat(s, ones[one - 1]); } } Soruyu genelleştirerek de çözebiliriz: #include #include char *num_to_text(unsigned long val, char *s); int main(void) { char s[100]; unsigned long val; for (;;) { printf("Sayi:"); scanf("%lu", &val); if (val == 123) break; num_to_text(val, s); puts(s); /* dokuz yüz seksen üç */ } 178 return 0; } char *num_to_text(unsigned long val, char *s) { char *ones[] = { "bir", "iki", "uc", "dort", "bes", "alti", "yedi", "sekiz", "dokuz" }; char *tens[] = { "on", "yirmi", "otuz", "kirk", "elli", "altmis", "yetmis", "seksen", "doksan" }; char *others[] = { "bin", "milyon", "milyar", "trilyon", "katrilyon", "katrilyar" }; int one, ten, hundred; int digits3[5], temp; int i; *s = '\0'; temp = val; for (i = 0; val; ++i) { digits3[i] = val % 1000; val /= 1000; } val = temp; if (!val) { strcat(s, "sifir"); return; } while (--i >= 0) { hundred = digits3[i] / 100; ten = digits3[i] % 100 / 10; one = digits3[i] % 10; if (hundred) { if (hundred != 1) { strcat(s, ones[hundred - 1]); strcat(s, " "); } strcat(s, "yuz"); } if (ten) { if (hundred) strcat(s, " "); strcat(s, tens[ten - 1]); } if (one) { if (ten) strcat(s, " "); if (i != 1 || digits3[i] != 1) strcat(s, ones[one - 1]); } if (i != 0) { strcat(s, " "); strcat(s, others[i - 1]); strcat(s, " "); } } } exit Fonksiyonu exit prototipi dosyasında olan standartd bir C fonksiyonudur. exit çağrıldığında program sonlanır. Yani sanki main bitmiş gibi bit etki oluşur. Bu durumda biz bir programı sonlandırmak için illa da akışın main'e geri dönmesini beklemek zorunda değiliz. Herhangi bir zaman herhangi bir fonksiyonda exit fonksiyonunu çağırırsak program sonlanır. Fonksiyonun prototipi şöyledir: 179 #include void exit(int code); Fonksiyon parametre olarak programın exit kodunu alır. İşletim sistemlerinde her sonlanan programın bir exit kodu vardır. Bu exit kodunun kaç olduğunun bir önemi yoktur. Bu kod işletim sistemine iletilir. İşletim sistemi de birisi (genellikle üst proses) isterse bunu ona verir. Ancak geleneksel olarak başarılı sonlanmalar için sıfır değeri, başarısz sonlanmalar için sıfır dışı değerler kullanılmaktadır. Hatta okunabilirliği artırmak için içerisinde iki sembolik sabit bildirilmiştir: #define EXIT_SUCCESS #define EXIT_FAILURE 0 1 Her ne kadar C standartlarında EXIT_SUCCESS ve EXIT_FAILURE sembolik sabitlerinin değerleri 0 ve 1 olarak kesin bir biçimde belirtilmemişse de derleyicilerin hemen hepsi EXIT_SUCCESS için 0 değerini, EXIT_FAILURE için 1 değerini kullanmaktadır. main fonksiyonu sonlandığında program sonlanmaktadır. O halde main fonksiyonun da return işlemi programın sonlanmasına yol açar. İşte C standartlarına göre main bittiğinde main fonksiyonunun geri dönüş değeri alınarak exit çağrılır. Yani program aslında her zaman exit ile sonlandırılır. O halde bir C programının şöyle çalıştırıldığı düşünülmelidir: exit(main()); Ayrıca C'de main fonksiyonuna özgü olmak üzere, main fonksiyonunda hiç return kullanılmazsa sanki return 0 kullanılmış gibi işlem görür. Prosesin Adress Alanı Ve Bölümleri İşletim sistemi çalıştıracağı progamı ikincil bellekten alarak fiziksel belleğe yükler. Bu program için bellekte belli bir yer ayırmaktadır. Buna prosesin adres alanı (address space) denir. Proseisn adres alanı içerisinde şu bölümler vardır: Stack Bölümü: Yerel ve parametre değişlkenlerinin yaratıldığı ve yok edildiği alana stack denir. Stack'te nesneler çok hızlı yaratılır ve yok edilir. Bloktan çıkıldığında o blok içerisinde tanımlanmış olan nesnelerin hepsi stack'ten atılır. Data Bölümü: İlkdeğer verilmiş global nesnelerin yaratıldığı bölümdür. Data bölümü programın başından sonuna kadar kalır. Burada yaratım ve boşaltım yapılmaz. Program çalışırken artık bu bölümde tahsisat yapılamaz. BSS Bölümü: İlkdeğer verilmemiş global nesnelerin yaratıldığı bölümdür. BSS bölümü programın başından sonuna kadar kalır. Burada yaratım ve boşaltım yapılmaz. Program çalışırken artık bu bölümde tahsisat yapılamaz. Heap Bölümü: Dinamik bellek fonksiyonlarıyla tahsisat yapılan bölümdür. Heap'te alan tahsisatı programın çalışma zamanı sırasında yapılır ve yok edilir. Tahsisat yapılırken ve serbest bırakılırken nerelerin boş ve doldu olduğu sorgulandığı için stack'teki tahsisata göre çok yavaştır. Ancak bu bölümde yapılan tahisatlar bloktan çıkıldığında otomatik geri bırakılmaz. Yani yaşamaya devam eder. Sistemlerde en küçük alan stack olma eğilimindedir. Bunu daha sonra data ve bss alanları sonra da heap alanı izler. En büyük alan heap olma eğilimindedir. Dinamik Bellek Yönetimi (Dynamic Memory Management) 180 Bilindiği gibi C'de dizi uzunlukları sabit ifadesi biçimde verilmek zorundadır. Fakat bazen bir dizinin hangi uzunlukta açılacağı işin başında belli değildir. Ancak program çalışırken birtakım olaylar sonucunda dizinin uzunluğu bilinmektedir. İşte bu tür durumlarda programın çalışma zamanı sırasında dizi açmaya gereksinim duyulur. Programın çalışma zamanı sırasında bellek tahsis etme işlemine "dinamik bellek yönetimi" denilmektedir. C'de dinamik bellek yönetimi ismine "dinamik bellek fonksiyonları" denilen bir grup standart C fonksiyonuyla gerçekleştirilir. Bunlar malloc, calloc, realloc ve free fonksiyonlarıdır. Dinamik bellek fonksyionları bellekte ismine "heap" denilen bir alanda tahsisatlarını yaparlar. Heap alanının büyüklüğü sistemden sisteme değişebilmektedir. C standartlarında söylenmese de modern sistemlerde her programın heap'i ayrıdır. Yani bir orgramdaki dinamik tahsisatların başka bir programa etkisi yoktur. Program bitinci modern sistemlerde o programın kullandığı heap alanı da sisteme iade edilir. malloc Fonksiyonu malloc programın çalışma zamanı sırasında parametresiyle belirtilen sayıda byte kadar ardışıl alanı tahsis eder. Tahsis ettiği alanın başlangıç adresiyle geri döner. Prototipi şöyledir: #include void *malloc(unsigned n); malloc parametresiyle belirtilen miktarda byte kadar ardışıl alanı heap'te tahsis eder. Tahsis ettiği alanın başlangıç adresine geri döner. Eğer heap doluysa malloc tahsisatı yapamaz. Bu durumda NULL adrese geri döner. malloc ile tahsis edilen blokta çöp değerler vardır. Her ne kadar Windows gibi Linux gibi işletim sistemlerinde prosesin heap alanı çok büyükse de yine de her bellek tahsisatının başarısının kontrol edilmesi iyi bir tekniktir. malloc fonksiyonunun bize verdiği adres herhangi türden bir dizi olarak kulanılabilir. Örneğin biz malloc ile biz 32 byte bir alan tahsis etmiş olalım. Orayı 4 elemanlı double bir dizi gibi de, 8 elemanlı int bir dizi gibi de, 32 elemanlı char türden bir dizi gibi de kullanabiliriz. Ne de olsa tüm diziler bellekte ardışıl tutulmaktadır. Örneğin: #include #include int main(void) { int n, i; int *pi; printf("Kac elemanli bir int dizi olusturmak istiyorsunuz?"); scanf("%d", &n); pi = (int *)malloc(n * sizeof(int)); if (pi == NULL) { printf("cannot allocate memory!\n"); exit(EXIT_FAILURE); } for (i = 0; i < n; ++i) { printf("%d. elemani giriniz:", i + 1); scanf("%d", &pi[i]); } for (i = 0; i < n; ++i) printf("%d ", pi[i]); printf("\n"); return 0; 181 } calloc Fonksiyonu Aslında calloc fonksiyonu taban bir fonksiyon değildir. Pek çok kütüphanede calloc fonksiyonu malloc fonksiyonunu çağıracak biçimde yazılmıştır. Prototipi şöyledir: #include void *calloc(unsigned count, unsigned size); calloc iki parametresinin çarpımı kadar arşılı alan tahsis eder. Geleneksel olarak birinci parametre veri yapısının eleman sayısı, ikinci parametre ise bir elemanın byte uzunluğu olarak girilir. Örneğin biz 20 elemanlı bir int dizi tahsis edecek olsak birinci parametreye 20, ikinci parametresizeof(int) biçiminde girilir. calloc fonksiyonu malloc fonksiyonundan farklı olarak tahsis ettiği alanı sıfırlar. Halbuki malloc Yine fonksiyon başarı durumunda tahsis edilen alanın başlangıç adresine, başarısızlık durumunda NULL adrese geri döner. Örneğin: #include #include int main(void) { int *pim; int *pic; int i; if ((pim = (int *)malloc(10 * sizeof(int))) == NULL) { printf("cannot allocate memory!\n"); exit(EXIT_FAILURE); } if ((pic = (int *)calloc(10, sizeof(int))) == NULL) { printf("cannot allocate memory!\n"); exit(EXIT_FAILURE); } for (i = 0; i < 10; ++i) printf("%d ", pim[i]); printf("\n"); for (i = 0; i < 10; ++i) printf("%d ", pic[i]); printf("\n"); return 0; } calloc fonksiyonunu malloc kullanarak biz de yazabiliriz: void *mycalloc(unsigned count, unsigned size) { void *ptr; if ((ptr = malloc(count * size)) == NULL) return NULL; return memset(ptr, 0, count * size); } realloc Fonksiyonu 182 realloc daha önce tahsis edilmiş bir alanı büyütmek ya da küçültmek için kullanılır. Fonksiyonun prototipi şöyledir: #include void *realloc(void *ptr, unsigned newsize); Fonksiyonun birinci parametresi daha önce tahsis edilmiş alanın başlangıç adresini alır. İkinci parametre toplam yeni uzunluğu belirtir. Fonksiyon tipik olarak şöyle çalışmaktadır: eğer blok büyütülüyorsa realloc önce tahsis edilmiş alanın hemen altında istenilen kadar ilave yer var mı diye bakar. Eğer varsa orayı da tahsis eder. Böylece blok yer değiştirmemiş olur. Eğer önceki alanın hemen altında toplam yeni uzunluğu karşılayacak kadar boş yer yoksa realloc heap alanının başka bir yerinde toplam yeni uzunluk kadar yeni bir yer arar. Bulursa burayı tahsis eder. Eski alandaki bilgileri burayqa kopyalar. Eski alanı free hale getirir ve yeni alanın başlangıç adresiyle geri döner. Her ne kadar standartlarda realloc'un önce eski bloğun aşağısında yer arayıp aramayacağı belirtilmemişse de tipik çalışma böyledir. Örneğin realloc aslında eski bloğun altında boş yer olsa bile yine de bloğu taşıyabilir. Tabi realloc yine de başarısz olabilir. Yani ne eski yerin altında ne de herap'in başka bir yerinde toplam yeni uzunluğu karşılayacak kadar yer olmayabilir. Bu durumda realloc NULL adrese geri döneri Ayrıca bloğun küçültülmesi durumunda da blok yer değiştirebilir. Küçültme durumunda başarısızlık olmaz fakatg blok yer değiştirebilir. O halde realloc fonksiyonunun geri dönüş değeri her zaman değerlendirilmelidir. Örneğin: p = malloc(10); ... realloc(p, 20); /* hata! */ Burada realloc bloğun yerini değiştirebilir. Bu durumda p hala eski bloğu gösterir durumda olacaktır. Oysa şöyle yapılmalıydı: p = malloc(10); ... p = realloc(p, 20); /* doğru, fakat başarısızlık durumuna dikkat */ realloc başarız olduğunda eski bloğu free hale getirmemektedir. eğer programa devam edilecekse eski bloğun kaybedilmemesi uygun olur: p = malloc(10); ... temp = realloc(p, 20); /* doğru */ if (temp == NULL) { ... } else p = temp; ... realloc ile bloğun büyütüldüğü durumda bloğa ek yapılan alanda rastgele (çöp) değerler vardır. Yani burası da tıpkı malloc fonksiyonunda olduğu gibi sıfırlanmaz. realloc fonksiyonunun birinci parametresi NULL girilirse realloc malloc gibi davranır. Yani: p = malloc(n); ile, 183 p = realloc(NULL, n); aynı işleve sahiptir. realloc kullanılması için daha önce kesinlikle bloğun dinamik bellek fonksiyonlarıyla tahsis edilmiş olması gerekir. Normal bir diziyi biz realloc ile büyütüp küçültemeyiz. Örneğin: #include #include #define BLOCK_ELEM_SIZE 5 int main(void) { int *pi = NULL; int val, count, i; count = 0; for (;;) { printf("Sayi giriniz:"); scanf("%d", &val); if (val == 0) break; if (count % BLOCK_ELEM_SIZE == 0) { pi = (int *)realloc(pi, (count + BLOCK_ELEM_SIZE) * sizeof(int) ); if (pi == NULL) { printf("cannot allocate memory!..\n"); exit(EXIT_FAILURE); } } pi[count++] = val; } for (i = 0; i < count; ++i) printf("%d ", pi[i]); printf("\n"); return 0; } aynı işleve sahiptir. realloc kullanılması için daha önce kesinlikle bloğun dinamik bellek fonksiyonlarıyla tahsis edilmiş olması gerekir. Normal bir diziyi biz realloc ile büyütüp küçültemeyiz. Sınıf Çalışması: Bir döngü içerisinde sürekli isim isteyiniz. Girilen isimleri dinamik olarak büyütülen char türden bir diziye aralarına ',' koyarak ekleyiniz. "exit" yazıldığında döngüden çıkınız ve üm yazıyı tek bir puts ile yazdırınız. Çözüm: #include #include #include int main(void) { char *names; char name[64]; int count; count = 1; names = (char *)malloc(1); 184 if (names == NULL) { printf("cannot allocate memory!..\n"); exit(EXIT_FAILURE); } *names = '\0'; for (;;) { printf("Isim giriniz:"); gets(name); if (!strcmp(name, "exit")) break; if (count != 1) strcat(names, ", "); count += strlen(name) + 2; names = (char *)realloc(names, count); if (names == NULL) { printf("cannot allocate memory!\n"); exit(EXIT_FAILURE); } strcat(names, name); } puts(names); return 0; } Önce malloc kullanmadan realloc fonksiyonunu malloc gibi kullanmak isteyebiliriz. Ancak bu durumda ilk tahsisattan önce names dizisini başında null karakter bulunması gerekir. Şöyle çözüm söz konusu olabilir: #include #include #include int main(void) { char *names = NULL; char name[64]; int count, len; count = 1; for (;;) { printf("Isim giriniz:"); gets(name); if (!strcmp(name, "exit")) break; if (count != 1) strcat(names, ", "); len = strlen(name) + 2; names = (char *)realloc(names, count + len); if (names == NULL) { printf("cannot allocate memory!\n"); exit(EXIT_FAILURE); } if (count == 1) *names = '\0'; count += len; strcat(names, name); } puts(names); return 0; } 185 free Fonksiyonu free fonksiyonu daha önce malloc, calloc ya da realloc fonksiyonlarıyla tahsis edilmiş alanı serbest bırakmak için kullanılır. Yani free'den sonra artık o alan sani hiç tahsis edilmemiş gibi olur. Fonksiynunun prototipi şöyledir: #include void free(void *ptr) Fonksiyon daha önce tahsis edilmiş olan bloğun başlangıç adresini alır ve o bloğun tamamını serbest bırakır. Bloğun bir kısmının serbest bırakılması söz konusu değildir. Modern sistemlerin hepsinde çalışan programların (proseslerin) heap alanları birbirlerinden farklıdır. Yani bir programdaki dinamik tahsisatlar diğer programın heap alanını azaltmazlar. Ancak yine de C standartlarında proseslerin heap alanlarının farklı olduğu belirtilmemiştir. Yani bazı küçük kapasiteli sistemlerde programdan çıkılsa bile heap tahsisatları kalıyor olabilir. Fakat mevcut sistemlerde bir program sonlandığında onun bütün tahsisatları otomatik free hale getirilmektedir. Örneğin: if ((ptr = malloc(100)) == NULL) { printf("cannot allocate memory!..\n"); exit(EXIT_FAILURE); } ... free(ptr); Heap Organizasyonu Nasıl Yapılmaktadır? Heap alanının hangi büyüklükte olacağı ve heap olarak belleğin neresinin kullanılacağı sistemden sisteme değişebilmektedir. Ancak dinamik bellek fonksiyonları kendi aralarında hangi bölgelerin tahis edilmiş olduğunu bir tahsisat tablosu yoluyla tutmaktadır. Pek çok sistemde tahsisat algoritması olarak "boş blok bağlı listeleri" kullanılıyorsa da çok çeşitli tahsisat algoritmaları vardır. Burada bir fikir oluşsun diye bu fonksiyonların aaşağıdaki gibi bir tahsisat tablosu oluşturduğunu düşünebiliriz: Yapılar (Structures) Elemanları bellekte ardışıl biçimde tutulan fakat farklı türlerden olabilen veri yapılarına "yapı (structure)" denilmektedir. Dizi ile yapılar birbilerine çok benzerler. Her ikisinde de elemanları ardışıl olarak bellekte tutulmaktadır. Fakat dizi elemanları aynı türden olduğu halde yapı elemanları farklı türlernden olabilmektedir. 186 Yapılarla çalışırken önce yapıların bir şablonu bildirilir. Buna "yapı bildirimi" denir. Yapıl bildirimi bellekte yer kaplamaz. Yani tanımlama değildir. Yapı bildirimini gören derleyici yapı elemanlarının türlerini ve isimlerini öğrenir. Daha sonra bu yapı türünden gerçek nesnler tanımlanır. Yapı bildiriminin genel biçimi şöyledir: struct { }; Örneğin: struct DATE { int day, month, year; }; struct COMPLEX { double real; double imag; }; struct SAMPE { int a; long b; double c; }; Yapı isimlerini bazı programcılar büyük harflerle bazıları küçük harflerle harflendirmektedir. Biz kurusumuzda yapı isimlerini büyük harflerle harflendireceğiz. Yapı bildirimi ile derleyici yapı elemanlarının isimlerini ve türlerini öğrenir. Yapı bildirimleri yerel ya da global düzeyde yapılabilir. Eğer yapı bildirimi yerel düzeyde yapılmışsa o yapı ismi ancak o blokta kullanılabilir. Global olarak yapılmışsa her yerde kullanılabilir. Yapı bildirimleri genellikle global düzeyde yapılmaktadır. Çünkü çoğu kez yapıların farklı fonksiyonlarda kullanılması gerekmektedir. Her yapı bildirimi aynı zamanda bir tür de belirtmektedir. Yani bir yapı bildirimi ile biz bir tür de oluşturmuş oluruz. Yapı bildirimleriyle oluşturulan türler struct anahtar sözcüğü ve yapı ismiyle ifade edilir. Örneğin struct DATE gibi, struct SAMPLE gibi. Yapı bildirimi yapıldıktan sonra artık o yapı türünden nesneler tanımlanır. Örneğin: struct DATE d; struct SAMPLE s; Burada d "struct DATE" türünden, s ise "struct SAMPLE" türündendir. Yapılar bileşik nesnelerdir. Yani parçalardan oluşmaktadır. Örneğin: struct SAMPLE { int a; long b; double c; }; 187 struct SAMPLE s; Burada s a, b, ve c parçalarından oluşan nesnein bütününü temsil eder. Yapı nesneleri genellikle bütünsel olarak kullanılmaz. Onların parçaalarına erişilip parçaları kullanılır. Yapı elemanlarına erişmek için nokta operatörü kullanılmaktadır. Nokta operatörü iki operandlı araek bir operatördür. Nokta operatörünün sol tarafındaki operand yapı nesnesinin bütünü, sağ taraftaki operand onun bir elemanı olmak zorundadır. Bu operatör sol taraftaki yapının sağ taraftaki elemanına erişmekte kullanılır. Örneğin: #include struct SAMPLE { int a; long b; double c; }; int main(void) { struct SAMPLE s; s.a = 10; s.b = 20; s.c = 12.4; printf("%d, %ld, %f\n", s.a, s.b, s.c); return 0; } Burada s nesnesi struct SAMPLE türündendir. s.a ifadesi int türdendir. s.a ifadesi "s nesnesinin a parçası" anlamına gelir. s.b ifadesi long türden, s.c ifadesi ise double türdendir. C standartlarına göre yapı bildiriminde ilk yazılan eleman düşük adreste bulunacak biçimde eleman ardı şıllığı oluşturulur. Yani örneğimizde s'in a parçası en düşük adrestedir. Bunu b parçası izler onu da c parçası izler. Nokta operatörü öncelik tablosunun en yüksek düzeyinde soldan sağa grupta bulunur 188 () []. Soldan-Sağa + - ++ -- ! sizeof & * Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa ... ... Örneğin &s.a gibi bir ifadede s.a'nın adresi alınmaktadır. Çünkü nokta operatörü & operatöründen daha yüksek önceliklidir. Örneğin: #include struct SAMPLE { int a; long b; double c; }; int main(void) { struct SAMPLE s; printf("%p, %p, %p\n", &s.a, &s.b, &s.c); return 0; } Anahtar Notlar: Anımsanacağı gibi Kernighan & Ritchie yazıam tarzında iki operandlı operatörlerle operandlar arasında birer boşluk karakteri bırakılıyordu. Fakat nokta ve -> operatörleri iki operandlı olmasına karşın bunlarla operandlar arasında boşluk karakteri bırakılmamaktadır. Bir yapı nesnesinin elemanlarına henüz değer atanmadıysa içerisinde ne vardır? İşte eğer yapı nesnesi yerelse onun tüm elemanlarında çöp değerler, global ise sıfır değerleri bulunur. Bir yapı nesnesine küme parantezleri içerisinde ilkdeğer verebiliriz. (Tıpkı dizilerd eolduğu gibi) Örneğin: #include struct SAMPLE { int a; long b; double c; }; int main(void) { struct SAMPLE s = { 10, 20, 20.5 }; printf("%d, %ld, %f\n", s.a, s.b, s.c); return 0; } Tıpkı dizilerde olduğu gibi yapının az sayıda elemanına ilkdeğer verebiliriz. Bu durumda geri kalan elemanlar derleyici tarafından sıfırlanır. Fakat yapının fazla sayıda elemanına ilkdeğer vermeye çalışırsak derleme aşamasında error oluşur. Aynı türden iki yapı nesnesi birbirlerine atanabilir. Bu durumda yapının karşılıklı elemanları birbirlerine atanmaktadır. Örneğin: 189 #include struct SAMPLE { int a; long b; double c; }; int main(void) { struct SAMPLE s = { 10, 20, 30.5 }; struct SAMPLE k; k = s; printf("%d, %ld, %f\n", s.a, s.b, s.c); printf("%d, %ld, %f\n", k.a, k.b, k.c); return 0; } Atama işleminde yapıların türlerinin aynı olması gerekir. Tür ise isme bağlıdır. İçi aynı olan farklı isimli yapıları birbirlerine atayamayız. Yapı Elemanı Olarak Diziler Bir dizi bir yapının elemanı olabilir. Bu durumda yapının dizi elemnanına erişildiğinde bu ifade dizinin tamamını temsil eder, ifade içerisinde kullanıldığında ise yapı içerisindeki dizinin başlangıç adresini belirtir. Örneğin: #include struct PERSON { char name[32]; int no; }; int main(void) { struct PERSON per; printf("Adi soyadi:"); gets(per.name); printf("No:"); scanf("%d", &per.no); printf("%s, %d\n", per.name, per.no); return 0; } 190 Böyle yapı nesnesine ilkdeğer de verebiliriz: struct PERSON per = {"Kaan Aslan", 123}; Tabi buradaki iki tırnak ifadesi adres anlamına gelmez. Karakterlerin name dizisine kopyalanacağı anlamına gelir. Nokta operatöryle [] operatörünün aynı grupta soldan sağa öncelikli olduğuna dikkat ediniz. Örneğin: per.name[3] ifadesi per.name dizisinin 3. indeksli elemanı anlamına gelir. Yani: per -------> struct PERSON türündendir per.name -------> char * türündendir per.name[3] ------> char türündendir Örneğin: #include struct PERSON { char name[32]; int no; }; int main(void) { struct PERSON per = { "Kaan Aslan", 123 }; char ch; ch = per.name[3]; putchar(ch); return 0; } Yapı Elemanı Olarak Göstericilerin Kullanılması Bir yapının elemanı bir gösterici olabilir. Örneğin: struct PERSON { char *name; int no; 191 }; struct PERSON per; Burada name elemanı bir göstericidir ve bu göstericinin tahsis edilmiş bir alanı gösteriyor olması gerekir. Örneğin: #include struct PERSON { char *name; int no; }; int main(void) { struct PERSON per; per.name = "Kaan Aslan"; per.no = 123; printf("%s, %d\n", per.name, per.no); return 0; } Yapı Türünden Adresler ve Göstericiler Bir yapı nesnesinin adresi alınabilir. Bu durumda elde edilen adresin sayısal bileşeni tüm yapı nesnesinin bellekteki başlangıç adresi, tür bileşeni ise po yapı türündendir. Bir yapı nesnesinin adresi aynı türden bir yapı göstericisne atanabilir. Örneğin: struct SAMPLE { int a; long b; double c; }; struct SAMPLE s; struct SAMPLE *ps; ps = &s; 192 Bir yapı göstericisi ya da bir yapı türünden adres * operatörüyle kullanılırsa yapı nesnesinin tamamına erişilir. Yani yukarıdaki örnekte *ps ile s aynıdır. Burada ps struct SAMPLE * türünden *ps ise struct SAMPLE türündedir. Yapı Göstericisi Yoluyla Yapı elemanlarına Erişilmesi p bir yapı türünden adres a da bu yapının bir elemanı olmak üzere bu adresin gösterdiği yerdeki yapı nesnesinin a elemanına erişmek için (*p).a ifadesi kullanılır. *p.a ifadesi geçersizdir. Çünkü nokta operatörü * operatöründen daha yüksek öncelikli olduğu için bu ifadede önce p.a yapılmaya çalışılır ki nokta operatörünün solundaki operand geçersi olur. Çünkü nokta operatörünün solundaki operand yapı nesnesinin kendisi olmalıdır, adresi olmamalıdır. Örneğin: #include struct SAMPLE { int a; long b; double c; }; int main(void) { struct SAMPLE s; struct SAMPLE *ps; ps = &s; (*ps).a = 10; (*ps).b = 20; (*ps).c = 30; printf("%d, %ld, %f\n", (*ps).a, (*ps).b, (*ps).c); return 0; } Ok (Arrow) Operatörü Ok operatörü -> karaketerleriyle elde edilir. Ok operatörü iki operandlı araek bir operatördür. Ok operatörünün 193 solundaki operand bir yapı türünden adres, sağındaki operand o yapının bir elemanı olmak zorundadır. On operatörü sol taraftaki operandla belirtilen adresteki yapı nesnesinin sağ taraftaki operandla belirtilen elemanına erişmekte kullanılır. Yani: p->a ile (*p).a tamamen eşdeğerdir. Nokta operatörüyle ok operatörünün her ikisi de yapı elemanlarına erişmekte kullanılır. Nokta operatörü nesnenin kendisiyle ok operatörü adresiyle erişim sağlar. Ok operatörü öncelik tablosunun en üst düzeyinde soldan sağa grupta bulunur: ( ) [ ] . -> Soldan-Sağa + - ++ -- ! sizeof & * Sağdan-Sola * /% Soldan-Sağa + - Soldan-Sağa ... ... Örneğin: #include struct SAMPLE { int a; long b; double c; }; int main(void) { struct SAMPLE s; struct SAMPLE *ps; ps = &s; ps->a = 10; ps->b = 20; ps->c = 30; printf("%d, %ld, %f\n", ps->a, ps->b, ps->c); return 0; } Örneğin: #include struct POINT { int x, y; }; int main(void) 194 { struct POINT pt; struct POINT *ppt; ppt = &pt; ppt->x = 10; ppt->y = 20; printf("(%d, %d)\n", ppt->x, ppt->y); return 0; } s bir yapı türünden nesne a da bu yapının bir elemanı olmak üzere &s->a ifadesi geçersizdir. Çünkü & operatörü -> operatöründen daha düşük önceliklidir. Ancak (&s)->a ifadesi geçerlidir ve s.a ile eşdeğerdir. p bir yapı türünden adres ve a da bu yapının bir elelmanı olmak üzere &p->a ifadesi geçerlidir ve bu ifade p göstericisinin gösterdiği yerdeki nesnenin a parçasının adresi anlamına gelir. Örneğin: #include struct PERSON { char name[32]; int no; }; int main(void) { struct PERSON per = { "Ali Serce", 234 }; struct PERSON *pper; pper = &per; printf("%s, %d\n", pper->name, pper->no); printf("%c\n", pper->name[2]); return 0; } Yapıların Fonksiyonlara Parametre Yoluyla Aktarılması Yapıların fonksiyonlara aktarılmasında iki teknik kullanılır. Bunlardan birincisi nesnenin kopyalama yoluyla aktarılması tekniği, diğeri ise adres yoluyla aktarma tekniğidir. Nesnenin kendisinin aktarılması genel olarak kötü bir tekniktir. Adrtes yoluyla aktarma iyi bir tekniktir. Gerçekten de C'de yapı nesneleri hemen her zaman fonksiyonlara adres yoluyla aktarılır. 2) Yapı Nesnelerinin Fonksiyonlara Kopyalama Yoluyla Aktarılması: Bu yöntemde fonksiyonun parametre değişkeni bir yapı türünden yapı nesnesidir. Fonksiyon da aynı yapı türünden bir nesneyle çağrılır. Aynı türden iki yapı nesnesi atanabildiğine göre bu çağırma geçerlidir. Ancak burada aktarım kopyalama yoluyla yapılmaktadır. Örneğin: #include struct PERSON { char name[32]; int no; }; 195 void foo(struct PERSON per) { printf("%s, %d\n", per.name, per.no); } int main(void) { struct PERSON x = { "Ali Serce", 234 }; foo(x); return 0; } Bu yöntem genel olarak kötü bir tekniktir. Çünkü büyük bir yapının bu yöntemde tüm elemanlarını tek tek aktarım sırasında fonksiyona kopyalanır. Üstelik bu yöntemde fonksiyon içerisinde artık biz orijinal nesneye erişemeyiz. Tabi eğer yapı çok küçükse bu teknik kötü bir teknik olmaz. Bu durumda kullanılabilir. 2) Yapı Nesnelerinin Fonksiyonlara Adres Yoluyla Aktarılması: Bu yöntemde fonksiyonun parametre değişkeni yapı türünden gösterici olur. Fonksiyon da aynı türden bir yapı nesnesinin adresiyle çağrılır. Bu kullanılması gereken doğru tekniktir. Örneğin: #include struct PERSON { char name[32]; int no; }; void foo(struct PERSON *per) { printf("%s, %d\n", per->name, per->no); } int main(void) { struct PERSON x = { "Ali Serce", 234 }; foo(&x); return 0; } Örneğin: #include struct DATE { int day; int month; int year; }; void get_date(struct DATE *date) { printf("Gun:"); scanf("%d", &date->day); printf("Ay:"); scanf("%d", &date->month); printf("Yil:"); scanf("%d", &date->year); 196 } void disp_date(struct DATE *date) { printf("%02d/%02d/%04d\n", date->day, date->month, date->year); } int main(void) { struct DATE date; get_date(&date); disp_date(&date); return 0; } Örneğin: #include struct COMPLEX { double real, imag; }; void get_comp(struct COMPLEX *comp) { printf("Gercek Kisim:"); scanf("%lf", &comp->real); printf("Sanal Kisim:"); scanf("%lf", &comp->imag); } void disp_comp(struct COMPLEX *comp) { printf("%.0f+%.0fi\n", comp->real, comp->imag); } int main(void) { struct COMPLEX z; get_comp(&z); disp_comp(&z); return 0; } Yapılara Neden Gereksinim Duyulmaktadır? 1) Yapılar olgular için mantıksal bir kap oluşturmaktadır. Yani tarih gibi, sanal sayılar gibi, şahıs bilgileri gibi birbirleriyle ilgili çok sayıda nesne bir yapıyla ifade edilirse daha kolay bir temsil yeteneği el edilir. Gerçekten de mantıksal bakımdan birbirleriyle bağlantılı nesnelerin yapılarla temsil edilmesi okunabilirliği ve anlaşılabilirliği artırmaktadır. 2) Bir fonksiyona çok sayıda parametre aktarılacaksa onların tek tek aktarılmaları hem yazılımsal olarak zordur. Hem anlaşılır olmaktan çıkar hem de yavaştır. Bunun yerine çok sayıda parametre bir yapında toplanım tek bir parametre biçiminde fonksiyona geçirilebilir. 3) Bir fonksiyonun tek bir geri dönüş değeri vardır. Eğer fonksiyon dış dünyaya çok sayıda değer iletecekse bu yapılarla sağlanabilir. Örneğin iletilecek değerler bir yapıyla ifade edilir. Fonksiyona yapı nesnesinin adresi 197 geçirilir. Fonksiyon da bu nesnenin içini doldurur. Fonksiyonların Geri Dönüş Değerlerinin Yapı Olması Durumu Bir fonksiyonun geri dönüş değeri bir yapı türünden olabilir. Bu durumda return ifadesi de aynı türden bir yapı nesnesi olmalıdır. Aslında bu yöntem de C'de çoğu kez (yani yapı büyükse) iyi teknik kabul edilmez. Çünkü burada return işlemi sırasında geçici nesneye bir kopyalama yapılmakta ve geri dönüş değerinin atanması sırasında da aynı sorun oluşmaktadır. Örneğin: #include struct COMPLEX { double real, imag; }; void disp_comp(struct COMPLEX *comp) { printf("%.0f+%.0fi\n", comp->real, comp->imag); } struct COMPLEX add_comp(struct COMPLEX *z1, struct COMPLEX *z2) { struct COMPLEX result; result.real = z1->real + z2->real; result.imag = z1->imag + z2->imag; return result; } int main(void) { struct COMPLEX z1 = { 3, 2 }; struct COMPLEX z2 = { 1, 4 }; struct COMPLEX result; result = add_comp(&z1, &z2); disp_comp(&result); return 0; } Genellikle programcılar çoklu bilgiyi böyle almaktansa bir nesne verip fonksiyonun onun içerisine yerleştirme yapmasını tercih ederler. Örneğin: #include struct COMPLEX { double real, imag; }; void disp_comp(struct COMPLEX *comp) { printf("%.0f+%.0fi\n", comp->real, comp->imag); } void add_comp(struct COMPLEX *z1, struct COMPLEX *z2, struct COMPLEX *result) { result->real = z1->real + z2->real; result->imag = z1->imag + z2->imag; } 198 int main(void) { struct COMPLEX z1 = { 3, 2 }; struct COMPLEX z2 = { 1, 4 }; struct COMPLEX result; add_comp(&z1, &z2, &result); disp_comp(&result); return 0; } Yapılarda Hizalama (Alignment Kavramı) Modern 32 işlemcilerde bellek bağlantısı bir hamlede 4 byte bilgiyi çekeck biçimde yapımıştır. Benzer biçimde 64 bit işlemcilerde de bellek bağlantısı bir hamlede 8 byte bilgiyi çekecek biçimde yapılmıştır. Böylece örneğin 32 bit işlemciler aslında 32 adres yoluna değil 30 adres yoluna sahiptir. Tabi makina komutları yine aynı biçimde byte ardeslemeli çalışmaktadır. Aşağıda 32 bit bir işlemcinin bellek erişimi resmedilmiştir: Bu sistemlerde eğer 4 byte'lık bir bilgi (örneğin int türden bir bilgi) 4'ün katlarında değilse makina komutları göreli olarak daha yavaş çalışmaktadır. Çünkü bu 4 byte'ı işlemci iki bus erişimiyle elde eder: Tabi bir byte bir bilgiye kaçın katlarında olursa olsun tek bus hareketiyle erişilebilmektedir. Peki 2 byte'lık bilgiler? Bunların da 2'nin katlarında olması gerekir. Örneğin 2'nin katlarında olmayan 2 byte'lık (örneğin short türden bir nesne) bir nesne aşağıdaki gibi gösterilebilir: İşte derleyiciler işlemcilerin böyle çalıştıklarını bildiği için makina komutları daha hızlı çalışsın diye 4 byte'lık 199 nesneleri 4'ün katlarına, 8 byte'lık nesneleri 8'in katlarına, 2 byte'lık nesneleri 2'nin katlarına, 1 byte'lık nesneleri 1'in katlarına yerleştirmektedir. Derleyiciler bunlara yerel değişkenler için ve global değişkenler için dikkaet ederler. Yapı elemanları bellekte ardışıl olacağından ve ilk yazılan elemanın düşük adreste olması gerekeceğinden bu hizalama (alignement) yapılar için nasıl gerçekleştirilecektir? İşte derleyiciler yapı elemanlarının arasına boşluklar koyarak o elemanların belli adres katlarında olmasını sağlayabilmektedir. Örneğin: struct SAMPLE { char a; int b; char c; int d; }; struct SAMPLE s; 32 bit bir işlemcide bu yap nesnesinin bellkete 10 byte yer kaplayacağı düşünülebilir. Ancak derleyiciler a ile b arasına ve c ile d arasına 3'er byte boşluk bırakarak int olan kısımların 4'ün katlarına gelmesini sağlayabilmektedir. Böylece bu yapı nesnesinin sizeof değerinin 16 çıkması programcıyı şaşırtmamalıdır. Örneğin: #include struct SAMPLE { char a; int b; char c; int d; }; int main(void) { struct SAMPLE s; printf("%u\n", sizeof(s)); /* 16 */ return 0; } Peki yukarıdaki yapıyı aşağıdaki gibi düzenleseydik ne olurdu? #include struct SAMPLE { char a; char b; int c; int d; }; int main(void) { struct SAMPLE s; printf("%u\n", sizeof(s)); /* 12 */ return 0; } Hizalama pek çok derleyicide derleyici seçeneklerinden yönetilebilmektedir. Örneğin Microsoft C derleyicilerinde hizalama Proje ayarlarında C-C++/Code Generation/Struct Member Alignment ile değiştirilebilmektedir. Eğer hizalama "1 byte hizalama moduna çekilirse derleyici yapı elemanlarını 1'in katlarına yerleştirmeye çalışır. 200 Dolayısıyla elemanlar arasında hiç boşluk bırakmaz. C standartlarına hizalama kavramı bir kurala bağlanmamıştır. Standartlarda yalnızca derleyicinin yapı elemanları arasında boşluk bırakabileceği belirtilmiştir. Peki derleyicinin yapı elemanları arasında boşluk bırakabilmesi erişimde bir soruna yol açar mı? Yanıt hayır. Çünkü derleyici nerelere boşluk bıraktığını bildiği için ona göre erişimi yapmaktadır. Örneğin aşağıdaki yapı için derleyici 4 byte hizalama kullanmış olsun: struct SAMPLE { char a; int b; char c; int d; }; Şimdi p göstericisinin bu yapıyı gösterdiğini düşünelim. Artık derleyici p->b ifadesiyle p adresinden 1 byte sonraya değil 4 byte sonraya erişir. Çünkü araya boşluk bıraktığını zaten kendisi bilmektedir. Yani yukarıdaki yapıyı biz şöyle düşünebiliriz: struct SAMPLE { char a; char temp1, temp2, temp3; int b; char c; char temp4, temp5, temp6; int d; }; Yapılar İçin Dinamik Tahsisat Yapılması Mademki yapı elemanları bellekte ardışıl bir biçimde tutulmaktadır. O halde yapı nesneleri için de heap'te dinamik tahsisatlar yapılabilir. Dinamik tahsisat yaparken hizalama olasılığını göz önüne almak gerekir. Bu nedenle yapının byte uzunluğunun sizeof operatörü ile elde edilmesi uygun olur. sizeof operatörü derleyicinin o anda uyguladığı hizalamayı da hesaba katmaktadır. Örneğin: #include #include struct PERSON { char name[32]; int no; }; int main(void) { struct PERSON *per; per = (struct PERSON *)malloc(sizeof(struct PERSON)); if (per == NULL) { printf("cannot allocate memory!..\n"); exit(EXIT_FAILURE); } printf("Adi soyadi:"); gets(per->name); printf("No:"); scanf("%d", &per->no); printf("%s, %d\n", per->name, per->no); 201 free(per); return 0; } Yapı Dizileri Her elemanı bir yapı nesnesi olan dizilere yapı dizileri (array of structures) denilmektedir. Yapı dizileri de normal dizilerde olduğu gibi bildirilir. Örneğin: struct PERSON persons[3]; Örneğin: #include #include struct PERSON { char name[32]; int no; }; int main(void) { struct PERSON persons[3]; int i; strcpy(persons[0].name, "Kaan Aslan"); persons[0].no = 123; 202 strcpy(persons[1].name, "Ali Serce"); persons[1].no = 234; strcpy(persons[2].name, "Necati Ergin"); persons[2].no = 678; for (i = 0; i < 3; ++i) printf("%s, %d\n", persons[i].name, persons[i].no); return 0; } Anımsanacağı gibi dizi isimleri tüm diziyi temsil etmektedir. Ancak bunlar işleme sokulduğunda derleyici tarafından dizinin başlangıç adresine dönüştürülürler. Yani biz dizi isimlerini kullandığımızda aslında o dizilerin başlangıç adreslerini kullanıyor oluruz. Dizi isimleri kullanıldığında nesne belirtmezler. Biz bir yapı dizisinin ismini aynı yapı türünden bir yapı göstericisine atayabiliriz. Örneğin: #include #include struct PERSON { char name[32]; int no; }; int main(void) { struct PERSON persons[3], *per; int i; strcpy(persons[0].name, "Kaan Aslan"); persons[0].no = 123; strcpy(persons[1].name, "Ali Serce"); persons[1].no = 234; strcpy(persons[2].name, "Necati Ergin"); persons[2].no = 678; per = persons; for (i = 0; i < 3; ++i) { printf("%s, %d\n", per->name, per->no); ++per; } return 0; } Bir yapı göstericini bir artırdığımızda göstericinin içerisindeki adres yapı uzunluğu kadar artmaktadır. Yapı dizilerine de küme parantezleri ile ilkdeğer verilebilir. Bu durumda eleman olan her yapı nesnesi için de ayrı bir küme parantezi kullanılır. Örneğin: struct PERSON persons[3] = { { "Kaan Aslan", 123 }, { "Ali Serce", 345 }, { "Necati Ergin", 654 } }; Aslında bu tür durumlarda ,çteki küme parantezleri zorunlu değildir. Örneğin yukarıdaki ilk değer verme şöyle yapılabilirdi: 203 struct PERSON persons[3] = { "Kaan Aslan", 123 , "Ali Serce", 345, "Necati Ergin", 654 }; Fakat bu biçimde ilkdeğer verme hem okunabilirliği azaltmakta hem de aradaki bir değer unutulduğunda diğer tüm değerlerin yanlış elemanlara atanması gibi bir soruna yol açabilmektedir. Örneğin: struct POINT { int x; int y; }; struct POINT points[5] = { { 1, 2 }, { 4, 7 }, { 6, 8 }, { 7, 9 }, { 3, 4 } }; Burada 2 değerinin yazılmadığını düşünelim: struct POINT points[5] = { { 1 }, { 4, 7 }, { 6, 8 }, { 7, 9 }, { 3, 4 } }; Artık dizinin ilk elemanının y değeri sıfır olacaktır. Ancak: struct POINT points[5] = { 1, 4, 7 , 6, 8, 7, 9, 3, 4 }; Burada tamamen bir kaydırma oluşmaktadır. Örneğin: #include struct POINT { int x; int y; }; int main(void) { struct POINT points[5] = { { 1, 2 }, { 4, 7 }, { 6, 8 }, { 7, 9 }, { 3, 4 } }; int i; for (i = 0; i < sizeof(points) / sizeof(*points); ++i) printf("(%d, %d)\n", points[i].x, points[i].y); return 0; } Bir yapı dizisini de fonksiyona parametre yoluyla aktarabiliriz. Bunun için yine onun başlangıç adresini ve uzunluğunu fonksiyona geçiririz. Örneğin: #include #include struct PERSON { char name[32]; int no; }; 204 void sort_persons_byname(struct PERSON *per, int size); void sort_persons_byno(struct PERSON *per, int size); void disp_persons(struct PERSON *per, int size); int main(void) { struct PERSON persons[] = { { "Selami Hakyemez", 123 }, { "Ahmet Hamdi Tanpinar", 523 }, { "Hulisi Sen", 323 }, { "Siracettin Bilyap", 654 }, { "Ali Ipek", 234 } }; sort_persons_byname(persons, 5); disp_persons(persons, 5); printf("-----------\n"); sort_persons_byno(persons, 5); disp_persons(persons, 5); return 0; } void sort_persons_byname(struct PERSON *per, int size) { int i, k; struct PERSON temp; for (i = 0; i < size - 1; ++i) for (k = 0; k < size - 1 - i; ++k) if (strcmp(per[k].name, per[k + 1].name) > 0) { temp = per[k]; per[k] = per[k + 1]; per[k + 1] = temp; } } void sort_persons_byno(struct PERSON *per, int size) { int i, k; struct PERSON temp; for (i = 0; i < size - 1; ++i) for (k = 0; k < size - 1 - i; ++k) if (per[k].no > per[k + 1].no) { temp = per[k]; per[k] = per[k + 1]; per[k + 1] = temp; } } void disp_persons(struct PERSON *per, int size) { int i; for (i = 0; i < size; ++i) printf("%s, %d\n", per[i].name, per[i].no); } Yapı Bildirimiyle Nesne Tanımlamasının Birlikte Yapılması Bir yapı bildirilirken bildirim ';' ile kapataılmayıp bir değişken listesi de yazılırsa aynı zamanda o yapı türünden nesneler de tanımlanmış olur. Örneğin: struct POINT { int x, y; 205 } pt1, pt2; Bu bildirimin aşağıdakinden hiçbir farkı yoktur. struct POINT { int x, y; }; struct POINT pt1, pt2; Örneğin: struct PERSON { char name[32]; int no; } per = {"Kaan Aslan", 123}, *pper, persons[] = { {"Ali Serce", 123}, {"Ahmet Altintarti", 345} }; Burada "per" struct PERSON türünden bir nesnedir, "pper" struct PERSON türünden bir göstericidir. persons ise struct PERSON türünden bir dizidir. Bu biçimde bildirilmiş değişkenler yapı global olarak bildirildiyse global, yerel olarak bildirildiyse yerel biçimdedir. C standartlarına göre eğer yapı bildirimi ile aynı zamanda o yapı türünden n esne tanımlanıyorsa bu durumda yapıya isim verilmeyebilir. Örneğin: struct { char name[32]; int no; } x, y; Ancak eğer yapı türündne değişken tanımlanmıyorsa yapıya isim verilmek zorudadır. Örneğin: struct { /* geçersin */ char name[32]; int no; }; İsimsiz yapıların hepsi farklı bir tür kabul edilir. Yani örneğin: struct { char name[32]; int no; } x; struct { char name[32]; int no; } y; Burada x ve y derleyici tarafından aynı türden kabul edilmemektedir. Bu nedenle biz örneğin bunları birbirlerine atayamayız. İç İçe Yapı Bildirimleri 206 Bir yapının elemanları başka yapı türünden olabilir. Bu tür durumlara "iç içe yapı bildirimleri" denilmektedir. Örneğin: struct DATE { int day, month, year; }; struct PERSON { char name[32]; struct DATE bdate; int no; }; Örneğin: #include struct DATE { int day, month, year; }; struct PERSON { char name[32]; struct DATE bdate; int no; }; int main(void) { struct PERSON per = { "Sacit Bayram", { 12, 10, 1970 }, 2000 }; printf("%s, %d/%d/%d, %d\n", per.name, per.bdate.day, per.bdate.month, per.bdate.year, per.no); return 0; } İç içe yapılarda ilkdeğer verilirken iç yapı ayrıca küme parantezine alınabilir ya da alınmayabilir. Ancak okunabilirlik bakımından iç yapıya ilişkin değerlerin küme parantezleri içerisine alınması tavsiye edilir. İç içe yapıların alternatif bildirim biçimi de vardır. Bu biçimde iç yapı dış yapının fiziksel olarak içerisinde bildirilir. Örneğin: struct PERSON { char name[32]; struct DATE { int day, month, year; } bdate; int no; }; 207 Burada dış yapı bildirimi içerisinde hem iç yapı bildirilmiş hem de o yapı türünden değişken bildirimi yapılmıştır. Bu biçimdeki bildirimde içeride bildirilen yapı (örneğimizde struct DATE) ayrıca dış yapıdan bağımsız olarak da kullanılabilir. Örneğin: struct PERSON per; struct DATE date; /* geçerli */ Yani bu iki iç içe yapı bildirim biçimi semantik olarak tamamen eşdeğerdir. Genel olarak birinci biçimin daha okunabilir olduğu söylenebilir. typedef Bildirimleri typedef anahtar sözcüğü bir tür isminin tam olarak yerini tutabilen alternatif isimler oluşturmak için kullanılır. typedef standartlarda bildirimdeki bir yer belirleyici (storage class specifier) biçiminde tanımlanmıştır. Bir bildirime typedef anahtar sözcüğü eklenirse bildirimdeki değişken ismi o değişkenin türünü belirten tür ismi haline gelir. Örneğin: int I; Burada I bir değişken ismidir ve int türdendir. Şimdi bildirime typedef ekleyelim: typedef int I; Burada I artık int türünü temsil eder. Örneğin: int a, b, c; ile, I a, b, c; tamamen eşdeğerdir. Örneğin: char *STR; Burada STR char * türünden bir değişkendir. Şimdi bildirimin başına typedef yerleştirelimr: typedef char *STR; Artık STR char * türünü temsil etmektedir. Yani: char *s; ile STR s; aynı anlamdadır. Örneğin: #include 208 typedef int I; typedef char *STR; int main(void) { I a; STR s = "Ankara"; a = 10; printf("%d\n", a); printf("%s\n", s); return 0; } Örneğin: int A, B, C; Burada A, B ve C int türden değişken isimleridir. Bildirimin başına typedef getirelim: typedef int A, B, C; Burada artık, A, B ve C int türünü temsil eden tür ismidir. Örneğin: int I, *PI; Burada I int türünden, PI da int * türündendir. Bildirimin başına typedef getirelimr: typedef int I, *PI; Burada artık I int türünü, PI da int * türünü temsil eder. Örneğin: struct PERSON { char name[32]; int no; }; struct PERSON PER; Burada PER struct PERSON türündendir. Şimdi bildirimin başına typedef getirelim: typedef struct PERSON PER; Artık PER struct PERSON türünü belirten bir tür ismi haline gelmiştir. Yani örneğin: struct PERSON per; ile, PER per; aynı anlamdadır. Örneğin: #include 209 struct PERSON { char name[32]; int no; }; typedef struct PERSON PER; int main(void) { PER per = { "Ali Serce", 123 }; printf("%s, %d\n", per.name, per.no); return 0; } Örneğin aynı işlem şöyle de yapılabilirdi: typedef struct PERSON { char name[32]; int no; } PER; PER per = {"Ali Serce", 123}; Örneğin: struct { char name[32]; int no; } PER; Burada PER bildirilen yapı türünden bir nesnedir. Başına typedef getirelim: typedef struct { char name[32]; int no; } PER; Burada PER bu yapıyı temsil eder. Örneğin: PER x; PER y; x ve y aynı türdendir. Örneğin: int ARY[3]; Burada ARY int[3] türündedir. Şimdi başına typedef getirelim: typedef int ARY[3]; Burada artık ARY 3 elemanlı bir int dizi türü ismidir. Yani örneğin: 210 int a[3]; ile, ARY a; aynı anlamdadır. Örneğin: #include typedef int ARY[3]; int main(void) { ARY a = { 1, 2, 3 }; int i; for (i = 0; i < 3; ++i) printf("%d\n", a[i]); return 0; } Örneğin: void FOO(int a); Burada FOO geri dönüş değeri void parametresi int olan bir fonksiyon prototipidir. Başına typedef getirelim: typedef void FOO(int a); Artık Foo geri dönüş değeri void parametresi int olan bir fonksiyon türünü temsil eder. Yani: FOO x, y; ile, void x(int a); void y(int a); aynı anlmadadır. typedef anahtar sözcüğünün bildirimde başa gelmesi zounlu değildir. Fakat değişken isminin solunda bulunmak zorudadır. Örneğin: int typedef I; typedef int I; int I typedef; /* geçerli */ /* geçerli */ /* geçersiz */ typedef bildirimleri yerel ya da global düzeyde yapılabilir. Eğer yerel düzeyde yapılırsa o typedef ismi yalnızca o blokta kullanılabilir. Eğer global düzeyde yapılırsa her yerde kullanılabilir. typedef bidirimleri genellikle globak düzeyde yapılmaktadır. Hatta çoğu kez programcılar typedef bildirimlerini başlık dosyalarının içerisine yerleştirir. Bazı typedef bildirimleri ile yaratılmak istenen etki #define önişlemci komutuyla ya yaratılabilmektedir. Örneğin: #define I int 211 I a, b, c; Fakat pek çok tür #defile ile oluşturulamaz. Örneğin: typedef int ARY[3]; Bunun karşılığını #define ile oluşturamayız. Ya da örneğin: #define I int * I a, b; Burada yalnızca a gösterici olur. Halbuki: typedef int *I; I a, b; Burada hem a hem de b göstericidir. Karmaşık türler #define ile zaten oluşturulamaz. Ayrıca #defibe önişlemci aşamasına ilişkindir. Halbuki typedef derleme modülü tarafından değerlendirilir. typedef Bildirimlerine Neden Gereksinim Duyulmaktadır? 1) typedef karmaşık türlerin daha kolay yazılmasını sağlar. Örneğin: char *PARY[5]; PARY names = {"ali", "veli,", "selami", "ayse", "fatma"}; 2) typedef okunabilirliği artırmak için de kullanılabilmektedir. Yani bir türe onun kullanım amacına uygun bir isim verirsek kodu inceleyen kişiler onu daha iyi anlamlandırabilirler. Örneğin: typedef int BOOL; BOOL foo() { ... } Burada biz foo'nun başarı ya da başarısızlık biçiminde bir geri dönüş değerine sahip olduğunu anlıyoruz. 3) typedef taşınabilirliği artımak için kullanılabilir. Örneğin bir kütüphane oluşturan ekip bazı türlerin duruma göre sistemden sisteme değişebileceğini gördüğünden bunlara typedef ismi atayabilir. /* lib.h */ typedef int HANDLE; HANDLE foo(void); 212 Burada HANDLE int türdendir. Programcı fonksiyonu şöyle kullanır: HANDLE h; h = foo(); Burada kütüphaneyi yazanlar başka bir sistem için HANDLE değerini long olarak typedef edebilirler. Biz de int yerine HANDLE kullandığımızda boşuna kodumuzu değiştirmek zorunda kalmayız. Yani değişiklikten kodumuz etkilenmemiş olur. Gerçekten de C/C++'ta yazılmış framework'ler ve kütüphanelerde typedef ile oluşturulmuş tür isimlerine çık sık rastlanır. C'de Standart Olarak Bildirilen Bazı typedef İsimleri C'de de bazı başlık dosyalarında bildirilmiş olan çeşitli typedef isimleri vardır. Bunlar taşınabilirlik sağlamak amacıyla oluşturulmuştur. Tabi bunları kullanmak için ilgili dosyasının include edilmesi gerekir. C'nin tüm standart typede isimleri isimli bir dosyada bildirilmiştir. Bu standart typedef isimlerinin hepsinin sonu xxx_t ile biter. Fakat bazı typedef isimleri ayrıca başka başlık dosyalarında da bildirilmiş durumdadır. Bunların bazıları aşağıda açıklanmaktadır: size_t türü: Standartlara göre size_t işaretsiz bir tamsayı türü olarak typedef edilmek zorundadır. size_t tipik olarak ilgili sistemdeki bellek büyüklüğünü ifade edecek biçimde derleyicileri yazanlar tarafından uygun bir türe typedef edilir. Bazı sistemlerde size_t unsigned int olarak bazı sistemlerde de unsigned long int olarak karşımıza çıkabilmektedir. size_t C'de bazı fonksiyonların prototilerinde kullanılmıştır. Örneğin malloc fonksiyonunun standartlarda belirtilen prototipi şöyledir: void *malloc(size_t size); Yani malloc fonksiyonunun parametresi o sistemde derleyiciyi yazanlar size_t türünü nasıl typedef etmişlerse o türdendir. Örneğin strlen fonksiyonunun da orijinal prototipi şöyledir: size_t strlen(const char *str); C programcıları da kendi programlarında genellikle dizi uzunluklarını size_t ile fade ederler. size_t türü dosyasıının yanı sıra , gibi temel başlık dosyalarında typedef edilmiştir. ptrdiff_t Türü: C'de iki adres toplanamaz. Ancak aynı türden iki adres çıkartılabilir. İşlem sonucu bir tamsayı türündendir. Öyle ki, önce adreslerin sayısal bileşenleri çıkartılır, sonuç adresin türünün uzunluğuna bölünür. Yani C'de aynı türden iki adresi çıkarttığımızda sonuç sanki aradaki eleman sayısını vermektedir. Örneğin: int a[10]; Burada &a[5] - &a[0] ifadesi bize 5'i verir. Aslında a dizisi hangi türden olursa olsun bu işlem 5 değerini verecektir. Aynı türden iki adresi çıkarttığımızda sonuç hangi türdendir? İşte standartlar sonucun ptrdiff_t türünden olacağını söyler. ptrdiff_t işaretli bir tamsayı türü olarak typedef edilmelidir. Pek çok derleyicide ptrfdiff_t int ya da long türdendir. C'de standart olan birkaç tür daha ileride ele alınacaktır. C'de Bildirim İşleminin Genel Biçimi Daha önce biz bildirimin genel biçimini şöyle görmüştük: ; 213 Aslında bildirimin genel biçimi şöyledir: [yer belirleyici anahtar sözcükler] [tür niteleyici anahtar sözcükler] [tür belirten sözcükler] ; Görüldüğü gibi bildirimde türün yanı sıra yer belirleyiciler (storage class specifers) ve tür niteleyiciler (type qualifiers) de kullanılmaktadır. Örneğin: Genel olarak bildirimde yer belirleyicileri, tür niteleyicileri ve tür belirten sözcükler yer değiştirebilir. Örneğin aşağıdaki iki bildirim eşdeğerdir: static const int a = 10; const int static a = 10; Fakat genellikle programcılar önce yer belirleyici, sonra tür niteleyici en sonra da tür belirten anahtar sözcükleri yzamaktadır. C'de yer belirleyici (storage class Specifiers) olarak aşağıdaki 5 anahtar sözcük kullanılabilir: auto static register extern typedef Aslında typedef anahtar sözcüğünün yer belirleyici işlevi yoktur. Tasarımcılar typedef'i başka bir yere yerleştiremeyince buraya yerleştirmişlerdir. Bildirimde en fazla 1 tane yer belirleyici anahtar sözcük bulundurulabilir. C'de ayrıca iki de tür niteleyici (type qualifier) anahtar sözcük vardır: const volatile C90'da eğer bildirimde tür belirten sözcük kullanılmamışsa fakat yer belirleyici ya da tür niteleyici anahtar sözcüklerden en az biri kullanılmışsa bu durumda default olarak tür belirten sözcük int kabul edilmektedir. Yani örneğin: const a = 10; bildirimi geçerlidir. Burada a'nın türü int'tir. Ancak bildirimde yalnızca değişken ismi bulundurulamaz. Örneğin: a, b = 20; /* geçersiz */ Böyle bir bildirim geçersizdir. Halbuki C99 ve C11'de bildirimde mutlaka tür belirten sözcük bulundurulmak zorundadır. 214 Yer Belirleyici Anahtar Sözcüklerin İşlevleri auto Belirleyicisi: auto aslında işlevi olmayan bir anahtar sözcük durumundadır. auto belirleyicisi yerel değişkenlerle ya da parametre değişkenleriyle kullanılabilir. Global değişkenlerle kullanılamaz. auto değişkenin ilgili blok bittiğinde bellekten atılacağını (yani stack'te yer ayrılacağını) vurgulamaktadır. extern Belirleyicisi: Aslında biz tek bir .c dosyasını derlemek zorunda değiliz. Bir proje çok uzun olabilir ve programcı bunu çeşitli dosyalara yaymak isteyebilir. Bu durumda .c dosyaları bağımsız olarak derlenir. Oluşan object modüller birlikte link edilerek çaıştırılabilen dosya (executable) oluşturulur. Örneğin projemiz a1.c a2.c a3.c biçiminde üç kaynak dosyadan oluşuyor olsun: Görüldüğü gibi linker aslında birden fazla object modülü çalıştırılabilen dosyaya dönüştürebilmektedir. C standartlarında projeyi oluşturan kaynak dosyalara "translation unit" denilmektedir. Programcılar ise genellikle bıu dosyalara "modül" demektedir. Peki bir programı neden birden fazla kaynak dosyaya bölerek yazmak isteriz? Birinci neden karmaşıklığı azaltmaktır. Büyük bir projenin birden fazla dosyaya bölünmesi yönetlebilirliği artırır. Örneğin farklı kişiler garklı dosyalar üzerinde çalışabilirler. Sonra bunları birleştirebilirler. İkinci neden derleme zamanının azaltılmasıdır. Şöyle ki, örneğin 100000 satırlık bir projeyi tek bir kaynak dosya olarak organizae etmiş olalım. Proje içerisindeki bir satırı bile değiştirsek tüm projeyi yeniden derlememiz gerekir. Halbuki bunu 10000 satırlık 10 dosyaya ayırsak yalnızca değişikliğin yapıldığı modülü derlememiz yeterli olur. Tabi hep berber her defasında bir link işlemi yine yapılmak zorundadır. Tabi link işlemi derleme işlemine göre daha kısa bir işlemdir. B irden fazla modülle çalışma nasıl yapılmaktadır? Derleyicilerin komut satırlı biçimlerinde önce her modülü derleyip sonra birlikte link işlemi yapılabilir. gcc derleyicisinde -c seçeneği Microsoft'un cl derleyicisinde /c seçeneği yalnızca derleme yapar link etme işlemini yapmaz. Daha sonra yalnızca derlenmiş bu modüller link edilebilir. gcc programına object modülleri verirsek zaten o link işlemini yapmaktadır. Microsoft'un linker programı link.exe isimli programdır. Örneğin: gcc -c a1.c gcc -c a2.c gcc -c a3.c Buradan a1.o, a2.o ve a3.o dosyaları elde edilecektir. Sonra bunlar aşağıdaki gibi link edilebilirler: gcc -o app a1.o a2.o a3.o Bunun yerine aynı işlem şöyle de yapılabilirdi: gcc -o app a1.c a2.c a3.c Biz gcc'ye birden fazla .c dosyası verirsek gcc onları önce bağımsız olarak derler sonra object modülleri link 215 ederek çalıştırılabilir (executable) dosyayı oluşturur. IDE'lerde projeye birden fazla kaynak dosya eklenirse bunlar otomatik olarak derlenir ve object modüller link edilerek çalıştırılabilir dosya elde edilir. Birden fazla dosya ile çalışırken bir dosyadaki global değişkenin diğer dosyalardan da erişilebilmesi gerekir. Ancak her dosya bağımsız olarak derlendiği için diğer dosyada global olarak tanımlanmış değişkenleri derleyici tanımaz. Yani derleyici bir dosya derlenirken hangi dosyaların projenin parçalarını oluşturduğunu bilmemektedir. Projeyi oluşturan her kaynak dosya bağımısız olarak derlenir. İşte başka bir müdlde global olarak tanımlanmış olan bir değişken kullanılacaksa onun için extern bildiriminin yapılması gerekir. Örneğin: extern int g_x; Global değişkenin yalnızca bir modülde global olarak tanımlanması fakat onun kullanıldığı tüm modüllerde extern olarak bildirilmesi gerekir. Global tanımlamanın hangi modülde yapıldığının bir önemi yoktur. extern bir tanımlama değil bildirim işlemidir. extern belirleyicisini gören derleyici değişkenin başka bir modülde tanımlandığını anlar.. Ona göre kod üretir. Object modül içerisine linker için şöyle bir mesaj yazar: "Sayın linker bu değişken başka bir modülde tanımlanmış. Ben durumu idare ettim. Fakat sen proje link edilirken bu değişkenin hangi modülde tanımlandığını bul, bunu onla ilişkilendir. Bu değişken o değişkendir." Linker da link işlemine sokulan tüm modüllere bakarak bu birleştirmeyi yapar. Burada dikkat edilmesi gerek durum global değişkenin yalnızca tek bir modülde global tanımlamasının yapılması gerekliliğidir. eğer global değişken birden fazla modülde global tanımlanırsa her modül sorunsuz derlenir fakat link aşamasında aynı isimli birden fazla global tanımlama yapılmış olması nedeniyle error oluşur. Benzer biçimde global değişken her modülde extern bildirilirse yine her modül başarılı olarak derlenir ancak link aşamasında global tanımlamaya rastlanmadığından error oluşur. Bir değişkenin başka modüllerden kullanılabilirliğine o değişkenin bağlama durumu (linkage) denilmektedir. Değişkenin bağlama durumu üç biçimde olabilir: 1) Dışsal Bağlama Durumu (External Linkage): Burada değişken başka modüllerden kullanılabilir. Global değişkenler default olarak dışsal bağlamaya sahiptir. Global değişkenleri extern ile bildirirsek de dışsal bağlama oluşur. Fakat extern bir tanımlama değildir. Global değişkenin tek bir tanımlamasını olması gerektiği için tek bir modülde global tanımlama yapılmalı diğer modüllerde extern bildirilmelidir. 2) İçsel Bağlama Durumu (Internal Linkage): İçsel bağlamaya sahiğ değişkenler yalnızca bir modülün her yerinde kullanılabilirler. Ancak farklı modüllerden kullanılamazlar. Bir global değişken static anahtar sözcüğü ile bildirilirse onun bağlaması içsel bağlama olur. Artık diğer modüllerden o kullanılamaz. Özetle C'de bir global değişken default olarak dışsal bağlamaya sahiptir. Ancak onu static anahtar sözcüğüyle bildirirsek içsel bağlamaya sahip olur. 3) Bağlamasız Durum (No Linkage): C'de yerel değişkenlerin ve parametre değişkenlerinin bağlaması yoktur. Bir global değişken C'de aynı kaynak dosyada hem global olarak tanımlanmış olabilir hem de extern olarak bildirilmiş olabilir. Bu durum hata oluşturmaz. Değişkenin bağlama durumu dışsal bağlamadır. Bu durumda değişken için yer ayrılır. Yani extern bildiriminin bir etkisi olmaz. Örneğin: int g_a; /* global tanımlama */ extern int g_a; /* Ggobal bildirim */ 216 Buradaki etki aşağıdakiyle eşdeğerdir: int g_a; Bir global değişken extern belirleyicisi kullanılarak bildirilmiş olsun ve ilkdeğer de verilmiş olsun. Örneğin: extern int g_a = 10; C standartlarına göre artık bu bildirim aynı zamanda bir tanımlamadır. Yani değişken için yer ayrılır. Başka bir deyişle yukarıdaki bildirim aşağıdaki ile eşdeğerdir: int g_a = 10; extern bildirimi yerel bir blokta yapılabilir. Tabi bu durumda o değişken yerel bir değişken olmaz. Başka modüldeki global bir değişken anlamındadır. Fakat bu isim yalnızca o blokta kullanılabilir. Örneğin: void foo(void) { extern int a; ... } void bar(void) { a = 10; } /* geçersiz! */ Burada yerel bloktaki extern belirleyicisi ile bildirilmiş değişken aslında yerel bir değişken değildir. Dışsal bağlamaya sahip muhtemelen başka modülde tanımlanmış bir global değişkendir. Ama biz bu ismi yalnızca foo bloğunda kullanabiliriz. C standartlarına göre farklı modüllerde aynı global değişkenin ilkdeğer verilmeden tanımlanması yapılabilir. Buna standartlarda "tentative declaration" denilmektedir. Bu durumda her ne kadar global değişkenin her modül için tanımlaması object modüle yazılsa da linker bunların tek bir kopyasını çalıştırılabilen dosyaya yazmaktadır. Dolayısıyla aşağıdaki kod geçerlidir: Ancak ilkdeğer verme durumunda bu geçerlilik kaybolur. Aşağıdaki derlemelerde link aşamasında error oluşur: 217 Peki bir modülde tanımlanmış bir fonksiyonu diğer modüllerden çağırabilir miyiz? Fonksiyonlar teknik olarak global değişkenler gibidir. Fonksiyonların da default bağlama biçimleri dışsal bağlamadır. Yani onlar başka bir modülden kullanılabilirler. Ancak kullanmadan önce bir fonksiyonun tanımlamasının ya da prototip bildirimin derleyici tarafından görülmesi gerekir. Bu nednele biz bir modülde tanımladığımız fonksiyonu diğer modülden kullanmadan önce fonksiyon prototipini bulundurmalıyız. eğer prototip bulundurmadan foo gibi bir fonksiyonu çağırırsak derleyici onun aşağıdaki gibi bir prototipe sahip olduğunu varsayar: int foo(); Örneğin: /* a1.c */ #include extern int g_x; void foo(void); int main(void) { g_x = 10; foo(); printf("%d\n", g_x); return 0; } /* a2.c */ #include int g_x; void foo() { g_x = 100; } Prototiplerde extern belirleyicisinin kullanılmasına gerek yoktur. Fakat kullanılsa da sorun oluşmaz. Çok modüllü derlemelerde önerilen durum. Proje için ortak bir başlık dosyası hazırlamak ve tüm modeüllerden bunu include etmektir. Bu başlık dosyasında yer kaplayan hiçbir tanımlama bulunmamalıdır. Yalnızca #'li önişlemci bidirimleri, extern bildirimler, fonksiyon prototipleri, yapı bildirimleri vs. bulunmalıdır. Global değişkenlerin ayrıca bir dosyada global tanımlaması yapılmalıdır. Örneğin: /* project.h */ #ifndef PROJECT_H_ #define PROJECT_H_ void foo(void); void bar(void); extern int g_a; extern int g_b; #endif 218 /* a1.c */ #include #include "project.h" int g_a; int g_b; int main(void) { foo(); bar(); printf("%d, %d\n", g_a, g_b); return 0; } /* a2.c */ #include #include "project.h" void foo() { g_a = 10; } /* a3.c */ #include #include "project.h" void bar() { g_b = 20; } static Belirleyicisi: static belirleyicisi yer değişkenlerle ya da global değişkenlerle kullanılabilir. Fakat parametre değişkenleriyle kullanılamaz. Static yerel ve static global değişkenler tamamen farklı anlamlara gelirler. static belirleyicisi yerel değişkenlerle kullanılırsa bu durum o değişkenin ömrünün static ömürlü olduğunu gösterir. Yani blok bittiğinde değişken yok edilmez. Program çalışmaya başladığında bellekte yer kaplar, program sonlanana kadar bellekte kalır. Örneğin: #include void foo(void) { static int count = 1; printf("%d\n", count); ++count; } int main(void) { foo(); foo(); foo(); 219 return 0; } static yerel dğeişkene ilkdeğer verilmemişse içerisinde sıfır bulunur. Verilen ilkdeğer derleme aşasında değerlendirilir. Program yüklendiğinde static yerel değişken o değerle başlar. Artık bloğa her girişte bu ilkdeğer tekrar atanmaz. Peki static yerel değişkenin global değişkenden ne farkı vardır? Bunların ömürleri aynı olmasına karşın faaliyet alanları farklıdır. static yerel değişkenler blok faaliyet alanı kuralına uyarlar. Global değişkenler ise dosya faaliyet alanına sahiptir. Bir fonksiyonun static yerel bir nesnenin ya da dizinin adresiyle geri dönmesinde bir sakınca yoktur. Çünkü programdan çıkılsa bile o nesne yaşamaya devam etmektedir. Örneğin: #include char *getname(void) { static char name[64]; printf("Adi soyadi:"); gets(name); return name; } int main(void) { char *nm; nm = getname(); puts(nm); return 0; } static anahtar sözcüğü global değişkenlerle kullanılırsa bu durumda global değişkenin bağlama durumu "içsel bağlama (internal linkage)" olur. Yani böyle deişkenler diğer modüllerden hiçbir biçimde kullanılamazlar. Başka bir modülde aynı isimli global nesne olsa bile bu durum soruna yol açmaz. Bir global değişken aynı modülde hem static hem de extern olarak bildirilebilir mi? Tabi bu durum saçmadır. Böyle bir şeyden kaçınmak gerekir. Ancak standartlara göre bir global değişken önce static ile sonra extern ile bildirilirse nesnenin içsel bağlamaya sahip olduğu kabul edilir. Fakat önce extern sonra static ile bildirilirse bu durum tanımsız davranışa yol açar. register Belirleyicisi: register belirleyicisi global değişkenlerle kullanılamaz. Yerel ya da parametre değişkenleri ile kullanılabilir. Aslında uzunca bir süredir bu belirleyicinin ciddi bir işlevi kalmamıştır. Çünkü derleyicilerin kod optimizasyonları son derece gelişmiş durumdadır. Dolayısıyla bu belirleyicinin sağlayacağı fayda tartışmalıdır. CPU içerisinde ismine "yazmaç (register)" denilen küçük bellek bölgeleri vardır. CPU ile RAM bağlantılıdır. CPU aritmetik ya da mantıksal ya da karşılaştırma işlemlerini yapmak için operandları yazmaçlardan alır. Dolayısıyla örneğin: c = a + b; gibi bir işlem aşağıdaki gibi makine komutlarıyla yapılmaktadır: 220 mov mov add mov reg1, a reg2, b reg1, reg2 c, reg1 işte register anahtar sözcüğü söz konusu değişkenin RAM yerine CPU içerisindeki yazmaçlarda tutulmasını istemek için kullanılmaktadır. Böylece bu değişkenler işleme sokulurken boşuna yeniden yazmaçlara alınmak zorunda kalınmaz. Şüphesiz çok yoğun işlem gören (örneğin döngü değişkenleri gibi) nesnelerin yazmaçlarda tutulması anlamlı olur. register belirleyicisi bir emir değildir. Rica niteliğindedir. Yani biz bir nesneyi aşağıdaki gibi tanımlamış olalım: register int x; Derleyici bunu yazmaçlarda tutmak zorunda değildir. İsterse bunu normal nesnelerde olduğu gibi yine RAM'de tutabilir. Bunun için de programcıya herhangi bir bildirimde (uyarı gibi) bulunmaz. Zaten modern derleyiciler gerekli gördükleri nesneleri gerektiği kadar yazmaçlarda tutacak biçimde kod optimizasyonu uygulamaktadır. Bu belirleyiciyi kullanmak yerine derleyicinin optimizasyon mekanizmasına güvenmek daha uygun gözükmektedir. CPU'lardaki yazmaç miktarları CPU'dan CPU'ya değişebilmektedir. Genel olarak RISC tabanlı mimarilere sahip olan işlemcilerde CISC tabanlı mimarilere sahip olan işlemcilere göre daha fazla yazmaç bulunma eğilimindedir. Bir nesnenin programcının isteği ile yazmaçta tutulması eldeki yazmaç sayısını azaltabilir. Bunu farkeden derleyici bu belirleyiciyi hiç dikkate almayabilir. Tür Niteleyici Anahtar Sözcükler C'de tür niteleyici iki anahtar sözcük vardır: const ve volatile. Bunların her ikisi de bildirimde kullanılabilir. const Belirleyicisi const bir nesne ilkdeğer verilirerek bildirilir. Artık bundan sonra bu nesneye değer atanamaz. Eğer const bir nesneye değer atanmak istenirse derleme aşamsında "error" oluşur. Örneğin: const int x = 10; printf("%d\n", x); x = 20; /* geçerli */ /* geçersiz! */ const bir nesnenin ilkdeğer verilmeden tanımlanması C++'ta geçersizdir. C'de geçerli olsa da anlamsızdır. (İlkdeğer verilmediğine göre ve bundan sonra değer de atanamayacağından böyle bir tanımlamanın bir anlamı olamaz.) 221 const anahtar sözcüğü dekleratör ilişkin değildir. Türe ilişkin kabul edilir. Yani: int const x = 10, y = 20; Burada hem x hem de y const durumdadır. Peki const belirleyicisinin ne faydası vardır? const temel olarak okunabilirliği ve anlaşılırlığı kuvvetlendirmek için kullanılır. Örneğin: const int personel_sayisi = 150; gibi bir tanımlamada koda bakan kişi program içerisinde personel sayısının değişmeyeceğini anlayabilir. const belirleyicisi kodu yazanın yanlışlıkla bir nesneye atama yapmasını da engelleyebilmektedir. Bu durumda derleme aşamasında hatayla karşılaşan programcı hatasını anlayabilir. const belirleyicisi bazı durumlarda derleyicilere bazı optimizasyonları yapmaya olanak sağlayabilir. const belirleyicisi ile #define sembolik sabitleri benzer amaçlarla kullanılabilmektedir. Örneğin: #define personel_sayisi 150 const int personel_sayisi = 120; Fakat sembolik sabitler gerçekten bir sabittir. Halbuki const nesneler gerçekten birer nesnedir. Örneğin biz const nesnelerin adreslerini alabiliriz. Fakat sembolik sabitlerin adreslerini alamayız. Bir dizinin tamamı const olabilir. Bu durumda biz dizinin hiçbir elemanını değiştiremeyiz Örneğin: const int a[5] = {10, 20, 30, 40, 50}; a[3] = 100; /* geçersiz! */ const bir dizinin tüm elemanları const bir nesne gibidir. Bir yapı nesnesi de const olabilir. Bu durumda yapının hiçbir elemanını değiştiremeyiz. Örneğin: struct COMPLEX { double real, imag; }; const struct COMPLEX z = {3, 2}; z.real = 5; z.imag = 7; /* geçersiz! */ /* geçersiz! */ Yapı bildiriminde belli elemanlar const yapılabilir. Bu durumda yapı nesnesinin tamamaı const olmasa bile bu elemanları daha sonra değiştiremeyiz. Örneğin: struct SAMPLE { int a; const int b; 222 }; struct SAMPLE s = {10, 20}; s.a = 30; s.b = 40; /* geçerli */ /* geçerli değil! */ Genellikle programcılar yapı elemanlarını const yapmazlar. Çünkü bu durum karmaşıklığa yol açmaktadır. const belirleyicisi en çok göstericilerle kullanılır. Böyle göstericilere const göstericiler denilmektedir. const Göstericiler Bir gösterici bildiriminde iki nesne söz konusu olur: Göstericinin kendisi ve onun gösterdiği yer. Bunların gangisinin const olacağına göre const göstericiler üçe ayrılmaktadır: 1) Kendisi değil gösterdiği yer const olan const göstericiler 2) Gösterdeiğ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 ve okunabilirlik bakımından en faydalı olan const göstericiler kendisi değil gösteridği yer const olan const göstericilerdir. Bir const göstericinin hangi gruba girdiği const anahtar sözcüğünün yerine bakılarak karar verilir. Eüer const anahtar sözcüğü * atomunun solundaysa bu gösterdiği yer const olan const göstericidir. Örneğin: const int *pi; /* int const *pi ile aynı */ Burada pi'nin kendisi const değildir. *pi yani pi'nin gösterdiği yer const durumdadır. Eğer const anahtar sözcüğü * atomunun sağına getirilirse bu durumda göstericinin kendisi const olur. Örneğin: int * const pi = ptr; Burada const pi'yi nitelemektedir. Yani burada pi const durumdadır. *pi const değildir. Nihayaet const anahtar sözcüğü iki yerde de bulunabilir: const int * const pi = ptr; Burada hem pi hem de *pi const durumdadır. Kendisi Değil Gösterdiği Yer const Olan const Göstericiler Bu tür göstericilerin bildirimlerinde const anahtar sözcüğü * atomunun soluna getirilir. Örneğin: const int *pi; Tabi aşağıdaki bildirim de eşdeğerdir: int const *pi; Burada göstericinin kendisi const değildir. Yani onun içerisine istedğimiz bir adresi istediğimiz zaman atayabiliriz. Ancak onun gösterdiği yere atama yapamayız. Örneğin: 223 int a = 10, b = 20; const int *pi; pi = &a; *pi = 30; /* geçerli, pi const değil */ /* Geçersiz göstericinin gösterdiği yer const */ pi = &b; *pi = 40; /* geçerli pi const değil */ /* Geçersiz göstericinin gösterdiği yer const */ Bu tür const göstericilerde tam olarak göstericinin gösterdiği yer const değil o gösterici ile erişilen her yer const biçimdedir. Örneğin: int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; const int *pi; pi = a; /* geçerli */ pi[3] = 20; /* geçersiz*/ pi[1000] = 30; /* Dizi taşması var ama derleme aşamasında yiner error oluşur */ pi += 500; *pi = 40; /* geçerli */ /* geçersiz! */ Gösteridiği yer const olan const göstericiler en çok karşımıza fonksiyon parametresi olarak çıkarlar. Örneğin: void foo(const int *pi) { /* ... */ } Burada foo bizden int türden bir adres alır. Parametre const bir gösterici olduğuna göre foo aldığı adresteki nesneyi değiştirmez. Onu kullanabilir ama değiştirmez. Böylece fonksiyona bakan kişi fonksiyonun adresini verdiği nesneyi değiştirip değiştirmeyeceini anlar. Gösterici parametrelerdeki const'luk çok önemlidir. Örneğin strlen fonksiyonunun orijinal prototipinde parametre const bir göstericidir: size_t strlen(const char *str); Bu prototipiten biz şunu anlarız: Biz strlen fonksiyonuna char türden bir dizinin başlangıç adresini vereceğiz. Fonksiyon bu adresteki yazıyı değiştirmeyecek. Yalnızca onu okuyacak. Göstericilerde const'luk çok kararlı bir biçimde kullanılmalıdır. Böylece bir fonksiyonun parametresinin const olmayan bir gösterici olduğunu gördüğümüzde artık biz onun verdiğimiz adresteki bilgiyi değiştireceğini anlarız. (Çünkü değiştirmeyecek olsaydı zaten onu yazan göstericiyi const yapardı.). Çeşitli örnekler verelim: - strcpy fonksiyonun orijinal prototipinde birinci parametre const olmayan bir gösterici, ikinci parametre const bir göstericidir: char *strcpy(char *dest, const char *source); Bu prototipe baktığımızda bizim fonksiyona iki char türden adres göndereceğimiz anlaşılır. Fonksiyon birinci argümanla gönderdiğimiz adresteki bilgiyi değiştirecektir. Ancak ikinci argümanla gönderdiğimzi adresteki bilgiyi değiştirmeyecektir. - strchr fonksiyonun parametresi const bir göstericidir: 224 char *strchr(const char *s, int c); - strcat fonksiyonunun birinci parametresi const olmayan bir gösterici ikinci parametresi const bir göstericidir: char *strcat(char *dest, const char *source); - Bir diziyi sıraya dizen sort isimli bir fonksiyon yazacak olsak onun parametresi const gösterici olamaz: void sort(int *pi, size_t size); - puts fonksiyonun parametresi const bir gösterici, gets fonksiyonun parametresi const olmayan bir göstericidir: void puts(const char *str); char *gets(char *str); Yapı nesnelerinin adresini alan fonksiyonların gösterici parametreleri const ise fonksiyonlar bizden aldıkları yapı neesnesinin elemanlarını kullanırlar fakat değiştirmezler. Fakat bu fonksiyonların gösterici parametreleri const olmayan bir göstericiyse bu durumda fonksiyon yapı nesnesinin içini doldurmaktadır. Yani eğer fonksiyonun yapı gösterici parametresi const ise nesnenin içini biz doldururuz o kullanır. eğer const olmayan bir gösteric ise o doldurur biz kullanırız. Çeşitli örnekler verelim: - Örneğin sistemin zamanını alan GetSystemTime isimli bir fonksiyon olsun. Bu fonksiyon bizden TIME isimli bir yapı nesnesinin adresini alsın onun içerisine sistem zamanını yerleştirsin. Bu durumda fonksiyonun gösterici parametresi const olmayan bir gösterici olmalıdır: struct TIME { int hour, minute, second; }; void GetSystemTime(struct TIME *t); ... struct TIME t; GetSystemTime(&t); - Sistem zamanını bizim belirdiğimiz zaman olacak biçimde değiştiren SetSystemTime fonksiyonunun yapı gösterici parametresi const gösterici olmalıdır: void SetSystemTime(const TIME *t); - Veritabanından birisini ismiyle arayıp bulursa bulduğu kişinin bilgilerini PERSON isimli bir yapı nesnesine yerleştren fonksiyonun parametrelerinin const'luk durumu şöyledir: BOOL FindPerson(const char *name, struct PERSON *per); - Seri portun setting değerlerini alıp bize veren fonksiyonun yapı gösterici parametresi const olmayan bir gösterici olmalıdır: void GetSerialPortSettings(struct SERIAL *serial); - Bunun tam tersi işlemini yapan fonksiyonun parametresi const gösterici olmalıdır: void SetSerialPortSettings(const struct SERIAL *serial); 225 Fonksiyonların gösterici parametrelerindeki const'luğa çok önem verilmelidir. Maalesef acemi C programcıları bunlara önem vermezler ve acemilikleri hemen anlaşılır. Fonksiyonun gösterici olmayan parametrelerinin const'luğunun hiçbir önemi yoktur. Bunların const yapılması da amaç bakımından faydasız ve saçmadır. Örneğin: void foo(const int x); Burada bildirim geçersiz değildir. Ancak böyle const bildirimin kimseye hiçbir faydası yoktur. Şöyle ki: Fonksiyonun parametre değişkenini değiştirip değiştirmeyeceği bizi ilgilendirmez. Önemli olan bizden aldığı nesnelerin değerlerini değiştirip değiştirmeyeceğidir. Bu nedenle parametrelerdeki const'luk yalnızca göstericiler söz konusu olduğunda anlamlı olur. const Nesnelerin Adresleri ve const Adres Dönüştürmeleri const bir nensenin adresini bir göstericiye atayıp o nesneyi derleyiciyi kandırarak değiştebilir miyiz? Örneğin: const int a = 10; int *pi; pi = &a; *pi = 20; /* geçersiz */ İşte bu durumu engellemek için C'ye şöyle bir kural eklanmeiştir: "const bir nesnenin adresi ancak gösterdiği yer const olan const bir göstericiye atanabilir." O halde yukarıdaki örnekte zaten biz a'nın adresini pi'ye atayamayız. Peki pi'yi const yapalım: const int a = 10; const int *pi; pi = &a; *pi = 20; /* geçerli */ /* geçersiz */ Bir fonksiyonun adresini aldığı nesneyi değiştirmediği halde onun parametresinin const gösterici yapılmamasının önemli bir dezavantajı vardır. Biz artık o fonksiyonu const bir nesnenin adresiyle çağıramayız. Örneğin bir int dizi içerisindeki sayıları ekrana yazdıran disp isimli fonksiyonun normal olarak gösterici parametresinin const olması gerekir. Fakat programcının kötü teknik uygulayarak bunu const yapmadığını düşünelim: void disp(int *pi, size_t size); Biz artık bu fonksiyonu const bir adresle çağıramayız: const int a[] = {1, 2, 3}; disp(a, 3); /* geçersiz */ const nesnelerinin adresleri const adreslerdir. const bir adres ancak gösterdiği yer const olan göstericilere atanabilir. Tabi const olmayan bir adresin const bir göstericiye atanmasının hiçbir sakınscası yoktur. Örneğin biz coonst olmayan x nesnesinin adresini const bir göstericiye atayabiliriz. Çünkü zatane x'in değiştirilmemesi konusunda bir çekince yoktur. const anahtar sözcüğü türe ilişkin olduğu için başka bir deyişle T bir tür belirtmek üzere const T * türünden T * türüne otomatik dönüştürme yoktur. Fakat T * türünden const T * türüne otomatik dönüştürme vardır. 226 Bazen const bir adresin const'luğunu kaldırmak gerekebilir. Bu durumda const T * türünden T * türüne tür dönüştürme operatörü ile dönüştürme yapabiliriz. Yani const T * türünden T * türüne otomatik dönüştürme yoktur fakat tür dönüştürme operatörü ile dönüştürme yapılabilir. C'de (ve tabi C++'ta) const bir nesnenin adresini tür dönüştürme operatörüyle const olmayan bir göstericiye atayıp orayı değiştirmeye çalışırsak bu durum tanımsız davranışa (undefined behavior) yol açar. (Gerçekten de statik ömürlü const nesnelerin Windows ve Linux'ta güncellenmeye çalışılması programın çökmesine yol açmaktadır.) Örneğin: #include int main(void) { const int a = 20; int b = 30; int *pi; const int *pci; pi = &a; pi = (int *)&a; *pi = 40; /* geçersiz */ /* geçerli */ /* geçerli ama tanımsız davranışa yol açar */ pci = &b; pi = pci; pi = (int *)pci; *pi = 50; /* geçerli */ /* geçersiz */ /* geçerli */ /* normal yani tanımsız davranış değil */ return 0; } Örneğin: #include const int g_a = 10; int main(void) { int *pi; pi = (int *)&g_a; *pi = 20; printf("%d\n", g_a); /* geçerli */ /* tanımsız davranış, Windows'ta ve Linux'ta program çöker */ return 0; } Gösterdiği Yer Değil Kendisi const Olan const Göstericiler Daha önce de belirtildiği gibi böyle const göstericiler seyrek kullanılmaktadır. Bunların bildirimlerinde const anahtar sözcüğü * atomunun sağına getirilir. Örneğib: char s[50]; char d[50]; char * const pc = s; *pc = 'x'; pc = d; /* geçerli */ /* geçersiz */ 227 Hem Gösterdiği Yer Hem de Kendisi const Olan const Göstericiler Bunlar hepten seyrek kullanılırlar. Burada const beelirleyicisi hem * atomunun soluna hem de sağına getirilir. Örneğin: char s[50]; char d[50]; const char * const pc = s; pc = d; *pc = 'x'; /* geçerli değil */ /* geçerli değil */ Derleyicilerin Kod Optimizasyonları ve volatile Belirleyicisi Kodun çalışmasında hiçbir değişikliğin olmaması koşuluyla derleyiciler programcının yazdığı kodu yeniden düzenleyebilirler. Bundan amaç kodun daha hızlı çalışması (speed optimization) ya da daha az yer kaplamasıdır (size optimization). Derleyicinin kodun daha hızlı çalışması ya da daha az yer kaplaması için kodu yeniden düzenlemesine kod optimizasyonu denilmektedir. Pek çok kod optimizasyon teması vardır. Örneğin "ortak alt ifadelerin elimine edilmesi (common subexpression elimination)" denilen teknikte derleyici ortak alt ifadeleri yeniden yapmamak için bir yerde (muhtemelen bir yazmaçta) toplar. Örneğin: x = a + b + c; y = a + b + d; z = a + b + e; deyimlerinde derleyici her defasında a + b işlemini yapmamak için bunun toplamını bir yazmaçta saklayabilir ve her defasında onu kullanabilir: reg = a + b; x = reg + c; y = reg + d; z = reg + e; Derleyici kullanılmayan nenseleri sanki tanımlanmamış gibi elemine edebilir. Döngü içerisindeki gereksiz ifadeleri döngünü dışına çıkartabilir vs. Derleyiciler nesneleri ikide bir yeniden yazmaçlara çekmemek için belli büre yazmaçlarda tutabilirler. Örneğin: x = g_a + 10; y = g_a + 20; z = g_a + 30; Burada derleyici her defasında a'yı yeniden RAM'den yazmaca çekmeyebilir. Onu bir kez çekip sonraki erişimlerde o yazmacı kullanabilir. Fakat bzen özellikle çok thread'li uygulamalarda ya da kesme (interrupt) uygulamalarında bu istenmeyebilir. Şöyle ki: 228 Okla gösterilen noktada başka bir kaynak tarafından g_a değiştirildiğinde derleyicinin ürettiği kod bu değişikliği anlayamaz. Halbuki bazı durumlarda programcı bu değişikliğin kod tarafından anlaşılmasını isteyebilir. İşte volatile belirleyicisi buna sağlamak için kullanılmaktadır: volatile int g_a; volatile bir nesne kullanıldığında derleyici her zaman onu yeniden belleğe başvurarak alır. Yani onu optimizasyon amaçlı yazmaçlarda bekletmez. Örneğin aşağıdaki bir döngü söz konusu olsun: Başka bir akışın (örneğin bir thread'in) bu g_falg nesnesini 0 yaparak döngüyü sonlandırabilme olanağı olduğunu düşünelim. eğer derleyici g_flag nesnesini bir kez yazmaca çekip hep onu yazmaçtan kullanırsa diğer thread g_flag nesnesini sıfıra çekse bile döngüden çıkılmaz. Bunu engellemek için g_flag volatile yapılmalıdır: volatile int g_flag; Nesnenin volatile olduğunu gören derleyici artık o neesnenin değerini her zaman belleğe başvurarak elde eder. volatile bir gösterici söz konusu olabilir. Aslında tıpkı const'luk durumunda olduğu gibi volatile belirleyicisinde de gösterici üç biçimde olabilir: 1) Kendisi değil gösteridği yer volatile olan volatile göstericiler 2) Gösterdiği yer değil kendisi volatile olan volatile göstericiler 3) Hem kendisi hem de gösterdiği yer volatile olan volatile göstericiler Yine en çok kullanılan volatile göstericiler "kendisi değil gösterdiği yer volatile olan volatile göstericilerdir". Yine 229 volatile anahtar sözcüğünün getirildiği yere göre bunlar değişebilmektedir: volatile int *pi; int * volatile pi; volatile int * volatile pi; Gösteridği yer volatile olan bir göstgericinin anlamı nedir? volatile int *pi; Burada *pi ya da pi[n] gibi ifadelerle erişen nesneler geçici süre yazmaçlarda bekletilmezler. Bunlar her defasında yeniden belleğe başvurularak oradan alınırlar. volatile bir nesnenin adresi ancak gösterdiği yer volatile olan bir göstericiye atanabilir. Örneğin: volatile int a; int *pi; volatile int *piv; pi = &a; piv = &a; /* geçerli değil */ /* geçerli */ volatile olma durumu const olma durumuyla aynı mantığa sahiptir. Yani T bir tür belirtmek üzere 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 doğurdan dönüştürme vardır. Örneğin: int a; volatile int *piv; piv = &a; /* geçerli */ Tabi tür dönüştürme operatörüyle volatile'lığı atarak volatile T * türünden T * türüne dönüştürme yapılabilir. const ve volatile belirleyicilerine kısaca İngilizce "cv qualifiers" denilmektedir. const ve volatile birlikte kullanılabilir. Örneğin: const volatile int g_a = 10; enum Türleri ve Sabitleri enum türleri C'de bir grup sembolik sabiti kolay bir biçimde oluşturmak için kullanılmaktadır. enum bildiriminin genel biçimi şöyledir: enum [isim] {}; enum sabit listesi ',' atomuyla ayrılan isimlerden oluşur. Örneğin: enum COLORS {Red, Green, Blue, Yellow}; enum DAYS {Sunday, Monday, Tuesday, Wednesday, Thursday, Friday, Saturday}; İlk enum sabitinin değeri sıfırdır. Sonraki her sabit öncekinden bir fazladır. Örneğin: 230 Enum sabitlerine İnglizce "enumerator" da denilmektedir. Enum sabitleri nesne belirtmezler. Derleyici enum sabitleri kullanıldığında onlar yerine koda gerçekten o sabitin değerini yerleştirir. Yani enum sabitleri #define oluşturulmuş sembolik sabitlere benzetilebilir. Ancak #define önişlemciye ilişkindir. Halbuki enum türleri ve sabitleri derleme aşamasına ilişkindir. Enum bildirimi global ya da yerel düzeyde yapılabilir. Bu durumda enum sabitlerinin de faaliyet alanı global ya da yerel olmaktadır. Örneğin: #include enum {Red, Green, Blue}; int main(void) { int color, city; enum {Adana, Ankara, Eskisehir}; color = Green; printf("%d\n", color); /* geçerli */ city = Ankara; printf("%d\n", city); /* geçerli */ return 0; } void Foo() { int color, city; color = Green; printf("%d\n", color); /* geçerli */ city = Ankara; printf("%d\n", city); /* geçersiz */ } Bir enum sabitine '=' atomu ile bir değer verilebilir. Bu durumda sonrakiler onu izler. Aynı değere sahip birden fazla enum sabiti bulunabilir. Örneğin: enum COLORS {Red = 10, Green, Blue = -5, Yellow, Magenta = 9, Purple}; Burada Red = 10, Green = 11, Blue = -5, Yellow = -4, Magenta = 9, Purple = 10 değerlerini almaktadır. C'de enum türleri int türü gibi değerlendirilir. Bu durumda enum sabitleri de sanki int türden sabitlermiş gibi ele alınacaklardır. enum türleri işlem öncesinde yine büyük türe dönüştürülürler. Yani tamamen birer tamsayı türü gibi değerlendirilirler. enum türünden nesneler bildirilebilir. Örneğin: 231 enum COLORS color; color = Red; color = 10; /* geçerli */ /* geçerli */ enum türünden nesneler sanki int türündenmiş gibi değerlendirilirler. Böylece bir enum türünden nesneye diğer sayısal türleri doğrudan atayabiliriz. Farklı enum türünden değeri de birbirlerine doğrudan atayabiliriz. Anahtar Notlar: C++'ta, C# ve Java'da enum türleri farklı birer tür belirtmektedir. Yani bir tamsayı türü enum türüne doğrudan atanamaz. enum türlerine aynı türden enum sabitleri atanabilir. Fakat C'de enum türleri sanki int türü gibi değerlendirilmektedir. Enum'lar yukarıda da belirtildiği gibi aynı türden bir dizi sembolik sabiti kolay bir biçimde ifade etmek için kullanılmaktadır. C'de Dosya İşlemleri İşletim sistemi tarafından organize edilen ikincil belleklerde tanımlanmış bölgelere dosya (file) denilmektedir. Dosyaların isimleri vardır. Özellikleri vardır. Dosyaların erişim hakları vardır. Pek çok sistemde her dosyaya herkes erişemez. Modern sistemlerde dosyalar dizinlerin (directories) içerisinde bulunur. Aynı dizin içerisinde aynı isimli birden fazla dosya bulunamaz. Fakat farklı dizinlerde aynı isimli dosyalar bulunabilmektedir: Dosya isimlerinin Windows sistemlerinde büyük harf-küçük harf duyarlılığı yoktur. Fakat Unix/Linux sistemlerinde vardır. Yani örneğin "test.txt" dosyası ile "Test.txt" dosyası Windows'ta aynı isimli isimli dosyalardır. Fakat UNIX/Linux sistemlerinde bunlar farklı isimli dosyalardır. Bir dosyanın yerini belirten yazısal ifadelere yol ifadeleri (path) denilmektedir. Yol ifadeleri mutlak (absolute) ve göreli (relative) olmak üzere ikiye ayrılır. Yol ifadesinin ilk karakteri Windows'ta '\', UNIX/Linux'ta '/' ise böyle yol ifadelerine mutlak (absolute) yol ifadeleri denilmektedir. Mutlak yol ifadeleri kök dizinden itibaren yer belirtir. Örneğin: "/a/b/c.txt" "a/b/c.txt" "a.txt" /* mutlak */ /* göreli */ /* göreli */ Windows'ta dizinler sürücüler (drives) içerisindedir. Her sürücünün ayrı bir kökü (root) vardır. Halbuki UNIX/Linux sistemlerinde toplamda tek bir kök vardır. Bu sistemlerde sürücü kavramı yoktur. Başka bir aygıt bu sistemlere takıldığında onun kökü bir dizinin altında gözükür. Bu işleme "mount işlemi" denilmektedir. Windows'ta sürücü bir harf ve ':' karakterindne oluşur. Örneğin C:, D:, E: gibi... İşletim sistemlerinde çalışmakta olan programlara "process" ya da "task" denilmektedir. Her prosesin bir çalışma dizini (current working directory) vardır. İşte göreli yol ifadeleri prosesin çalışma dizininden itibaren yer belirtir. Örneğin prosesimizin çalışma dizini "/temp" olsun: "/a/b/c.txt" biçimindeki yol ifadesi mutlaktır. Ve her zaman kökten itibaren yer beliritir. Fakat "a/b/c.txt" biçimindeki yol ifadesi görelidir. Bu durumda bu yol ifadesi "/temp/a/b/c.txt" biçiminde ele alınır. Örneğin "a.txt" yol ifadesi görelidir ve prosesin çalışma dizininde aranır. Pekiyi prosesin çalışma dizini neresidir? Prosesin çalışma dizini program çalışırken değiştirilebilir. Fakat program çalışmaya başladığında default olarak çalıştırılabilen dosyanın bulunduğu yerdedir. Visual Studio idesinden çalıştırılan programların çalışma dizini proje dizinidir. Windows sistemlerinde mutlak yol ifadelerinde sürücü belirtilmemişse prosesin çalışma dizini hangi sürücüye 232 ilişkinse o mutlak yol ifadesinin o sürücüye ilişkin olduğu anlaşılır. Örneğin prosesin çalışma dizini "E:\Study" olsun. Biz de "\A\B\C.txt" biçiminde bir yol ifadesi belirtelim. Bu yol ifadesi E'nin kökünden itibaren yer belirtecektir. Tabi mutla k yol ifadeleri sürücü içeriyorsa her zaman o sürücü dikkate alınır. C'de Dosya İşlemleri C'de dosya işlemleri prototipleri içerisinde bulunan standart C fonksiyonlarıyla yapılmaktadır. Bütün dosya fonksiyonlarının isimleri f harfi başlar. Dosya işlemleri aslında işletim sisteminin sunduğu sistem fonksiyonlarıyla yapılmaktadır. Standart C fonksiyonları bu işletim sisteminin sistem fonksiyonlarını çağırarak işlemini yapar. C'de tipik olarak dosya işlemleri 3 aşamda yürütülür: 1) Önce dosya açılır 2) Sonra dosyadan okuma yazma yapılır 3) En sononda dosya kapatılır Dosyanın kapatılması zorunlu değildir. Çünkü program bittiğinde exit işlemi sırasında zaten tüm açık dosyalar kapatılmaktadır. Tabi biz dosyayı çeşitli sebeplerden dolayı program bitmeden erken kapatmak isteyebiliriz. Dosyanın Açılması Dosyayı açmak için fopen fonksiyonu kullanılır. Fonksiyonun prototipi şöyledir: FILE *fopen(const char *path, const char *format); Fonksiyonun birinci parametresi açılacak dosyanın yol ifadesini belirtir. İkinci parametre açış modunu belirtir. Açış modu aşağıdakilerden biri biçiminde olabilir: Açış Modu "r" "r+" "w" "w+" "a" "a+" Anlamı Olan dosyayı açmak için kullanılır. Eğer dosya yoksa fonksiyon başarısız olur. Böyle açılmış dosyalardan yalnızca okuma yapabiliriz. Olan dosyayı açmak için kullanılır. Eğer dosya yoksa fonksiyon başarısız olur. Böyle açılmış dosyalardan hem okuma hem de yazma yapabiliriz (+ okuma yazma anlamına gelir.) Dosya yoksa yaratılır ve açılır. Dosya varsa sıfırlanır ve açılır. Yalnızca yazma yapılabilir. Dosya yoksa yaratılır ve açılır. Dosya varsa sıfırlanır ve açılır. Hem okuma hem de yazma yapılabilir. Dosya yoksa yaratılır ve açılır. Dosya varsa olan dosya açılır. Dosyadan okuma yapamayız. Fakat her yazılan sona ekleme anlamına gelir. (Yani dosyanın ortasına yazma diye bir durum yoktur. Her yazma işlemi ekleme anlamına gelir.) Dosya yoksa yaratılır ve açılır. Dosya varsa olan dosya açılır. Dosyadan okuma yapabiliriz. Fakat her yazılan sona ekleme anlamına gelir. (Yani dosyanın ortasına yazma diye bir durum yoktur. Her yazma işlemi ekleme anlamına gelir.) fopen fonksiyonu başarı durumunda FILE isimli bir yapı türünden adrese geri döner. fopen açılan dosyanın bilgilerini stdio içerisinde bildirilmiş ve FILE ismiyle typedef edilmiş bir yapı nesnesinin içerisine yerleştirmektedir ve o o yapı nesnesinin adresine geri dönmektedir. Programcı FILE yapısının elemanlarını bilmek zoruda değildir. Ancak FILE isminin bir yapı belirttiğini bilmelidir. Fonksiyon başarısız olabilir. Bu durumda fopen NULL adrese geri döner. Mutlaka başarı kontrolü yapılmalıdır. FILE türünden adrese biz "dosya bilgi gösterici" diyeceğiz. İngilizce genellikle bu adrese "file stream" denilmektedir. 233 Örneğin: #include #include int main(void) { FILE *f; if ((f = fopen("test.dat", "w")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } return 0; } Dosyanın Kapatılması Dosyayı kapatmak için fclose fonksiyonu kullanılır. Fonksiyonun prototipi şöyledir: int fclose(FILE *f); Fonksiyon parametre olarak fopen fonksiyonundan alınan FILE yapı adresini (yani dosya bilgi göstericisini) almaktadır. Fonksiyon başarı durumunda sıfır değerine başarıszlık durumunda -1 değerine geri döner. Dosyanın kapatılmasının başarısının kontrol edilmesine gerek yoktur. Zaten dosya başarılı bir biçimde açılmışsa kesinlikle başarılı kapatılır. Örneğin: #include #include int main(void) { FILE *f; if ((f = fopen("test.dat", "w")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } Dosya Göstericisi (File Pointer) Kavramı Dosya byte'lardan oluşan bir topluluktur. Dosyanın içerisinde ne oolursa olsun dosya işletim sistemi için bir byte yığınıdır. Dosyanın her bir byte'ına ilk byte sıfır olmak üzere bir numara verilmiştir. Bu numaraya ilgili byte'ın offset'i denir. Örneğin aşağıdaki çizimde her bir X bir byte'ı gösteriyor olsun. Tüm bu byte'ların bir offset2i vardır. Dosya göstericisi bellekte bir adres belirten daha önce gördüğümüz gibi bir gösterici değildir. Buradaki isim 234 tamamen kavramsal olarak verilmiştir. Dosya göstericisi bir offset belirtir. Bir imleç görevindedir. Tüm okuma ve yazma işlemleri dosya göstericisinin gösterdiği yerden itibaren yapılır. Örneğin dosya göstericisi 2'inci offset'i gösteriyor olsun: Şimdi biz dosyadan 2 byte okuyacak olalım. İşte okuma işlemi dosya göstericisinin gösterdiği yerden itibaren yapılır. Yani 2'inci ve 3'üncü offset'teki byte'lar okunur. Benzer biçimde yazma işlemi de her zaman dosya göstericisinin gösterdiği yerden itibaren yapılır. Okuma ve yazma işlemleri dosya göstericisini okunan ya da yazılan byte miktarı kadar ilerletir. Örneğin dosyaya YY ile temsil edilen iki byte'ı yazmış olalım: Dosya ilk kez açıldığında dosya göstericisi sıfırıncı offset'i gösteriyor durumdadır. EOF Durumu Dosyanın sonında özel hiçbir karakter ya da byte yoktur. Dosya göstericisinin dosyanın son byte'ından sonraki byte'ı göstermesi durumuna EOF durumu denilmektedir. Örneğin: Dosya gösterisici EOF durumundaysa artık o dosyada olan bir byte'ı göstermiyordur. Bu nedenle EOF durumundan okuma yapılamaz. Dosya göstericisi EOF durumundaysa okuma fonksiyonları başarısız olur. Ancak EOF durumunda yazma yapılabilir. Bu durum dosyaya ekleme yapma anlamına gelir. Pekiyi bir dosyayı açıp byte byte okuduğumuzu düşünelim. Ne olur? Biz sırasıyla byte'ları okuruz. Dosya göstericisi en sonında EOF durumuna gelir. Artık okuma yapamayız. fgetc Fonksiyonu fgetc dosyadan bir byte okuyan standart C fonksiyonudur. Prototipi şöyledir: int fgetc(FILE *f); Fonksiyon parametre olarak fopen fonksiyonundan elde edilen dosya bilgi göstericisini alır. Dosya göstericisinin gösterdiği yerdeki byte'ı okur. Okuduğu byte ile geri döner. Geri dönüş değeri int rüdendir. Okuma başarılıysa int bilginin yüksek anlamlı byte'larında sıfır bulunur. En düşük anlamlı byte'ında okunan değer bulunur. fgetc IO 235 hatası nedeniyle ya da EOF nedeniyle okumayı yapamazsa EOF denilen bir değere geri döner: EOF değeri içerisinde -1 olarak define edilmiştir: #define EOF -1 Pekiyi fgetc okuduğu byte'a geri döndüğüne göre neden geri dönüş değeri char değil de int türdendir. Eğer fgetc'in geri dönüş değeri char olsaydı bu durumda dosyadan FF numaralı byte'ı okuduğunda FF = -1 olduğundan fgetc'in başarısız mı olduğu yoksa gerçekten FF numaralı byte'ı mı okuduğu anlaşılamazdı. Halbuki fonksiyonun geri dönüş değeri int olduğu için bu sorun oluşmamaktadır. Yani: 00 00 00 FF ---> FF numaralı byte okunmuş FF FF FF FF --> başarısız olunmuş Örneğin bir dosyayı sonuna kadar okuyup yazdıran örnek program şöyle yazılabilir: #include #include int main(void) { FILE *f; int ch; if ((f = fopen("Sample.c", "r")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) putchar(ch); fclose(f); return 0; } feof ve ferror Fonksiyonları Bir dosya fonksiyonu (örneğin fgetc gibi) başarısız olduğunda bunun nedeni dosya sonuna gelinmiş olması olabilir ya da daha ciddi (genellikle biz artık şey yapamayız) IO hatası olabilir. fgetc fonksiyonu her iki durumda da EOF değeriyle geri dönmektedir. Dosya sonuna gelmiş olmak gayet normal bir durumdur. Halbuki bir disk hatası ciddi bir soruna işaret eder. İşte bizim fgetc gibi bir fonksiyonun neden EOF'a geri döndüğünü tespit edebilmemeiz gerekir. Bunun için feof ve ferror fonksiyonlarından faydalanılır: int feof(FILE *f); int ferror(FILE *f); feof fonksiyonu dosya göstericisinin EOF durumundaolup olmadığını tespit eder. Eğer dosya göstericisi EOF durumundaysa fonksiyon sıfır dışı herhangi bir değere, EOF durumunda değilse 0 değerine geri döner. Ancak bu fonksiyon son okuma işlemi başarısız olduğunda kullanılmalıdır. Şöyle ki: Biz dosyadaki son byte'ı fgetc ile okuduğumuzda dosya göstericisi EOF'a çekildiği halde feof sıfıra geri döner. Bundan sonra bir daha fgetc uygularsak fgetc başarısz olur. Arık feof sıfır dışı değere geri döner. ferror ise son okuma ya da yazma işleminde IO hatası oluşup oluşmadığını belirlemek için kullanılmaktadır. Eğer son dosya işlemi IO hatası nedeniyle balarısız olmuşsa ferror sıfır dışı bir değere, değilse sıfır değerine geri döner. Özellikle son işlemin başarısı iyi bir teknik bakımından kontrol edilmelidir. Örneğin: 236 #include #include int main(void) { FILE *f; int ch; if ((f = fopen("Sample.c", "r")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) putchar(ch); if (ferror(f)) { printf("cannot read file!..\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } Örneğin aşağıdaki gibi dosya sonuna kadar okuma işlemi hatalıdır: while (!feof(f)) { ch = fgetc(f); putchar(ch); } Çünkü burada son byte okunduktan sonra henüz feof sıfır dışı değer vermez. Dolayısıyla program fazladan bir byte okunmuş gibi davranır. Ayrıca burada IO hatası için bir tespit yapılmamıştır. Doğru teknik şöyledir: while ((ch = fgetc(f)) != EOF) putchar(ch); if (ferror(f)) { printf("cannot read file!..\n"); exit(EXIT_FAILURE); } fputc Fonksiyonu fputc dosyaya dosya göstericisinin gösterdiği yere bir byte yazan en temel yazma fonksiyonudur. Prototipi şöyledir: int fputc(int ch, FILE *f); Fonksiyon birinci parametresiyle belirtilen int değerin en düşün anlamlı byte'ını yazar. Baaşarı durumunda yazdığı değer, başarısızlık durumunda EOF değerine geri döner. Örneğin: #include #include int main(void) { 237 FILE *f; char s[] = "this is a test, yes this is a test!"; int i; if ((f = fopen("test.txt", "w")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } for (i = 0; s[i] != '\0'; ++i) if (fputc(s[i], f) == EOF) { printf("cannot write file!..\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } Dosya kopyalayan örnek bir program şöyle yazılabilir: #include #include int main(void) { FILE *fs, *fd; int ch; if ((fs = fopen("sample.c", "r")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } if ((fd = fopen("xxx.c", "w")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } while ((ch = fgetc(fs)) != EOF) if (fputc(ch, fd) == EOF) { printf("cannot write file!..\n"); exit(EXIT_FAILURE); } if (ferror(fs)) { printf("cannot read file!..\n"); exit(EXIT_FAILURE); } printf("success...\n"); fclose(fs); fclose(fd); return 0; } fread ve fwrite Fonksiyonları fread ve fwrite fonksiyonları getc ve fputc fonksiyonlarının n byte okuyan ve n byte yazan genel biçimleridir. fread dosya göstericisinin gösterdiği yerden itibaren n byte'ı okuyarak bellekte verilen adresten itibaren bir diziye yerleştirir. fwrite ise tam tersini yapmaktadır. Yani bellekte verilen bir adresten başlayarak n byte'ı dosya 238 göstericisinin gösterdiği yerden itibaren dosyaya yazar. Fonksiyonların prototipleri şöyledir: size_t fread(void *ptr, size_t size, size_t count, FILE *f); size_t fwrite(const void *ptr, size_t size, size_t count, FILE *f); fread fonksiyonun birinci parametresi bellekteki tarnsfer adresidir. Fonksiyon ikinci ve üçüncü parametrelerin çarpımı kadar byte okur (yani size * count kadar). Son parametre okuma işleminin yapılacağı dosyayı belirtmektedir. Genellikle ikinci parametre okunacak dizinin bir elemanının byte uzunluğu olarak, ikinci parametre ise okunacak eleman uzunluğu olarak girilir. Örneğin dosyada int bilgiler olsun ve biz 10 int değeri okumak isteyelim: int a[10]; fread(a, sizeof(int), 10, f); fread fonksiyonu başarı durumunda okuyabildiği parça sayısına geri döner. Başarıszlık durumunda sıfır değerine geri dönmektedir. fread fonksiyonu ile dosyada olandan daha fazla byte okunmak istenebilir. Bu durumda fread okuyabildiği kadar byte'ı okur ve okuyabildiği parça sayısına geri döner. Örneğin dosya göstericisinin gösterdiği yerden itibaren dosyada 20 byte kalmış olsun. Biz de aşağıdaki gibi bir okuma yapmış olalım: fread(a, sizeof(int), 10, f); Biz buarad 40 byte okuma talep etmiş olabiliriz. Ancak fread kalan 20 byte'ın hepsini okur ve 5 değerine geri öner. Yani fread toplam okunan byte sayısının ikinci parametrede belirtilen değere bölümüne geri dönmektedir. fwrite fonksiyonu da tamamen aynı biçimde çalışır. Tek fark fwrite dosyadan belleğe okuma değil bellekten dosyaya yazma yapmaktadır. fwrite da başarı durumunda yazılan parça sayısına başarısızlık durumunda sıfır değerine geri döner. Örneğin 10 elemanlı bir int dizi dosyaya tek hamlede fwrite fonksiyonuyla şöyle yazdırılabilir: #include #include int main(void) { FILE *f; int a[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10 }; if ((f = fopen("Sample.dat", "wb")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } if (fwrite(a, sizeof(int), 10, f) != 10) { printf("cannot write file!..\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } Şimdi de yazdığımız sayıları tek hamlede fread ile okuyalım: #include 239 #include int main(void) { FILE *f; int a[10]; int i; if ((f = fopen("Sample.dat", "rb")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } if (fread(a, sizeof(int), 10, f) != 10) { printf("cannot read file!..\n"); exit(EXIT_FAILURE); } for (i = 0; i < 10; ++i) printf("%d ", a[i]); printf("\n"); fclose(f); return 0; } Şimdi de bir dosyadan belli bir miktar okuyup diğerine yazarak dosya kopyalaması yapmaya çalışalım: #include #include #define BUF_SIZE 512 int main(void) { FILE *fs, *fd; char buf[BUF_SIZE]; size_t n; if ((fs = fopen("sample.c", "rb")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } if ((fd = fopen("xxx.c", "wb")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } while ((n = fread(buf, 1, BUF_SIZE, fs)) > 0) if (fwrite(buf, 1, n, fd) != n) { printf("cannot write file!..\n"); exit(EXIT_FAILURE); } if (ferror(fs)) { printf("cannot read file!..\n"); exit(EXIT_FAILURE); } printf("success...\n"); fclose(fs); fclose(fd); 240 return 0; } Yapıların Dosyalara Yazılması ve Okunması Yapı elemanları bellekte ardışıl bir biçimde tutulduğuna göre, biz bir yapıyı tek hamlede fwrite fonksiyonuyla yazıp, fread fonksiyonuyla okuyabiliriz. Örneğin struct PERSON türünden per isimli bir yapı nesnesi olsun: fwrite(&per, sizeof(struct PERSON), 1, f); Klavyeden girilen değerlerden oluşan yapı nesneleri fwrite fonksiyonuyla dosyaya yazdırılmaktadır: #include #include #include typedef struct tagPERSON { char name[32]; int no; } PERSON; int main(void) { FILE *f; PERSON per; if ((f = fopen("person.dat", "wb")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } for (;;) { printf("Adi soyadi:"); gets(per.name); if (!strcmp(per.name, "exit")) break; printf("No:"); scanf("%d", &per.no); getchar(); /* tamponu boşaltmak için, şimdilik takılmayın */ if (fwrite(&per, sizeof(per), 1, f) != 1) { printf("cannot write file!..\n"); exit(EXIT_FAILURE); } } fclose(f); return 0; } Aynı dosyadan kayıtları okuyan program da şöyle yazılabilir: #include #include typedef struct tagPERSON { char name[32]; int no; } PERSON; int main(void) 241 { FILE *f; PERSON per; if ((f = fopen("person.dat", "rb")) == NULL) { printf("cannot open file!..\n"); exit(EXIT_FAILURE); } while (fread(&per, sizeof(per), 1, f) > 0) printf("%s, %d\n", per.name, per.no); if (ferror(f)) { printf("cannot read file!..\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } fprintf ve fscanf fonksiyonları fprintf fonksiyonu printf gibi çalışan fakat dosyaya yazdırma amaçlı kullanılan standart C fonksiyonudur. İlk parametresi dışında diğer parametreleri printf fonksiyonu ile aynıdır. fprintf fonksiyonunun prototipi aşağıdaki gibidir. int fprintf(FILE *f, const char *format, ...); Fonksiyonun birinci paramtresi yazılacak dosyayı belirtmektedir. Fonksiyonun ikinci parametresi formatlanması istenen yazıdır. printf fonksiyonu ile aynıdır. fprintf fonksiyonu aşağıdaki gibi kullanılabilir: #include #include int main(void) { FILE *f; char file_name[] = "test.txt"; int i; if ((f = fopen(file_name, "w")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } for (i = 0; i < 10; ++i) fprintf(f, "sayı=%d\n", i + 1); fclose(f); return 0; } Burada fprintf fonksiyonu ile formatlı bir şekilde dosyaya yazma yapılmıştır. Yukarıda yazma yapılan dosyanın içeriği okunarak ekrana aşağıdaki gibi bir programla yazdırılabilir: #include #include 242 int main(void) { FILE *f; char file_name[] = "test.txt"; int i, ch; if ((f = fopen(file_name, "r")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) putchar(ch); if (ferror(f)) { printf("can not read file...!"); exit(EXIT_FAILURE); } fclose(f); return 0; } fprintf fonksiyonu ile dosyaya formatlı bir bilgi yazılmaktadır. Aşağıdaki programlar fprintf ve fwrite fonksiyonlarının farkını göstermektedir: #include #include int main(void) { FILE *f; char file_name[] = "test.txt"; int val, ch; if ((f = fopen(file_name, "w")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } val = 1234; fprintf(f, "%d\n", val); fclose(f); if ((f = fopen(file_name, "r")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) putchar(ch); fclose(f); return 0; } #include #include 243 int main(void) { FILE *f; char file_name[] = "test.txt"; int val, ch; if ((f = fopen(file_name, "w")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } val = 1234; fwrite(&val, sizeof(int), 1, f); fclose(f); if ((f = fopen(file_name, "r")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) putchar(ch); fclose(f); return 0; } Anahtar Notlar: Bilindiği fwrite ve fread fonksiyonları dosyaya ikili olarak bilgi yazıp okumaktadırlar: #include #include int main(void) { FILE *f; char file_name[] = "test.txt"; int val, i; if ((f = fopen(file_name, "wb")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } for (i = 0; i < 4; ++i) fwrite(&i, sizeof(int), 1, f); fclose(f); if ((f = fopen(file_name, "rb")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } while (fread(&val, sizeof(int), 1, f) > 0) printf("%d\n", val); fclose(f); 244 return 0; } fscanf fonksiyonu scanf fonksiyonu gibi ancak dosyadan okuma yapma amaçlı kullanılmaktadır. fscanf fonksiyonu dosyadan formatlı olarak okuma yapar. Fonksiyonun prototipi aşağıdaki gibidir: int fscanf(FILE *f, const char *format, ...); Fonksiyonun birinci parametresi okuma yapılacak dosyayı belirtmektedir. Diğer parametreleri scanf fonksiyonu ile aynıdır. fscanf fonksiyonu ile dosyadan okuma işlemi aşağıdaki gibi yapılabilir. #include #include int main(void) { FILE *f; char file_name[] = "test.txt"; int a, b; if ((f = fopen(file_name, "r")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } fscanf(f, "%d %d", &a, &b); printf("a=%d\nb=%d\n", a, b); fclose(f); return 0; } fscanf fonksiyonun geri dönüş değeri dosyadan okunarak bellekteki alanlara yazılan değer sayısıdır. Hiç bir yazma yapılmadıysa fonksiyon 0(sıfır) değerine geri döner. Eğer ilk alana atama yapılmadan dosyanın sonuna gelinmişse, ya da bir hata oluşmuşsa fonsiyon EOF değerine geri döner. Aşağıdaki program her bir satırında 4 adet sayı olan bir dosyadan okuma yapmaktadır ve her bir dörtlü grubun toplamını bulmaktadır: #include #include int main(void) { FILE *f; char file_name[] = "test.txt"; int a, b, c, d; if ((f = fopen(file_name, "r")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } while (fscanf(f, "%d %d %d %d", &a, &b, &c, &d) != EOF) printf("%d\n", a + b + c + d); if (ferror(f)) { 245 printf("can not read file....!\n"); exit(EXIT_FAILURE); } fclose(f); return 0; } fscanf fonksiyonu ile her okunan bilgi dönüştürülerek bir bellek alanına aktarılmak zorunda değildir. Böyle okumalarda % ile format karakteri arasına * konularak okunan değerin devre dışı bırakılması (ama okunması) sağlanabilir. Örneğin: #include #include int main(void) { FILE *f; char file_name[] = "test.txt"; int a, b; if ((f = fopen(file_name, "r")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } fscanf(f, "%d%*s%d", &a, &b); if (ferror(f)) { printf("can not read file....!\n"); exit(EXIT_FAILURE); } printf("a=%d\n", a); printf("b=%d\n", b); fclose(f); return 0; } Burada dosya içerisinde örneğin 10 ------ 20 gibi bir bilgiden yalnızca 10 ve 20 sayıları okunmaktadır. Sınıf Çalışması: Formatı aşağıdaki gibi olan bir dosya içerisinde dosya formatının geçerliliğini kontrol etmeden yaş ortalamasını bulan program Dosya formatı: isim mert yas 18 isim oguz yas 40 isim serkan yas 35 isim mustafa yas 42 #include 246 #include int main(void) { FILE *f; char file_name[] = "test.txt"; int age, sum = 0, count = 0; if ((f = fopen(file_name, "r")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } while (fscanf(f, "%*s%*s%*s%d", &age) != EOF) { sum += age; count++; } if (ferror(f)) { printf("can not read file....!\n"); exit(EXIT_FAILURE); } printf("Yas Ortalamasi:%lf\n", (double) sum / count); fclose(f); return 0; } Sınıf Çalışması: Formatı aşağıdaki gibi olan bir dosya içerisinde dosya formatının geçerliliğini kontrol etmeden değerlerin ortalamasını bulan programı yazınız. Firma1 30 40 56 Firma2 10 20 35 Firma3 10 20 33 #include #include #define MAX_LEN 32 typedef struct tagCOMPANYINFO { char name[MAX_LEN]; int val1, val2, val3; } COMPANYINFO; int main(void) { FILE *f; char file_name[] = "test.txt"; int i; COMPANYINFO companies[3]; if ((f = fopen(file_name, "r")) == NULL) { printf("can not open file...%s\n", file_name); exit(EXIT_FAILURE); } for (i = 0; i < 3; ++i) { 247 fscanf(f, "%s &companies[i].val3); } %d %d %d", companies[i].name, &companies[i].val1, &companies[i].val2, if (ferror(f)) { printf("can not read file....!\n"); exit(EXIT_FAILURE); } for (i = 0; i < 3; ++i) { double avg = (companies[i].val1 + companies[i].val2 + companies[i].val3) / 3.; printf("%s ---> %lf\n", companies[i].name, avg); } fclose(f); return 0; } Aşağıdaki program rasgele ad, soyad ve not belirleyerek grades.txt isimli dosyaya formatlı bir şekilde yazma yapmaktadır. #include #include #include void exit_sys(const char *msg); int main(void) { FILE *f; char file_name[] = "grades.txt"; int i; int number_of_students; char *names[8] = {"oguz", "mert", "durukan", "boran", "mustafa", "serkan", "onur", "latif"}; char *snames[8] = { "aksoy", "kaya", "vural", "altan", "karpuz", "sert", "mulaim", "naif" }; if ((f = fopen(file_name, "w")) == NULL) exit_sys("can not open file\n"); srand((unsigned)time(NULL)); number_of_students = rand() % 20 + 30; for (i = 0; i < number_of_students; ++i) fprintf(f, "%s %s %d\n", names[rand() % 8], snames[rand() % 8], rand() % 101); fclose(f); return 0; } void exit_sys(const char *msg) { printf(msg); exit(EXIT_FAILURE); } Sınıf Çalışması: Yukarıda oluşturulan grades.txt dosyasını kullanarak belirli bir geçme değeri belirleyip, geçenleri pass.txt kalanları fail.txt isimli dosyaya formatlı olarak yazdıran programı yazınız. #include #include 248 #include #define PASS_GRADE 50 void exit_sys(const char *msg); int main(void) { FILE *fgrades, *fpass, *ffail; char grades_file_name[] = "grades.txt"; char pass_file_name[] = "pass.txt"; char fail_file_name[] = "fail.txt"; char name[20], sname[20]; int grade; if ((fgrades = fopen(grades_file_name, "r")) == NULL) exit_sys("can not open file\n"); if ((fpass = fopen(pass_file_name, "w")) == NULL) exit_sys("can not open file\n"); if ((ffail = fopen(fail_file_name, "w")) == NULL) exit_sys("can not open file\n"); while (fscanf(fgrades, "%s%s%d", name, sname, &grade) != EOF) if (grade < PASS_GRADE) fprintf(ffail, "%s %s %d\n", name, sname, grade); else fprintf(fpass, "%s %s %d\n", name, sname, grade); if (ferror(fgrades)) exit_sys("can not read file\n"); fclose(fgrades); fclose(fpass); fclose(ffail); return 0; } void exit_sys(const char *msg) { printf(msg); exit(EXIT_FAILURE); } Metin ve İkili Dosyalar C de bir dosya metin (text) ya da ikili (binary) modda açılabilmektedir. Açılış modunda hiç belirtme yapılmassa default olarak dosya text modda açılır. Programcı isterse açılış mod yazısının sonuna "t" ekleyerek de açılışı text modda yaptırabilir. Örneğin: if (f = fopen("test.txt", "w") == NULL) /*...*/ ya da örneğin if (f = fopen("test.txt", "wt") == NULL) /*...*/ Bir dosyayı binary modda açmak için mod yazısının "b" getirilmelidir. Örneğin: 249 if (f = fopen("test.txt", "wb") == NULL) /*...*/ Text ve binary modda açılan dosyalar için Windows ve Unix/Linux sistemlerinde farklılıklar bulunmaktadır. Bir dosya text modda açılmışsa ve çalışılan sistem windows ise yazma yapan herhangi bir fonksiyon Line feed (LF) ('\n') karakterini yazdığında aslında dosyaya Carriage Return (CR)('\r') ve LF karakterlerinin ikisi birden yazılır. Benzer şekilde dosyadan okuma yapan fonksiyonlar çalışılan sistem windows ise ve dosya text modda açılmışsa CRLF karakterlerini yanyana gördüğünde yalnızca LF olarak okuma yaparlar. Aşağıdaki program bir text modda açılmış bir dosyaya beş adet LF karakteri yazmaktadır: #include #include int main(void) { FILE *f; int i; if ((f = fopen("test.txt", "w")) == NULL) { printf("can not open file"); exit(EXIT_FAILURE); } for (i = 0; i < 5; ++i) fputc('\n', f); fclose(f); return 0; } Bu program Windows sistemlerinde çalıştırıldığında yazma yapılan dosya 10 byte olacaktır. Çünkü her LF yazması aslında CR ve LF karakterlerinin ikisinin birden yazılması demektir. Unix/Linux sistemlerinde text dosya ve binary dosya arasında hiç bir fark yoktur. Yani yukarıdaki program Unix/Linux sistemlerinde çalıştırıldığında 5 byte olacaktır. Çünkü beş adet LF karakteri dosyaya yazılmıştır. Aşağıdaki program az önce üretilmiş olan dosya içerisindeki karakterleri okumaktadır: #include #include int main(void) { FILE *f; int i; int ch; if ((f = fopen("test.txt", "rb")) == NULL) { printf("can not open file"); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) printf("%d\n", ch); fclose(f); return 0; 250 } Bu program çalıştırıldığında binary modda olduğundan CRLF çiftlerini okuyacaktır. Metin dosyaları için Windows sistemlerinde diğer sistemlere bir fark daha vardır. Windows sistemlerinde text modunda okuma yapılırken 26 numaralı Ctrl+Z karakteri sonlandırıcı karakter olarak kabul edilir. Örneğin aşağıdaki program bir exe dosyanın tüm karakterini text modda okumaktadır: #include #include int main(void) { FILE *f; int i; int ch; long count = 0; if ((f = fopen("Sample.exe", "r")) == NULL) { printf("can not open file"); exit(EXIT_FAILURE); } while ((ch = fgetc(f)) != EOF) count++; printf("Count=%ld\n", count); fclose(f); return 0; } Bu program windows sistemlerinde çalıştırıldığında dosya içerisinde ctrlz karakteri görüldüğünde okuma sonlanacaktır. Ancak aynı program Unix/Linux sistemlerinde çalıştırıldığında herhangi sonlanma olmayacaktır. Dosya Göstericisinin Konumlandırılması ve fseek Fonksiyonu Anımsanacağı gibi dosya açıldığında dosya göstericisi 0'ıncı offset'tedir. Okuma yazma işlemleriyle dosya göstericisi otomatik ilerletilmektedir. Ancak biz isteresek dosya göstericisini fseek fonkisyonuyla istediğimiz offset'e konumlandırabiliriz. fseek fonksiyonunun prototipi şöyledir: #include int fseek(FILE *stream, long offset, int whence); Fonksiyonun birinci parametres, konumlandırılacak dosyaya ilişkin dosya bilgi göstericisini alır. İ;kinci parametre dosya opffset'ini belirtmektedir. Üçüncü parametre konumlandırma orijinini belirtir. Üçüncü parametre üç değerden birisi olabilir: 0, 1 veya 2. Bu değerler aynı zamanda aşağıdaki gibi define da edilmiştir: #define SEEK_SET #define SEEK_CUR #define SEEK_END 0 1 2 Eğer üçüncü paramere sıfır girilirse ikinci parametre dosyanın başından itibaren offset belirtir. Yani ikinci parametre >= 0 olmak zorundadır. Örneğin: 251 fseek(f, 100, SEEK_SET); Burada dosya göstericisi 100'üncü offset'e konumlandırılmıştır. Eğer üçüncü parametre 1 girilirse konumlandırma dosya göstericisinin gösterdiği yerden itibaren yapılır. Tabi bu durumda ikinci parametre sıfır, pozitif ya da negatif olabilir. Örneğin: fseek(f, 1, SEEK_CUR); Bu işlemle dosya göstericisi neredeyse onun 1 ilerisine konumlandırılır. Örneğin: fseek(f, -10, SEEK_CUR); Burada dosya göstericisi 10 geriye konumlandırılmaktadır. Eğer son parametre 2 girilirse bu durumda konumlandırma EOF'dan itibaren yapılır. Tabi bu durumda ikinci parametre <= 0 girilmek zorundadır. Örneğin: fseek(f, 0, SEEK_END); Bu durumda dosua göstericisi EOF'a konumlandırılır. Yani artık yazma yapmak istediğimizde dosyaya ekleme yapmış oluruz. Fonksiyon konumlandırma başarılıysa 0 değerine, başarısızsa EOF (-1) değerine geri döner. Örneğin: #include #include int main(void) { FILE *f; if ((f = fopen("Test.txt", "r+")) == NULL) { printf("can not open file"); exit(EXIT_FAILURE); } fseek(f, 0, SEEK_END); fprintf(f, "Yes, this is a test"); fclose(f); return 0; } Burada dosya göstericisi dosyanın sonuna çekilip yazma yapılmıştır. Örneğin: #include #include int main(void) { FILE *f; char s[10]; if ((f = fopen("Test.txt", "r+")) == NULL) { printf("can not open file"); exit(EXIT_FAILURE); } 252 fseek(f, 10, SEEK_SET); fgets(s, 5, f); puts(s); fclose(f); return 0; } Burada dosya göstericisi dosyanın 10'uncu offset'ine çekilip okuma yapılmıştır. C'de okumadan yazmaya yazmadan okumaya geçişte mutlaka fseek ya da fflush işlemi yapılmalıdır. stdin, stdout ve stderr Dosyaları Bir C programında stdin, stdout ve stderr FILE * türünden dosya bilgi göstericisi belirtmektedir. Bu isimler dosyasında bildirilmiştir. (Dolayısıyla bu isimler anahtar sözcük değildir. Bunları kullanabilmemiz için dosyasını include etmeliyiz. stdin dosyasına "standard input" dosyası, stdout dosyasına "standart output" dosyası, stderr dosyasına ise "standard error dosyası" denilmektedir. C standartlarında "klavye" ve "ekran" lafı edilmemiştir. Standartlara göre stdout ekrana ya da istenirse başka aygıtlara yönlendirilebilir. Örneğin printf stdout nereye yönlendirilmişse oraya yazar. stdout eğer yazıcıya yönlendirilmişse printf de yazıcıya yazdıracaktır. Bugün kullandığımız masaüstü ve dizüstü bilgisayarlarda stdout default olarak ekrana yönlendirilmiş durumdadır. Fakat biz onu değiştirebiliriz. Örneğin standartlara göre getchar, gets fonksiyonlar stdin dosyasından okuma yapar. stdin de bilgisayarlarımızda default olarak klavyeye yönlendirilmiş durumdadır. Fakat biz onu başka aygıtlara yönlendirebiliriz. Örneğin: printf(.....); çağrısının aslında, fprintf(stdout, ....); çağrısından hiçbir farkı yoktur. Örneğin: #include int main(void) { fprintf(stdout, "This is a test\n"); return 0; } Benzer biçimde örneğin getchar aslında fgetc(stdin) ile aynı işlemi yapar. stderr dosyası hata mesajlarının yazdırırlacağı dosyayı temsil eder. Default durumda stderr dosyası da ekrana yönlendirilmiştir. Ancak biz onu dosyaya ya da başka aygıta yönlendirebiliriz. Programın hata msajlarının stdout yerine fprintf fonksiyonuyla stderr dosyasına yazdırılması iyi bir tekniktir. Örneğin: if ((p = malloc(size)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); 253 exit(EXIT_FAILURE); } Windows'ta ve UNIX/Linux sistemerinde stdout dosyasını yönlendirmek için komut satırında > işareti kullanılır. Örneğin: sample > x.txt Benzer biçimde stderr dosyası 2> sembolüyle yönlendirilir. Örneğin: sample 2> y.txt Burada stdout dosyasına yazılanlar yine ekrana çıkar fakat stderr dosyasına yazılanlar y.txt dosyasına yazılır. İki yönlendirme beraber de kullanılabilir: sample > x.txt 2> y.txt stdin dosyasını yönlendirmek için < sembolü kullanılmaktadır. Örneğin: sample < x.txt Buarada artık stdin dosyasından okuma yapıldığında sanki x.txt dosyasındakiler klavyeden girilmiş gibi işlem gerçekleşir. Bit Operatörleri (Bitwise Operators) Bit operatörleri sayıların karşılıklı bitleri üzerinde işlem yapan operatörlerdir. Bit operatörleri şunlardır: ~ << >> & | ^ Bit Not (Bitwise Not) Sola Öteleme (left shift) Sağa Öteleme (right shift) Bit And (Bitwise And) Bit Or (Bitwise Or) Bit Exor (Bit Exclusive Or) Bit And ve Bit Or Operatörleri Tek ampsersand Bit And, Tek çubuk Bit Or operatörü anlamına gelir. (Anımsanacağı gibi bunlardan iki tane yan yana getirilirse mantıksal And ve Mantıksal Or operatörleri anlaşılır.) Bu operatörler sayının karşılıklı bitlerini And ve Or işlemine sokarlar. Örneğin: #include int main(void) { unsigned char a = 0x5F; unsigned char b = 0xC4; unsigned c; c = a & b; printf("%02X\n", c); /* 0x44 */ c = a | b; 254 printf("%02X\n", c); /* 0xDF */ return 0; } Soru: Bir sayının n'inci bitinin durumunu belirleyiniz. Yanıt: Sayı tüm bitleri sıfır n'inci 1 olan bir sayıyla Bit And işlemine sokulur. Sonuç sıfırsa n'inci sıfır, sonuç sıfırdan farklıysa n'inci bir 1'dir. Soru: Sayının diğer bitlerine dokunmadan n'inci bitini 0 yapınız. Yanıt: O sayıyı bütün bitleri 1 olan n'inci biti 0 olan bir sayıyla Bit And işlemine sokarız. Soru: Sayının diğer bitlerine dokunmadan n'inci bitini 1 yapınız. Yanıt: O sayıyı bütün bitleri 0 olan n'inci biti 1 olan bir sayıyla Bit Or işlemine sokarız. Örneğin: #include int main(void) { unsigned char a = 0x7F; a &= 0xDF; printf("%02X\n", a); /* 0x5F */ return 0; } Bit Exor Operatörü Eğer bitler aynıysa 0 değerini veren bitler farklıysa 1 değerini veren işleme EXOR (exclusive Or) işlemi denilmektedir. EXOR işleminin doğruluk tablosu şöyledir: a 0 0 1 1 b 0 1 0 1 a^b 0 1 1 0 Örneğin: #include int main(void) { unsigned char a = 0x9A; unsigned char b = 0x7C; unsigned char c; 255 c = a ^ b; printf("%02X\n", c); /* 0xE6 */ return 0; } Exor geri dönüşümlü bir operatördür. Bu nedenle şifreleme işlemlerinde çok kullanılır. Yani a ^ b = c ise, c ^ b = a ve c ^ a = b'dir. Böylece bir dosyanın byte'ları birtakım değerlerle Exor çekilerek bozulmuşsa yine aynı değerlerle exor çekilerek düzeltilebilir. (Yani değeri bozan anahtarla açan anahtar aynı olabilmektedi.) Örneğin Exor ile şifreleme için şöyle bir örnek verilebilir: #include #include int main(void) { FILE *f; char passwd[128]; unsigned seed; int i; int ch; printf("Enter password:"); gets(passwd); seed = 0; for (i = 0; passwd[i] != '\0'; ++i) seed = seed * 31 + passwd[i]; if ((f = fopen("test.txt", "r+")) == NULL) { fprintf(stderr, "cannot open file!..\n"); exit(EXIT_FAILURE); } srand(seed); while ((ch = fgetc(f)) != EOF) { ch ^= rand() % 256; fseek(f, -1, SEEK_CUR); fputc(ch, f); fseek(f, 0, SEEK_CUR); } fclose(f); return 0; } Exor işleminde 0 etkisiz elemandır. 1 ise tersleme işlemi yapar. Soru: Bir sayının diğer bitlerine dokunmadan n'inci bitini onun tersiyle yer değiştiriniz. Yanıt: Sayı tüm bitleri 0 olan n'inci biti 1 olan bir sayıyla Exor çekilir. Bit Not Operatörü C'de ~ sembolü tek operandlı önek bir bit operatörüdür. Sayının 0 olan bitlerini 1, 1 olan bitlerini sıfır yapar. Örneğin : #include 256 int main(void) { unsigned a = 0, b; b = ~a; printf("%u\n", b); /* 4294967295 */ return 0; } &, |, ^ ve ~ Operatörlerinin Öncelik Tablosundaki Yerleşimi Bit operatörlerinden ~ operatörü tek operandlı olduğu için öncelik tablosunun ikinci düzeyinde sağdan-sola grupta bulunur. & ve | operatörleri mantıksal operatörlerden daha önceliklidir. ( ) [ ] -> . 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 ?: Sağdan Sola = += -= *= /= %= |= &= ^= Sağdan Sola , Soldan Sağa & ve | operatörleri karşılaştırma operatörlerinden daha düşük önceliklidir. Maalesef programcılar aşağıdaki gibi hataları sık yapmaktadır: if (a & 0x80 != 0) ... } { Burada sayının en yüksek anlamlı bitinin 1 olup olmadığında bakılmak istenmiştir. Ancak != operatörü & operatöründen daha öncelikli olduğu için kullanım hatalıdır. İşlemin şöyle yapılması gerekirdi: if ((a & 0x80) != 0) ... } { Bazı derleyiciler bu tür durumlarda uyarı ile programcının dikkatini çekmektedir. Sola ve Sağa Öteleme Operatörleri 257 C'de << operatörüne sola öteleme (left shift), >> operatörüne ise sağa öteleme (right shift) operatörü denilmektedir. Bu operatörler iki operandlı araek operatörlerdir. Öteleme operatörlerinin sol tarafındaki operand'lar ötelenecek değeri, sağ tarafındaki operand'lar öteleme miktarını belirtir. Sola ötelemede her bir bir sola kaydırlır. En soldaki bit kaybedilir, en sağdan sıfırla besleme yapılır. Örneğin: Sayıyı 1 kez sola ötelemek onu ikiyle çarpmak anlamına gelir. Örneğin: a = 0x02; b = a << 1; /* 0000 0010 */ /* 0000 0100 */ Örneğin: #include int main(void) { unsigned int a = 2, b; b = a << 1; printf("%d\n", b); return 0; } İşaretli syaılarda sola öteleme işlemi sırasında taşma olabilir. Eğer sola ötelemede taşma olursa tanımsız davranış (undefined behavior) oluşur. Örneğin: int a = -2147483647, b; b = a << 1; /* Bu sayının iki katı int sınırlarının dışında! Tanımsız davranış! */ Örneğin: int a = -1, b; b = a << 1; /* sorun yok b = -2 */ Sağa ötelemede ise sayının bütün bitleri bir sağa kaydırılır. en sağdaki bit kaybedilir. En soldan sıfır ile besleme yapılır. Sayıyı bir kez sağa ötelemek 2'ye tam bölmek anlamına gelir. Örneğin: #include 258 int main(void) { unsigned int a = 100, b; b = a >> 2; printf("%d\n", b); /* 25 */ return 0; } Eğer sayı işaretliyse ve negatifse en soldan besleme 0 ile yapılırsa sayı pğozitif hale gelir. Sayının negatifliğinin korunması için en soldan 1 ile beslenmesi gerekir. İşte standartlara göre işaretli negatif sayıların sağa ötelenmesinde en soldan 0'la mı besleme yapılacağı yoksa 1 ile mi besleme yapılacağı (başka bir deyişle işaret bitinin korunup korunmayacağı) derleyicileri yazanların isteğine bırakılmıştır. Bu nedenle işaretli sayıların sağa ötelenmesine dikkat edilmelidir. Örneğin Microsoft derleyhicileri ve gcc derleyicileri işaret bitini korumaktadır: #include int main(void) { int a = -2, b; b = a >> 2; printf("%d\n", b); /* -1 */ return 0; } Biz sola ya da sağa istediğimiz kadar öteleme yapabiliriz. Kuralları özetlemek gerekirse: - İşaretsiz sayıların sola ve sağa ötelenmesinde tanımsız davranış ya da derleyiciye bağlı davarnış gözükmez. Sola ötelemede taşma olsa bile yüksek alnmlı bit atılır. - İşaretli sayıların sola ötelenmesinde taşma olursa tanımsız davranış oluşur. - İşaretli negatif sayıların sağa ötelenmesinde işaret bitinin korunup korunmayacağı derleyicileri yazanların isteğine bırakılmıştır. Soru: Sayının n'inci, 1'leyiniz . Fakat n'in değerini bilmiyoruz. (Örneğin n klavyeden giriliyor.) Yanıt: Mademki n değeri bilinmiyor işlem şöyle yapılabilir (a ilgili sayı olsun, n de bit numarasını göstersin): b = a | 1 << n; Soru: Sayının n'inci, 0 layınız. Fakat n'in değerini bilmiyoruz. (Örneğin n klavyeden giriliyor.) Yanıt: İşlem şöyle yapılabilir: b = a & ~(1 << n ) Soru: Klavyeden bir n değeri isteyiniz. Öyle bir sayı oluşturunuz ki, son n biti 1 olsun diğer bitleri 0 olsun. Örneğin n = 4 için şöyle bir sayı oluşturulmalıdır: 0000 1111 n = 2 için şöyle bir sayı oluşturulmalıdır: 259 0000 0011 Yanıt: İşlem şöyle yapılabilir: a = ~(~0U << n); Soru: Klavyeden bir n değeri isteyiniz. Öyle bir sayı oluşturunuz ki ilk n biti 1 olsun diğer bitleri 0 olsun. Örneğin n = 4 için şöyle bir sayı oluşturulmalıdır: 1111 0000 Yanıt: a = ~(~0U >> n) Soru: İşaretsiz bir sayı var. 1 olan bitlerinin kaç tane olduğunu bulunuz. Yanıt: Eğer döngüsüz yapılacaksa bir look-up table oluşturulur. 1 byte'lık sayılar için bu mümkün olsa da 2 byte'lık ve daha uzun sayılar için sorunludur. Örneğin: #include int main(void) { unsigned n, count; printf("Sayi giriniz:"); scanf("%d", &n); count = 0; do { if (n & 1) ++count; n >>= 1; } while (n); printf("%d\n", count); return 0; } Öteleme operatörleri öncelik tablosunda artimetik operatörlerle karşılaştırma operatörleri arasında bulunur. Öncelik tablonun son şekli şö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 260 | Soldan Sağa && Soldan Sağa || Soldan Sağa ?: Sağdan Sola = += -= *= /= %= |= &= ^= Sağdan Sola , Soldan Sağa Programların Komut Satırı Argümanları Bir programı komut satırından çalıştırırken programdan isminden sonra girilen yazılara komut satırı argümanları (command line arguments) denilmektedir. Standartlara göre C'de main fonksiyonunun geri dönüş değeri int olmak zorundadır. main fonksiyonunun parametresi ya void olmak zorundadır ya da main iki parametreye sahip olabilir. Parametrelerden biri int türden diğeri char ** türündendir. Bu parametre char *[] biçiminde de ifade edilemektedir. Yani main fonksiyonu aşağıdaki iki biçimden biri olarak tanımlanmak zorundadır: int main(void) { ... } int main(int argc, char *argv[]) { ... } İkinci biçimdeki parametre değişken isimleri istenildiği gibi alınabilir. Ancak argc (argument counter) ve argv (argument variable list) isimleri gelenekseldir. main fonksiyonuna bu argümanları işletim sistemi ve derleyicilerin giriş kodları geçirmektedir. Birinci parametre komut satırına girilen boşlukla ayrılmış yazıların sayısını belirtir (programın ismi de dahil). Örneğin: ls -l -i Burada argc 3 olarak geçirilir. Yalnızca programın ismini yazarak programı çalıştırmak isteseydik argc değeri 1 olurdu. argv ise komut satırı argümanlarına ilişkin yazıların başlangıç adreslerinin bulunduğu diziyi belirtir. Yani argv'nin her elemanı sırasıyla programın isminden başlayarak bir argümanı bize verir. Dizinin sonunda NULL adresin bulunacağı garanti edilmektedir. Örneğin: 261 Örneğin: #include int main(int argc, char *argv[]) { int i; for (i = 0; i < argc; ++i) printf("%s\n", argv[i]); return 0; } Aynı döngü şöyle de kurulabilirdi: #include int main(int argc, char *argv[]) { int i; for (i = 0; argv[i] != NULL; ++i) printf("%s\n", argv[i]); return 0; } Visual Studio IDE'sinde komut satırı argümanları proje ayarlarında "Debugging / Command Arguments" kısmından girilebilir. Örneğin type (ya da cat) işlemini yapan bir program şöyle yazılabilir: #include #include int main(int argc, char *argv[]) { FILE *f; int ch; if (argc == 1) { printf("usage: \n"); exit(EXIT_FAILURE); } if (argc > 2) { fprintf(stderr, "too many arguments!..\n"); exit(EXIT_FAILURE); } if ((f = fopen(argv[1], "r")) == NULL) { fprintf(stderr, "cannot open file: %s\n", argv[1]); exit(EXIT_FAILURE); 262 } while ((ch = fgetc(f)) != EOF) putchar(ch); if (ferror(f)) { fprintf(stderr, "IO error!\n"); exit(EXIT_FAILURE); } return 0; } Programın başında komut satırı argümanlarının kontrol edilmesiyle sık karşılaşılmaktadır. Yukarıda da gördüğünüz gibi önce program uygun komut satırı argümanlarıyla çalıştırılmış mı diye bakılmıştır. Komut satırı argümanlarının birer yazı biçiminde bize verildiğine dikkat ediniz. Örneğin biz n tane sayının toplamını yazdıran add isimli bir program yazmak isteyelim: ./add 10 20 30 40 Önce bu tazıları sayıya dönüştürmemiz gerekir. Bunun için ise atoi, atol ya da atof fonksiyonları kullanılmaktadır. atoi, atol ve atof Fonksiyonları Prototipi dosyasında bulunan bu standart C fonksiyonları bizden sayı belirten bir yazıyı parametre olarak alır ve onu gerçekten sayı olarak bize verir: int atoi(const char *str); long atol(const char *str); double atof(const char *str); Örneğin: #include #include int main(void) { char s[] = "123.45"; double d; d = atof(s); printf("%f\n", d); return 0; } Bu fonksiyonlar yazının başındaki boşluk karakterlerini (white space) atarlar. İlk uygun olmayan karakter gördüklerinde işlemini sonlandırırlar. Hiçbir uygun karaktger bulunamazsa bu fonksiyonlar 0 ile geri dönmektedir. atoi fonksiyonu şöyle yazılabilir. #include #include int myatoi(const char *str) { int val = 0; int sign = 1; 263 while (isspace(*str)) ++str; if (*str == '-') { sign = -1; ++str; } while (isdigit(*str)) { val = val * 10 + *str - '0'; ++str; } return val * sign; } int main(void) { char s[] = " -1205"; int i; i = myatoi(s); printf("%d\n", i); /* 12 */ return 0; } Şimdi yukarıda belirtilen add programını yazalım: #include #include int main(int argc, char *argv[]) { double total = 0; int i; for (i = 1; i < argc; ++i) total += atof(argv[i]); printf("%f\n", total); return 0; } itoa ve ltoa Fonksiyonları itoa fonksiyonu atoi fonksiyonun, ltoa fonksiyonu da atol fonksiyonun ters işlemini yapan fonksiyonlardır. Ancak bu fonksiyonlar standart C fonksiyonları değildir. Dolayısıyla bazı C derleyicilerinde bulunmayabilirler. Örneğin bu fonksiyonlar Microsoft ve Borland derleyicilerinde varken, gcc derleyicilerinde bulunmamaktadır. Bunun yerine standart sprintf sonlsiyonu kullanılabilir. sprintf Fonksiyonu sprintf fonksiyonu printf fonksiyonunun char türden bir diziye yazan versiyonudur. Nasıl fprintf fonksiyonu printf'in dosyaya yazan versiyonuysa sprintf de diziye yazan versiyonudur. Örneğin: #include int main(void) { 264 char s[100]; int a = 10, b = 20; sprintf(s, "a = %d, b = %d", a, b); puts(s); return 0; } sprintf fonksiyonun birinci parametresi char türden bir adrestir. Diğer parametreleri printf ile aynıdır. Genel olarak bir sayıyı yazıya dönüştürmek için de sprintf kullanılabilir. Örneğin: #include int main(void) { char s[100]; double d = 12.345; sprintf(s, "%f", d); puts(s); return 0; } sscanf Fonksiyonu Bu fonksiyon scanf'in char türden diziden okuyan versiyonudur. Yani fscanf nasıl dosyadan okuyorsa, sscanf de diziden girilenleri sanki klavyeden (stdin dosyasından) girilmiş gibi ele alır. Örneğin: #include int main(void) { char s[100] = " int a, b; 123 456 "; sscanf(s, "%d%d", &a, &b); printf("a = %d, b = %d\n", a, b); return 0; } sscanf fonksiyonunun birinci parametresi char türden dizinin adresini alır. Diğer parametreleri scanf fonksiyonunda olduğu gibidir. Dolayısıyla bu fonksiyon da yazıyı sayıya dönüştüren atoi, atol ve atof fonksiyonlarının daha genel bir biçim olarak kullanılabilir. Gösteriyi Gösteren Göstericiler (Pointers to Pointer) Göstericiler de birer nesne olduğuna ve bellekte yer kapladığına göre onların da adresleri alınabilir. Bir göstericinin adresi alınırsa nasıl bir göstericiye yerleştirilmelidir. İşte T türünden bir göstericinin adresi T* türünden bir göstericiye yerleştirilebilir. Böyle göstericiler iki tane * atomu ile bildirilirler. Örneğin: int **ppi; Burada ppi bir gösterixiyi gösteren göstericidir. Yani gösterici türünden göstericidir. Biz ppi'yi * operatörüyle kullanırsa (*ppi'yi kapatıp sola bakın) elde ettiğimiz nesne int * türündendir. Yani bir göstericidir. Yani: ppi, int ** türündendir. 265 *ppi, int * türündendir. **ppi, int türdendir. Örneğin: #include int main(void) { int a = 10; int *pi; int **ppi; pi = &a; ppi = π printf("%d\n", **ppi); return 0; } Anımsanacağı gibi bir dizinin ismini bir ifadede kullandığımızda aslında o dizinin adresini kullanmış oluruz. Başka bir deyişle bir dizinin ismi o dizinin ilk elemanın adresi gibidir. O halde bir gösterici dizisinin ismi de o gösterici dizisinin ilk elemanın adresi gibi olacağına göre iki yıldızlı bir göstericiye atanabilir. Örneğin: char *s[10]; Burada s ifadesi char ** türündendir. Yani char türden bir göstericiyi gösteren göstericiye atanabilir: char **ppc; ppc = s; Örneğin biz bir gösterici dizini bir fonksiyona geçirmek istesek fonksiyonun parametre değişkeni göstericiyi gösteren gösterici olmalıdır. Örneğin: #include void disp_names(char **ppnames); int main(void) { char *names[] = { "ali", "veli", "selami", "ayse", "fatma", NULL }; disp_names(names); return 0; } 266 void disp_names(char **ppnames) { int i; for (i = 0; ppnames[i] != NULL; ++i) printf("%s\n", ppnames[i]); } Fonksiyonun göstericiyi gösteren gösterici parametresi yine dizi formunda belirtilebilir. Yani örneğin: void disp_names(char **ppnames) { ... } ile, void disp_names(char *ppnames[]) { ... } aynı anlamdadır. Ya da örneğin: int main(int argc, char *argv[]) { ... } ile, int main(int argc, char **argv) { ... } aynı anlamdadır. Bazen fonksiyona bir gösterici bir göstericinin adresi gönderilir. Fonksiyon da o göstericinin içerisine birşeyler yazabilir. Örneğin: #include #include void mallocstr(char **ptr, size_t size) { *ptr = (char *)malloc(size); if (*ptr == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } } int main(void) { 267 char *str; mallocstr(&str, 32); gets(str); puts(str); return 0; } Bir göstericiyi gösteren göstericiyi 1 artırdığımızda içindeki adresin sayısal değeri kaç artar? Yanıt bir gösterici kadar artar. Örneğin argv char ** türünden olsun. ++argv yaptığımızda argv'nin içerisindeki adres bir gösterici (yani 32 bit sistemlerde 4 byte, 64 bit sistemlerde 8 byte) artacaktır. Örneğin: #include #include int main(int argc, char **argv) { while (*argv != NULL) puts(*argv++); return 0; } Bilindiği gibi C'de void * türünden bir göstericiye biz her türden adresi atayabiliriz. Ancak void ** türünden göstericiye her adresi atayamayız. Yalnızca void * türünden göstericinin adresini atayabiliriz. Örneğin: int a; int *pi; void *pv; void **ppv; pi = &a; pv = pi; ppv = &pv; ppv = π /* geçerli */ /* geçerli */ /* geçerli */ /* geçersiz! */ int * türünün void * türüne atanması normal ve geçerlidir. Ancak bu int ** türünün void ** türüne atanabileceği anlamına gelmez. void ** türüne biz yalnızca void ** türünden bir adresi atayabiliriz. Biz aslında int ** türünü de istersek void * türünden bir göstericiye atayabiliriz. Örneğin: int a; int *pi; int **ppi; void *pv; int **ppi2; pi = &a; ppi = π pv = ppi; ppi2 = (int **) pv; /* geçerli */ /* geçerli */ /* geçerli */ /* tür dönüştürmesi zorunluı değildir */ Gösterici dizileri için dinamik tahsisatlar yapabiliriz. Örneğin önce 5 elemanlı char türden bir gösterici dizisini dinamik olarak tahsis edelim. Sonra da dizinin her elemanının 32 byte'lık char türden dinamik tahsis edilmiş bir diziyi göstermesini sağlayalım: 268 char **ppc; int i; if ((ppc = (char **)malloc(5 * sizeof(char *))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } for (i = 0; i < 5; ++i) if ((ppc[i] = (char *)malloc(32)) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } ... for (i = 0; i < 5; +i) free(ppc[i]); free(ppc); Soru: Yukarıdaki tahsisat sistemini tek malloc ile oluşturunuz. Böylece tek free ile sistem serbest bırakılsın. Yanıt: Sistem için gereken tüm bellek 5 * sizeof(char *) + 5 * 32 byte kadardır. Önce bu tahsis edilir. Sonra da gösterici dizisinin elemanlarının kendi alanlarını göstermesi sağlanır: char **ppc; char *pc; int i; if ((ppc = (char **)malloc(5 * (sizeof(char *) + 32))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } pc = (char *)(ppc + 5); for (i = 0; i < 5; ++i) { ppc[i] = pc; pc += 32; } ... free(ppc); 269 Tabi böyle bir sistemde 32 byte'lık diziler daha sonra realloc ile büyütülü küçültülemezler. C'de aslında göstericiyi gösteren göstericiyi gösteren göstericiler ve daha çok kademeli göstericiler de bildirilebilir. Ancak üç yıldızlı göstericilerin bile pek bir kullanım alanı yoktur. Biz bir göstericiyi gösteren göstericinin adresini üç yıldızlı bir göstericiye yerleştirebiliriz. Örneğin: int a = 10; int *pi; int **ppi; int ***pppi; int ****ppppi; pi = &a; ppi = π pppi = &ppi; ppppi = &pppi; Çok Boyutlu Diziler Çok boyutlu dizler bazı olayları temsil ederken okunabilirliği artırmak için kullanılabilmektedir. Örneğin bir satranç tahtasını temsil etmek için iki boyutlu bir dizi kullanabiliriz. Bir matematiksel bir matrisi temsil etmek için yine iki boyutlu bir diziden faydalanabiliriz. Dizilerin boyut sayıları fazla olabilse de pratikte en çok kullanılan çok boyutlu diziler iki boyutlu dizilerdir. İki boyutlu dizilere matris de denilmektedir. C'de çok boyutlu diziler aslında bellekte tek boyutlu diziymiş gibi tutulmaktadır. Zaten bellek çok boyutlu değildir. Tek boyutludur. Çok boyutlu diziler aslında "dizi dizileri" gibi düşünülmelidir. Örneğin 4x3'lik bir matris aslında her elemanı 3 elemanlık dizi olan 4'lik dizi gibidir. C'de çok boyutlu diziler birden fazla köşeli parantezle bildirilirler. Örneğin: 270 Bildirimdeki ilk köşeli parantez dizi dizisinin uzunluğunu, ikinci köşeli parantez eleman olan dizilerin uzunluğunu belirtir. Örneğin: Çok boyutlu bir diziye birden fazla küme paranteziyle ilkdeğer verilebilir. Örneğin: int a[4][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}, {10, 11, 12}}; Örneğin: #include int main(void) { int a[4][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 }, { 10, 11, 12 } }; int i, k; for (i = 0; i < 4; ++i) { for (k = 0; k < 3; ++k) printf("%d ", a[i][k]); printf("\n"); } return 0; } Aslında iç küme parantezleri ayrıca kullanılmak zorunda değildir. Örneğin: int a[4][3] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12}; Fakat bir elemanın yazılması unutulduğunda diğer elemanlar birer kayabilir. Bu nedenle iç küme parantezlerinin belirtilmesi tavsiye edilir. Biz matriste erişim için yalnızca ilk köşeli parantezi kullanırsak (örneğin a[2] gibi) bu ifade bir nesne belirtmez. Dizi dizisinin o indise ilişkin dizisinin başlangıç adresini belirtir. Örneğin: 271 Örneğin: #include int main(void) { int a[4][3]; int *pi; pi = a[2]; pi[0] = 10; pi[1] = 20; pi[2] = 30; /* geçerli */ /* a[2][0] = 10 */ /* a[2][1] = 20 */ /* a[2][1] = 30 */ printf("%d, %d, %d\n", a[2][0], a[2][1], a[2][2]); return 0; } Bir matrisin yani dizisinin ismi yine o dizi dizisinin başlangıç adresini belirtir. Bir dizi dizisinin başlangıç adresi dizi göstericisine atanabilir. Gösterdiği yer bir dizi olan göstericiye dizi göstericisi (pointer to array) denir. Dizi göstericileri şöyle bildirilir: (*)[]; Örneğin: int (*pa)[4]; Burada pa'nın gösterdiği yerde (yani *pa) int[4] türünden bir nesne başka bir dyişle 4 elemanlı bir dizi vardır. *pa bir nesne değildir, sanki bir dizinin ismi gibidir. İşte int[N][M] türünden bir matrisin ismi böyle bir göstericiye atanabilir. Örneğin: 272 Aşağıdaki gibi bir matris bildirimi yapılmış olsun: int a[4][3]; Burada a'yı ve a[i]'yi atayacağımız göstericiler nasıl olmalıdır? Aşağıdaki gibi bir dizi göstericisi bildirilmiş olsun: int (*pa)[3]; Burada pa'yı bir artırdığımızda pa'nın içerisindeki adres 3 * sizoef(int) kadar artar. Bir matirisin ismini atayacağımız dizi göstericisinin sütun uzunluğuyla matrisin sütun uzunluğu aynı olmak zorundadır. Örneğin: İkiden fazla boyut söz konusu oılduğunda aynı prensip söz konusudur. Örneğin: int a[3][4][5]; Burada a "dizi dizisi dizisidir". a adresini atayacağımız dizi göstericisi de iki köşeli parantez içermelidir: int (*pa)[4][5]; pa = a; 273 Yani birinci boyut dışındaki bütün boyutların uzunlukları belirtilmek zorundadır. Burada *pa nesne değildir. pa[i][k]'da nesne değildir. Ancak pa[i][k][j] bir nesnedir. Pekiyi a[i]'yi atayacağımız gösterici nasıl olmalıdır? Yanit: int (*pa)[5]; biçiminde olmalıdır. Çünkü a[i] bir matrisin adresi gibidir. O da yukarıdaki gibi bir göstericiye atanabilir. Pekiyi bir matrisi fonksiyona parametre olarak nasıl geçirebiliriz: #include void disp(int(*pa)[3], int size) { int i, k; for (i = 0; i < size; ++i) { for (k = 0; k < 3; ++k) printf("%d ", pa[i][k]); printf("\n"); } } int main(void) { int a[4][3] = { { 1, 2, 3 }, { 4, 5, 6 }, { 7, 8, 9 }, { 10, 11, 12 } }; disp(a, 4); return 0; } Göstericiyi Gösteren Göstericiler ve Matrisler Matrisel bir sistem oluşturabilmek için iki seçenek söz konusu olabilir: 1) Önce göstericiyi gösteren gösterici için sonra da onun elemanı olan göstericiler için tahsisat yapmak. Örneğin: int **ppi; int i; ppi = (int **)malloc(4 * sizeof(int *)); for (i = 0; i < 4; ++i) ppi[i] = (int *) malloc(3 * sizeof(int)); 2) Doğrudan çok boyutlu bir dizi kullanmak. 274 int ppi[4][3]; Şüphesiz ikinci biçim bellekte daha az yer kaplamaktadır. Ancak birinci biçimde matrisin her satırı aynı uzunlukta olmak zorunda değildir. Bir matris için dinamik tahisat yapılabilir. Örneğin: int(*pa)[3]; pa = (int(*)[3])malloc(4 * 3 * sizeof(int)); Tabii typedef bildirimi ile türler daha sade gösterilebilir. Örneğin: typedef int(*PA3)[3]; PA3 pa; pa = (PA3)malloc(4 * 3 * sizeof(int)); Makrolar #define önişlemci komutunun parametrik kullanımına makro denilmektedir. Makro yazılırken #define komutunda STR1 yazısı parantez içerir. Parantez içerisindekiler makro parametrelerini belirtir. Makro parametreliyse biz onu kullanırken sanki bir fonksiyon gibi argüman gireriz. Örneğin: Burada square(10) ifadesini önişlemci 10 * 10 biçiminde açmaktadır. a makro parametresidir. #include #define square(a) a * a int main(void) { int result; result = square(10); 275 printf("%d\n", result); return 0; } Örneğin: #include #define square(a) a * a int main(void) { int result; int x = 10; result = square(x); /* result = x * x */ printf("%d\n", result); return 0; } Bir makro fonksiyon gibi işlev görmelidir. Kodu inceleyen kişi onun bir fonksiyon mu yoksa makro mu olduğunu anlamak zorunda kalmamalıdır. Oysa yukarıdaki square tam olarak bir fonksiyon gibi kullanılamaz. Örneğin: Burada önişlemci 5 - 2 ifadesini a kabul edip bunu 5 - 2 * 5 - 2 biçiminde açar. Buradaki problemi ortadan kaldırmak için makro parametreleri paranteze alınmalı ve makro en dıştan ayrıca paranteze alınmalıdır. Örneğin: #define square(a) ((a) * (a)) Şimdi artık square tam olarak b ir fonksiyon etkisi yaratır. En dıştan paranteze alınmasının nedeni yüksek öncelikli opetaörlerden tüm ifadenin etkilenmesini sağlamaktır. Örneğin: #include #define max(a, b) ((a) > (b) ? (a) : (b)) int main(void) { int a = 3, b = 4, c; c = max(a, b); /* c = ((a) > (b) ? (a) : (b)) */ printf("%d\n", c); return 0; } Örneğin: #include #define beep() putchar('\7'); int main(void) { 276 beep(); beep(); return 0; } Makro daha karmaşık olabilir. Bu durumda birden fazla satıra yazmak okunabiliği artırır. Anımsanacağı gibi \ karakterini hemen ENTER (LF) izlerse derleyici onun aşağısındaki satırı aynı satır gibi kabul eder. Örneğin: #include #include #define check(a) if ((a) == 0) { \ fprintf(stderr, "Error\n"); exit(EXIT_FAILURE); \ } \ int main(void) { int a = 0; check(a); return 0; } Ancak çok satırlı ve deyim içeren makrolar yazılırken dikkat edilmelidir. Yukarıdaki check(a); işlemi aslında önişlemci tarafından şöyle açılır: if ((a) == 0) { fprintf(stderr, "Error\n"); exit(EXIT_FAILURE); }; Buradaki son noktalı virgül check çağrısının sonundaki noltalı virgüldür. Gerçi bu noktalı virgül derleme modülü tarafından boş deyim kabul edileceğinden çoğu kez soruna yol açmaz. Fakat aşağıdaki gibi bir kullanımda soruna yol açacaktır: Bu nedenle çok satırlı makroların sanki do-while deyimiymiş gibi yazılması gerekmektedir. Örneğin: 277 Artık if içerisinde çağrılan makro şöyle açılacaktır: Böylece check fonksiyonun sonundaki noktalı virgül do-while'ın noktaı virgülü olur, boş deyim olmaz. Bazı makroları çağırırken ++ ve -- operatörlerinin argümanlarda kullanılmaması gerekir. Örneğin: #define square(a) ((a) * (a)) Biz böyle bir makroyu aşağıdaki gibi çağırmış olalım: int x = 3, y; y = square(++x); Burada square bir fonksiyon olsaydı hiçbir sorun ortaya çıkmazdı. Ancak makro olarak yazıldığında aynı ifadede birden fazla ++x gözüküyor olur. Böyle bir kod derleyici için tanımsız davranışa yol açacaktır. Makro mu Fonksiyon mu? Bir fonksiyonun çağrılması sırasında küçük bir maliyet söz konusudur. Bu maliyeti oluşturan etkenler şunlardır: - Fonksiyon çağrılırken kullanılan CALLve RET makina komutları - Fonksiyona girişte ve çıkışta gereken bazı makina komutları (örneğin stack frame düzüenlemesi için gereken komutlar). - Parametrelerin aktarımı için gereken makina komutları Yukarıdaki makina komutları ortalama 10 civarındadır. Oysa bazen birer satırlık fonksiyonlar yazmak isteyebiliriz. Örneğin: double square(double a) { return a * a; } İşte bir iki satırlık küçük fonksiyonların fonksiyon olarak yazılması onların çağrılması sırasında göreli (çoğu zaman bunun hiçbir önemi yoktur) zaman kaybına yol açabilmektedir. Bu tür fonksiyonların fonksiyon olarak değil de makro olarak organize edilmesi uygun olur. Büyük kodların makro olarak tanımlanması kodu ciddi biçimde büyütebilmektedir. O halde çok küçük fonksiyonlar makro olarak diğerleri normal fonksiyon olarak yazılabilirler. 278 Şüphesiz makrolar başlık dosyalarına yerleştirilmelidir. Onları çağıracak kişi onların fonksiyon mu makro mu olduğunu bilmek zorunda değildir. Örneğin getchar fonksiyonu bazı derleyicilerde makro olarak yazılmıştır. #define getchar() fgetc(stdin) Diğer Önişlemci Komutları Biz şimdiye kadar #include ve #define komutlarını gördük. Bu bölümde diğer önemli bazı önişlemci komutları görülecektir. #if, #else, #elif ve #endif Komutu #if önişlemci komutunun yanında tamsayı türlerine ilişkin bir sabit ifadesi bulunmak zorundadır. Örneğin: #if MAX > 10 .... #else ... #endif Önişlemci #if komutunun yanındaki ifadenin sayısal değerini hesaplar. Bu değer sıfır dışı bir değerse #else'e kadar kısım derleme modülüne verilir, sıfır ise #else ile #endif arasındaki kısım derleme modülüne verilir. #include #include #define SIZE 10000 int main(void) { #if SIZE < 1000 int a[SIZE]; #else int *a; if ((a = (int *)malloc(SIZE * sizeof(int))) == NULL) { fprintf(stderr, "cannot allocate memory!..\n"); exit(EXIT_FAILURE); } #endif ... return 0; } Burada SIZE değeri 1000'den küçükse dizi normal olarak büyükse dinamik olarak tahsis edilmiştir. Komutta #else kısmı olmak zorunda değildir. #if komutu bir önişlemci komutu olduğuna göre işleme sokulması derleme modülünden önce yapılır. #elif komutu #else #if etkisi yaratır ancak bu durumda tek bir #endif yetmektedir. Örneğin: #define SYSTEM 2 #if SYSTEM == 1 /* ... */ #elif SYSTEM == 2 /* ... */ #elif SYSTEM == 3 /* ... */ 279 #endif #ifdef, #else, #endif Komutu #ifdef komutunu bir değişken ismi izlemek zorundadır. Bu değişken ismi bir sembolik sabit ya da makro ismidir. Önişlemci böyle bir sembolik sabitin ya da makronun daha önce #define komutu ile define edilip edilmediğine bakar. (Ancak onun kaç olarak define edildiğine bakmaz). Eğer o sembolik sabit ya da makro daha yukarıda define edilmişse önişlemci #else'e kadar olan kısmı derleme modülüne verir, define edilmemişse #else ile #endif arasındaki kısmı derleme modülüne verir. Örneğin: #include #define TEST int main(void) { #ifdef TEST printf("TEST define edilmis!\n"); #else printf("TEST define edilmemis!\n"); #endif return 0; } C derleyicileri bir sembolik sabiti derleme sırasında define edebilme olanacağını vermiştir. Örneğin gcc ve cl derleyicilerinde -D seçeneği ile bu yapılabilmektedir: gcc -o sample -D TEST sample.c Visual Studio IDE'sinde aynı etki proje seçeneklerinde "C-C++/Preprocessor/Preprocessor Definitions" menüsüyle yapılabilmektedir. #ifdef çeşitli platformlar için farklı kodların derleme dahil edilmesi amacıyla çok sık kullanılır. Örneğin: #ifdef UNIX .... #else ... #endif Komutta #else kısmı olmak zorunda değildir. #ifndef, #else, #endif Komutu Bu komut #ifdef komutunun tam tersi işlem görmektedir. Yani yanındaki sembolik sabit ya da makro define edilmemişse #else'e kadar olan kısım #define edilmişse #else ile #endif arasındaki kısım derleme modülüne verilir. #ifndef include korumalarında (include guards) çok sık kullanılmaktadır. Aynı başlık dosyasının ikinci include edilmesi pek çok derleme hatasının oluşmasına yol açabilir. (Örneğin aynı yapının iki kez bildirilmesi, aynu enum'un ikinci kez bildirilmesi, aynı typedef bildiriminin ikinci kez yapılması geçersizdir.) Bazen kontrolümüz dışında aynı dosyanın birden fazla kez include edilme durumu oluşabilmektedir. Örneğin a.h dosyası ve b.h dosyaları kendi içerisinde x.h dosyasnı include etmiş olsun. Biz de bir uygulamada mecburen a.h ve b.h dosyalarını include edersek x.h dosyası iki kez include edilmiş olur. Büyük projelerde bu olasılık çok fazladır. include koruması şöyle oluşturulur: /* x.h dosyası */ 280 #ifndef X_H_ #define X_H_ ... ... ... #endif Burada önişlemci x.h dosyasını ilk kez açtığında X_H_ sembolik sabiti henüz define edilmediği için #endif'e kadar kısmı yani bütün dosya içeriğini derleme modülüne verir. İkinci kez aynı dosyayı açtığında artık X_H_ sembolik sabiti define edilmiş olacağı için artık dosya içeriğini derleme modülüne vermez. Bu örnekteki X_H_ makro ismi dosya isminden hareketle uydurulmuş herhangi bir isimdir. Tüm include dosyaları böyle bir koruma ile oluşturulmalıdır (çünkü onların her zaman dolaylı olarak birden fazla kez include edilmesi mümkün olabilmektedir.) #error Komutu #error komutu önişlemci tarafından derlem işlemini fatal error ile sonlandırır. Komutun yanında bir yazı bulunur. Bu yazı ekrana hata mesajı olarak yazdırılır (Yazının iki tırnak içerisinde olması gerekmez.) Örneğin: #include #ifndef LINUX #error this program only compiles in Linux #endif int main(void) { return 0; } #pragma Komutu #pragma komutu standart bir önişlemci komutudur. #pragma anahtar sözcüğünü başka bir komut sözcüğü izler. Bu komut sözcüğü standart değildir. Yani #pragma komutu standarttır ancak onun yanındaki komut sözcüğü derleyiciden derleyiciye değişebilmektedir. Her derleyicinin pragma komutları diğerinden farklı olabilir. Prgama komutları belli bir derleyiciye özgü işlemlerin yaptırılması için kullanılır. Örneğin: #include #pragma pack(1) struct SAMPLE { char a; int b; char c; int d; }; #pragma pack() int main(void) { struct SAMPLE s; 281 printf("%u\n", sizeof(s)); return 0; } Burada pack pragma komutu her derleycide olmak zorunda değildir. Microsoft derleyicilerinde ve gcc derleyicilerinde bu komut hizalama sağlamak için kullanılabilir. Derleyicilerin pragma komutlarının listesi onların referans dokümanlarından öğrenilebilir. ## (Token Pasting) Önişlemci Operatörü Bu önişlemci komutu iki yazıyı birleştirmek için kullanılır. Örneğin: #define Name(x, y) x##y struct Name(CSD, Sample) { ... }; Önişlemci bu yapı bildirimini şöyle açar: struct CSDSample { ... }; Örneğin: #include #define Name(x) CSD##x int main(void) { int Name(x), Name(y); CSDx = 10; CSDy = 20; printf("%d, %d\n", CSDx, CSDy); return 0; } C'nin Önceden Tanımlanmış Sembolik Sabitleri ve Makroları C önişlemcisi bazı sembolik sabit ve makro isimlerini hiç define edilmediği halde define edilmiş gibi kabul etmektedir. Bunlara önceden tanımlanmış (predefined) sembolik sabitler ve makrolar denir. Bunların hepsinin başında ve sonun iki alt tire vardır. __FILE__: Önişlemci bu sembolik sabiti gördüğünde bunun yerine iki tırnak içerisinde bunun bulunduğu kaynak dosyanın ismini yerleştirir. Örneğin: #include #include int foo(void) { 282 return 0; } int main(void) { if (!foo()) { fprintf(stderr, "Error in file: %s\n", __FILE__); exit(EXIT_FAILURE); } return 0; } __LINE__: Önişlemci bu sembolik sabiti gördüğünde bu sembolik sabit hangi satıra yerleştirilmişse onun satır numarasını yazar (iki tırnak içerisinde değil). Örneğin: #include #include int foo(void) { return 0; } int main(void) { if (!foo()) { fprintf(stderr, "Error in file: %s\n in line %d\n", __FILE__, __LINE__); exit(EXIT_FAILURE); } return 0; } __DATE__ ve __TIME__: Önişlemci bu makrolar yerine iki tırnak içerisinde derleme işleminin yapıldığı tarihi ve zamanı yerleştirir. __STDC__: Bu makro eğer standart C derleyicisinde çalışılıyorsa define edilmiş kabul edilir, değilse define edilmemiş kabul edilir. Örneğin: #ifndef __STDC__ error this program cannot compile with non standard C compiler #endif Yukarıdaki önceden tanımlanmış sembolik sabitlerin dışında derleyicilerin kendilerine özgü önceden tanımlanmış başka sembolik sabitleri ve makroları da vardır. Örneğin _WIN32 ve _WIN64 sembolik sabitleri Microsoft C derleyicilerine özgüdür: #ifndef _WIN64 #error this program can only compile in Windows 64 platform #endif 283 284