Bilgisayar ve Programlamaya Giriş · Ders Notu
Programlamaya Giriş
Bir algoritma kâğıt üzerinde ne kadar açık yazılmış olursa olsun, işlemci onu doğrudan çalıştıramaz. İşlemcinin anladığı tek dil, sıfır ve birlerden oluşan makine dilidir. Programlama dili bu iki dünyanın arasında durur: insan onu okuyabilir, bir çeviri aracı da onu makine diline çevirebilir. Not, önce programlama dilinin ne olduğunu, kaynak kodun nasıl makine koduna çevrildiğini ve derleyici, yorumlayıcı ve sanal makinenin bu işi nasıl paylaştığını anlatır. Ardından dilleri seviye, tür, paradigma ve tipleme biçimine göre sınıflandırır. Son kesimler bir problemin programa dönüşmesini ve yazılım geliştirme aşamalarını anlatır. Değişken, karar, döngü, fonksiyon ve dizi gibi temel yapılar ile hata bulma yolları adım adım izleme tablolarıyla gösterilir. Kod örnekleri Java, Python ve C dillerindendir; Java ve Python örneklerinin çıktıları gerçek çalıştırmalardan alınmıştır.
Öğrenme çıktıları
- Kavramlar
- Programlama dilini, söz dizimini ve anlamı tanımlamak; kaynak kod, assembly ve makine dilini birbirinden ayırt etmek.
- Çalıştırma
- Derleme, yorumlama ve hibrit çalışmayı açıklamak; bytecode, sanal makine ve JIT kavramlarını yerine oturtmak; derleyicinin aşamalarını ve bağlayıcı ile yükleyicinin görevini sıralamak; bir hatanın derleme, çalışma zamanı ya da mantık hatası olduğunu belirlemek.
- Diller
- Dilleri seviye, tür ve paradigmaya göre sınıflandırmak; statik ve dinamik tiplemenin hatayı ne zaman yakaladığını açıklamak; sözde kodu Java ve Python'a çevirmek.
- Problem çözme ve yazılım süreci
- Bir problemi girdi, çıktı ve kısıtlarıyla tanımlayıp algoritmadan programa ulaşmak; yazılım geliştirme aşamalarını ve geliştirme araçlarını tanımak.
- Temel yapılar ve hata ayıklama
- Değişken, veri tipi, işlem önceliği, karar, döngü, fonksiyon ve diziyi kullanmak; bir programı izleme tablosuyla adım adım izleyerek hatasını bulmak.
4.1Temel kavramlar
Bir yemek tarifi, mutfakta ne yapılacağını adım adım anlatır. Tarifi okuyan kişi “bir tutam tuz” gibi belirsiz bir ifadeyi kendi deneyimiyle tamamlar. Bilgisayarın böyle bir deneyimi yoktur. Ona verilen her adımın tek bir anlamı olmalı ve bu anlam önceden kesin olarak tanımlanmış olmalıdır. Programlama dilleri bu ihtiyaçtan doğdu.
4.1.1Programlama dili nedir?
Gündelik dil esnektir. “Yarın görüşürüz” cümlesinin saati belli değildir; dinleyen kişi bağlama bakarak anlamı tamamlar. Bir makinenin bağlamdan anlam çıkarması beklenemez. Makineye iş yaptırmak için her ifadenin tek bir yorumu olan, kuralları önceden yazılmış bir dil gerekir. Böyle dillere biçimsel dil denir.
Programlama dili (programming language), insanların bilgisayara işlem adımlarını ifade etmesini sağlayan biçimsel bir dildir. Bu dille yazılmış komutlar topluluğuna program denir.
Programlama dili bir algoritmayı bilgisayarın yürütebileceği biçimde yazmanın aracıdır. Algoritma bir fikirdir: “a ile b'yi topla, sonucu yaz.” Bu fikir Java'da, Python'da ya da C'de yazılabilir. Yazılışlar birbirinden farklıdır ama hepsi aynı adımları anlatır ve aynı girdi için aynı çıktıyı üretir. Şekil 4.1 bunu a = 3 ve b = 5 için gösterir: üç dilde de ekrana 8 yazılır.
Şekildeki satırlar tam programların yalnızca toplama kısmıdır. Bir programın çalışabilmesi için bunların çevresinde başka satırlar da gerekir. Aşağıda aynı işin Java ve C'deki tam hâli görülüyor. Java programı JDK 21 ile derlenip çalıştırılmıştır; C programı için çıktı satır satır izlenerek belirlenmiştir.
a = 3 ve b = 5 değerlerini toplayıp yazan tam bir program Java ve C'de nasıl yazılır?
Java programı:
public class Topla {
public static void main(String[] args) {
int a = 3;
int b = 5;
System.out.println(a + b);
}
}C programı:
#include <stdio.h>
int main(void) {
int a = 3;
int b = 5;
printf("%d\n", a + b);
return 0;
}İki programda da çıktı aynıdır:
8Java'da her program bir class içinde yazılır ve çalışma main metodundan başlar. C'de çalışma main fonksiyonundan başlar; ekrana yazmak için stdio.h kütüphanesindeki printf kullanılır. printf'teki %d “buraya bir tam sayı gelecek” demektir, \n ise satır sonudur. Python'da aynı iş tek satırla yapılır: print(a + b). Ayrıntılar farklıdır, ama algoritmanın üç adımı (a'yı hazırla, b'yi hazırla, toplamı yaz) üç programda da aynı sırayla bulunur.
Aynı problemin farklı dillerle çözülebilmesi bir seçim sorusu doğurur: hangi dil? Cevap problemin ihtiyaçlarına ve programın çalışacağı ortama bağlıdır. Bir telefonda çalışacak uygulama, bir sunucuda çalışacak hesap ve bir çamaşır makinesinin denetim yazılımı farklı dilleri gerektirebilir.
Programlamayı bir dili ezberlemek sanmak. Program yazmanın asıl işi algoritmayı kurmaktır: problemi anlamak, adımları bulmak, adımların doğru sırada ve doğru koşullarla çalıştığından emin olmak. Dil bu adımları yazmanın aracıdır. İyi kurulmuş bir algoritma bir dilden ötekine birkaç saatte aktarılabilir; kötü kurulmuş bir algoritmayı ise hiçbir dil kurtaramaz.
4.1.2Söz dizimi ve anlam
Türkçede “Ali okula gitti” cümlesi doğru kurulmuştur. “Gitti okula Ali” cümlesi de anlaşılır ama kurallı değildir. “Okul Ali'ye gitti” ise kurallıdır ama anlamsızdır. Programlama dillerinde de bu iki boyut ayrı ayrı tanımlanır: bir yazılışın kurala uygun olup olmadığı ve kurala uygun bir yazılışın ne yaptığı.
Bir programlama dilinin söz dizimi (syntax), hangi yapıların hangi sırayla yazılabileceğini belirleyen kurallardır. Anlam (semantics) ise kurala uygun yazılmış her yapının çalıştırıldığında ne yaptığını belirler.
Java'da her deyimin noktalı virgülle bitmesi bir söz dizimi kuralıdır. int b = 5 satırının sonunda noktalı virgül yoksa yazılış kurala aykırıdır ve program çevrilemez. a + b ifadesinin a ile b'nin toplamını hesaplaması ise anlam kuralıdır. Python'da söz dizimi farklıdır: deyim sonunda noktalı virgül gerekmez, ama blokların girintisi kuralın parçasıdır.
Söz dizimi doğru olan her program istenen sonucu vermez. Üç notun ortalamasını hesaplarken toplamı 3 yerine 4'e bölen bir satır kurala tamamen uygundur. Çeviri aracı bu satırı sorunsuz kabul eder, program çalışır ve yanlış bir sayı yazar. Yazılış doğrudur ama yazılan şey, programcının istediği şey değildir. Böyle hatalara mantık hatası denir.
Aşağıdaki üç Java satırının her biri hangi açıdan sorunludur: söz dizimi mi, anlam mı?
int b = 5(satır sonunda noktalı virgül yok)double ort = (n1 + n2 + n3) / 4;(üç notun ortalaması isteniyor)System.out.println("Toplam: " + a + b);(a = 3, b = 5 için “Toplam: 8” isteniyor)
Birinci satır söz dizimi hatasıdır; derleyici “';' expected” iletisiyle durur. İkinci satır kurala uygundur ama yanlış bölene böler: anlamı, programcının niyetiyle uyuşmaz. Üçüncü satır da kurala uygundur, ama Java'da + soldan sağa işlenir: önce “Toplam: ” ile 3 birleşir, sonra 5 eklenir ve ekranda “Toplam: 35” görünür. İstenen sonuç için toplamın paranteze alınması gerekir: "Toplam: " + (a + b). Son iki satırdaki sorunu ancak dilin anlam kurallarını bilen biri görebilir.
4.1.3Kaynak kod, assembly ve makine dili
Programcının yazdığı metin bir metin dosyasında durur. Bu dosya Not Defteri ile açılıp okunabilir, değiştirilebilir. İşlemci ise bu metni okuyamaz. İşlemcinin anladığı komutlar ikili sayılarla kodlanmıştır ve her işlemci ailesinin kendi komut kümesi vardır.
Kaynak kod (source code), bir programın geliştiricinin okuyup değiştirebildiği metin hâlidir. Makine dili (machine language), işlemcinin doğrudan çalıştırdığı ikili komutlardır; bu komutlardan oluşan programa makine kodu (machine code) denir. Assembly (assembly language), makine komutlarının insanın okuyabileceği simgesel adlarla yazılmış karşılığıdır.
Şekil 4.2 tek bir satırın bu üç biçimden nasıl geçtiğini gösterir. Derleyici, kaynak koddaki toplam = a + b; satırını üç işlemci komutuna çevirir. İlk komut a'nın değerini bellekten işlemcinin bir yazmacına (R1) yükler. İkincisi b'yi R1'e ekler. Üçüncüsü R1'deki sonucu bellekte toplam'ın yerine yazar. Assembly bu komutları LOAD, ADD ve STORE gibi kısa adlarla yazar. Assembler adı verilen bir araç da bu adları ikili kodlara çevirir. Şekildeki ikili kodlar yalnızca bir örnektir; gerçek kodlama işlemciden işlemciye değişir.
Bu çeviriden iki sonuç çıkar. Birincisi, programlama dili ile işlemci dili aynı şey değildir. Java ya da Python bir işlemcinin dili değildir; bu dillerle yazılmış metnin işlemciye ulaşması için bir çeviri gerekir. Bu çeviriyi yapan programlara genel olarak çeviri aracı denir: derleyici ve yorumlayıcı. İkincisi, yüksek seviyeli bir satır çoğu zaman birden fazla makine komutuna karşılık gelir. Programcı “topla ve sakla” der; yükleme, toplama ve yazma adımlarını çeviri aracı ayrıntılandırır.
İşlemcinin Java ya da Python kaynak kodunu “anladığını” düşünmek. İşlemci yalnızca kendi makine dilini çalıştırır. .java ya da .py dosyasını çift tıklayıp çalıştırmaya çalışmak bu yüzden sonuç vermez. Dosyanın önce bir derleyiciden ya da yorumlayıcıdan geçmesi gerekir. Java'da bu iş iki parçaya bölünür: javac kaynak kodu bytecode'a çevirir, JVM de bytecode'u çalıştırır.
4.2Programın çalıştırılması
Kaynak kod yazıldıktan sonra ekranda bir sonuç görünene kadar birkaç iş yapılır. Metin bir çeviri aracından geçer, çevrilen kod işlemcide ya da bir sanal makinede çalışır, çalışan program girdisini okuyup çıktısını üretir. Bu yolun her durağında farklı türden bir hata ortaya çıkabilir.
4.2.1Kaynak koddan çıktıya
Şekil 4.3 bir Java programının yolunu beş durakta gösterir. Programcı kaynak kodu Topla.java adlı bir metin dosyasına yazar. Bilgisayar bu metni olduğu gibi çalıştıramaz. Çeviri durağında derleyici metni okur ve dilin kurallarına uyup uymadığını denetler. Hata yoksa bir çıktı dosyası üretir. C gibi dillerde bu dosya doğrudan makine kodudur. Java'da ise Topla.class adlı bir ara kod dosyasıdır. Çalıştırma durağında komutlar işlemcide yürütülür; Java'da bu iş Java Sanal Makinesi (JVM, Java Virtual Machine) üzerinden yapılır. Son durakta program çıktısını üretir: a = 3 ve b = 5 için 8.
Java'da bu yol komut satırında iki komutla yürünür. javac Topla.java derleyiciyi çalıştırır ve Topla.class dosyasını üretir. java Topla ise JVM'yi başlatır ve bu dosyadaki programı çalıştırır. İki adımın ayrı olması önemlidir: derleme bir kez yapılır, üretilen dosya istendiği kadar çalıştırılabilir. Kaynak kodda bir değişiklik yapılırsa derlemenin yinelenmesi gerekir; yoksa JVM eski .class dosyasını çalıştırmaya devam eder.
Kaynak kodu düzelttikten sonra yeniden derlemeden java Topla komutunu çalıştırıp “düzeltme işe yaramadı” sonucuna varmak. JVM kaynak kodu değil, derlenmiş .class dosyasını çalıştırır. Kaynak kod her değiştiğinde javac yeniden çalıştırılmalıdır. IDE'ler bu adımı kaydetme ya da çalıştırma düğmesine basma anında kendiliğinden yapar; bu yüzden adım çoğu zaman gözden kaçar.
4.2.2Hata türleri
Hat üzerindeki her durak, kendine özgü bir hata türünü ortaya çıkarır. Bir hatanın hangi durakta göründüğünü bilmek, onu nerede arayacağımızı da söyler.
Derleme hatası (compile error), kaynak kodun dilin kurallarına uymamasıdır; çeviri sırasında bulunur ve program hiç çalışmaz. Söz dizimi hatası (syntax error) ve tip hatası bu türdendir. Çalışma zamanı hatası (runtime error), program çalışırken ortaya çıkan ve programı durduran hatadır. Mantık hatası (logic error), programın hata iletisi vermeden çalışıp yanlış sonuç üretmesidir.
Üç türü birer küçük Java programıyla görmek kolaydır. İlk programda ikinci atama satırının sonundaki noktalı virgül unutulmuştur:
public class NoktaliVirgul {
public static void main(String[] args) {
int a = 3;
int b = 5
System.out.println(a + b);
}
}Derleyici programı çevirmeyi reddeder ve hatanın yerini satır numarasıyla bildirir:
NoktaliVirgul.java:4: error: ';' expected
int b = 5
^
1 errorİleti dosya adını, satır numarasını (4) ve beklenen işareti (;) söyler. Alttaki ^ işareti, derleyicinin noktalı virgülü nerede beklediğini gösterir. Bu durumda .class dosyası üretilmez ve programın hiçbir satırı çalışmaz; ekranda “8” görünmez. Şekil 4.4 bu durumu hattın çeviri durağındaki kırmızı işaretle gösterir.
İkinci programda söz dizimi kusursuzdur ama üçüncü satırda sıfıra bölme vardır:
public class SifiraBolme {
public static void main(String[] args) {
System.out.println("Merhaba");
int x = 10;
System.out.println(x / 0);
System.out.println("Son");
}
}Derleme başarıyla biter. Program çalışınca önce “Merhaba” yazılır, sonra x / 0 hesaplanmaya çalışılır ve JVM programı durdurur:
Merhaba
Exception in thread "main" java.lang.ArithmeticException: / by zero
at SifiraBolme.main(SifiraBolme.java:5)Çıktıdaki ArithmeticException: / by zero sıfıra bölme hatasını, SifiraBolme.java:5 de hatanın kaynak dosyadaki satırını gösterir. “Son” satırı hiç çalışmaz. Derleyici bu hatayı yakalayamaz, çünkü x'in değeri ve bölme işlemi ancak çalışma sırasında hesaplanır. Bölen bir kullanıcı girdisi olsaydı, sıfır olup olmayacağını derleme sırasında bilmek zaten imkânsız olurdu.
Üçüncü program üç notun ortalamasını hesaplar ama bir parantez eksiktir:
public class OrtalamaHata {
public static void main(String[] args) {
int n1 = 70, n2 = 85, n3 = 90;
System.out.println(n1 + n2 + n3 / 3);
System.out.println((n1 + n2 + n3) / 3);
System.out.println((n1 + n2 + n3) / 3.0);
}
}185
81
81.66666666666667Program sorunsuz derlenir, hiçbir hata iletisi vermeden çalışır ve ilk satırda 185 yazar. Beklenen sonuç 81 ile 82 arasında bir sayıdır. Bölme toplamadan önce yapıldığı için yalnızca n3 üçe bölünmüştür: 70 + 85 + 90 / 3 = 70 + 85 + 30 = 185. Bu bir mantık hatasıdır. İkinci satır parantezi ekler ama yine tam doğru değildir. Java'da iki tam sayının bölümü tam sayıdır; 245 / 3 işleminin kesirli kısmı atılır ve sonuç 81 olur. Üçüncü satır 3.0 ile böler; işlem ondalıklı yapılır ve doğru değer, yaklaşık 81,67 çıkar. Mantık hatalarının ortak özelliği budur: program “çalışır”, sorunu ancak beklenen sonuçla gerçek sonucu karşılaştıran biri fark eder.
| Hata türü | Ne zaman ortaya çıkar? | Kim bildirir? | Örnek |
|---|---|---|---|
| Derleme (söz dizimi, tip) | Çeviri sırasında; program hiç çalışmaz | Derleyici, satır numarasıyla | Unutulan ;, metni int'e atamak |
| Çalışma zamanı | Yürütme sırasında, hatalı satıra gelince | JVM ya da işletim sistemi; program durur | Sıfıra bölme, dizinin dışına çıkmak |
| Mantık | Hiçbir zaman kendiliğinden görünmez | Kimse; ancak test ile bulunur | Eksik parantez, yanlış bölen |
Aşağıdaki durumların her biri hangi hata türüdür? (a) Java'da System.out.println("Merhaba") satırının sonunda noktalı virgül yok. (b) Kullanıcıdan alınan öğrenci sayısı 0 girildiğinde ortalama hesaplanırken program duruyor. (c) Bir sınıfın ortalaması hesaplanırken toplam, öğrenci sayısı n yerine n + 1'e bölünüyor.
- (a) Derleme hatasıdır. Noktalı virgül bir söz dizimi kuralıdır; derleyici satır numarasıyla bildirir ve program üretilmez.
- (b) Çalışma zamanı hatasıdır. Kod kurala uygundur ve derlenir; ancak bölen 0 olduğu anda yürütme durur. Aynı program sıfırdan farklı girdilerle sorunsuz çalışır. Bu yüzden hata yalnızca belirli girdilerle ortaya çıkar.
- (c) Mantık hatasıdır. Program her girdiyle çalışır ve bir sayı yazar; yazdığı sayı her zaman olması gerekenden küçüktür. Örneğin 60, 70 ve 80 notları için 210 / 4 = 52,5 yazılır; doğrusu 210 / 3 = 70'tir.
Programın hata iletisi vermeden çalışmasını doğru çalıştığının kanıtı saymak. Derleyici yalnızca kurallara uyulup uyulmadığını denetler; programın istenen işi yapıp yapmadığını bilemez. Her programın, sonucu elle hesaplanmış birkaç girdiyle denenmesi gerekir.
4.2.3Derlenen, yorumlanan ve hibrit diller
Kaynak kodu çalıştırmanın üç yaygın yolu vardır. Fark, çevirinin ne zaman ve ne kadarının yapıldığındadır.
Derleyici (compiler), programın tamamını çalıştırmadan önce hedef koda çeviren programdır; bu işe derleme (compilation) denir. Yorumlayıcı (interpreter), programı önceden bir çalıştırılabilir dosyaya çevirmeden, çalışma sırasında komut komut değerlendirerek yürüten programdır; bu işe yorumlama (interpretation) denir. Hibrit çalışmada kaynak kod önce bir ara koda derlenir, ara kod da bir sanal makinede yürütülür.
| Yaklaşım | İşleyiş | Örnek diller |
|---|---|---|
| Derlenen | Program önceden hedef makine koduna çevrilir | C, C++, Rust |
| Yorumlanan | Komutlar çalışma sırasında değerlendirilir | Python, JavaScript |
| Hibrit | Ara kod sanal makinede yürütülür | Java, C# |
Şekil 4.5 üç yolu yan yana gösterir. Derlenen yolda kaynak.c dosyası derleyiciden geçer ve program.exe gibi bir çalıştırılabilir dosya çıkar. Bu dosya işlemcide doğrudan çalışır; çalıştırma anında derleyiciye artık gerek yoktur. Yorumlanan yolda kaynak.py dosyası her çalıştırmada yorumlayıcıya verilir. Yorumlayıcı komutları sırayla işler ve işlemciye yaptırır. Ortada kalıcı bir makine kodu dosyası yoktur. Hibrit yolda Topla.java önce javac ile Topla.class dosyasına derlenir. Bu dosyayı JVM çalıştırır, JVM'nin kendisi de işlemcide çalışan bir programdır.
Hangi yolun seçildiği programın nasıl dağıtılacağını da belirler. Derlenmiş bir C programı yalnızca derlendiği işlemci ve işletim sistemi için geçerlidir. Windows için derlenmiş bir .exe dosyası Linux'ta ya da farklı işlemcili bir bilgisayarda çalışmaz. Yorumlanan bir Python programı kaynak kod olarak dağıtılır; Python yorumlayıcısı kurulu her bilgisayarda çalışır. Java'nın .class dosyası ise işlemciden bağımsızdır: aynı dosya, JVM kurulu her platformda çalışır. Java'nın bilinen “bir kez yaz, her yerde çalıştır” sloganı bu özellikten gelir.
Java'yı “derlenen bir dil” diye C ile aynı gruba koymak. Java kaynak kodu derlenir, ama derleyicinin ürettiği şey makine kodu değil, JVM'nin anladığı bytecode'dur. Bu kod işlemcide doğrudan çalışmaz, çalıştırmak için JVM gerekir. Java'yı hibrit sınıfa koymak bu iki aşamalı yapıyı doğru anlatır.
4.2.4Derleyici ve yorumlayıcı adım adım
Derleyici ile yorumlayıcının farkı en iyi aynı küçük program üzerinde görülür. Aşağıdaki üç satırlık sözde kodun üçüncü satırında sıfıra bölme vardır:
| Satır | Sözde kod |
|---|---|
| 1 | yaz "Merhaba" |
| 2 | x ← 10 |
| 3 | yaz x / 0 |
Sözde kodda atama ← ile gösterilir: x ← 10, “x'e 10 değerini ver” demektir. Böylece atama, eşitlik karşılaştırmasıyla karışmaz. Program önce bir derleyiciyle, sonra satır satır çalışan basit bir yorumlayıcıyla işlensin. Her adımda olanlar Tablo 4.3 içinde sıralanmıştır.
| Adım | Derleyici | Basit yorumlayıcı |
|---|---|---|
| 1 | Satır 1, 2 ve 3 baştan sona okunur ve çevrilir; hiçbir satır çalışmadı | Satır 1 çevrilir ve hemen çalışır: Merhaba |
| 2 | Bölme kurala uygundur; çeviri başarılı, çalıştırılabilir dosya üretilir | Satır 2 çevrilir ve çalışır: x = 10; satır 3 henüz okunmadı |
| 3 | Çalıştırma başlar; satır 1: Merhaba | Satır 3 çevrilir, çalışırken hata: sıfıra bölme |
| 4 | Satır 2: x = 10; satır 3: çalışma zamanı hatası | — |
| Ekran | Merhaba, ardından hata | Merhaba, ardından hata |
Bu programda iki yol aynı ekranla biter: “Merhaba” ve ardından hata. Fark, hatanın türünde değil, işin sırasındadır. Derleyici önce bütün metni çevirir, çalıştırma ayrı bir aşama olarak sonra gelir. Yorumlayıcı çevirme ile çalıştırmayı satır satır iç içe yürütür. Sıfıra bölme çalışma zamanı hatası olduğu için iki yolda da ancak üçüncü satır çalışırken görünür. Şekil 4.6 iki yolun son durumunu gösterir.
Hata bir söz dizimi hatası olduğunda iki yol birbirinden ayrılır. Üçüncü satırda bir parantez kapanmamış olsun: yaz (x + 2. Derleyici çeviri sırasında bu satıra takılır ve hiçbir çalıştırılabilir dosya üretmez. Ekranda yalnızca “satır 3: söz dizimi hatası” iletisi görünür; “Merhaba” bile yazılmaz. Satır satır çalışan basit bir yorumlayıcı ise satır 1'i çalıştırıp “Merhaba” yazar, satır 2'de x'e 10 atar ve ancak satır 3'e gelince hatayı fark eder.
Python ve JavaScript gibi bugünkü yorumlayıcılar bu basit modelden biraz ayrılır: çalıştırmaya başlamadan önce dosyanın tamamını ayrıştırırlar. Bu yüzden söz dizimi hatasını en başta bildirirler. Aşağıdaki Python dosyası bunu gösterir:
print("Merhaba")
x = 10
print((x + 2)
print("Son") File "sozdizimi.py", line 3
print((x + 2)
^
SyntaxError: '(' was never closedEkranda “Merhaba” yoktur. Python, birinci satırı çalıştırmadan önce dosyanın tamamını okumuş ve üçüncü satırdaki kapanmamış parantezi bulmuştur. Aynı dosyada söz dizimi hatası yerine sıfıra bölme olduğunda durum değişir:
print("Merhaba")
x = 10
print(x / 0)
print("Son")Merhaba
Traceback (most recent call last):
File "sifira.py", line 3, in <module>
print(x / 0)
~~^~~
ZeroDivisionError: division by zeroBu kez “Merhaba” yazılmıştır, çünkü sıfıra bölme ancak üçüncü satır çalışırken anlaşılır. Python'un iletisi Java'nınkine benzer: dosya adı, satır numarası, hata türü (ZeroDivisionError) ve kısa açıklama.
Dört satırlık bir programın 2. satırında söz dizimi hatası, 4. satırında sıfıra bölme var. Program (a) derleyiciyle, (b) satır satır çalışan basit bir yorumlayıcıyla, (c) Python ile çalıştırılırsa ekranda ne görünür? Satır 1 “Başla”, satır 3 “Orta” yazıyor.
- (a) Derleyici önce dört satırın hepsini çevirir. Satır 2'deki söz dizimi hatasında durur ve çalıştırılabilir dosya üretmez. Ekranda yalnızca satır 2 için bir derleme hatası iletisi görünür; “Başla” yazılmaz.
- (b) Basit yorumlayıcı satır 1'i çevirip çalıştırır: “Başla”. Satır 2'yi çevirmeye çalışınca söz dizimi hatasıyla durur. Satır 3 ve 4'e hiç ulaşılmaz. Ekranda “Başla” ve hata iletisi görünür.
- (c) Python dosyanın tamamını önce ayrıştırır ve satır 2'deki hatayı bulur. Ekranda yalnızca
SyntaxErroriletisi görünür; davranış (a) ile aynıdır. Satır 4'teki sıfıra bölme, üç durumda da hiç ortaya çıkmaz; çünkü programın oraya kadar çalışması gerekirdi.
4.2.5Bytecode, sanal makine ve JIT
Python'a “yorumlanan dil” denir, ama Python yorumlayıcısının içinde de bir derleme adımı vardır. Python'un en yaygın uygulaması olan CPython, kaynak kodu önce bytecode adı verilen bir ara koda derler. Sonra bu bytecode'u kendi içindeki bir sanal makinede komut komut yürütür. Ortaya bir makine kodu dosyası çıkmaz. Derleme adımı programcıdan gizlidir: python dosya.py komutu iki aşamayı art arda yapar.
Bytecode (bytecode, ara kod), belirli bir işlemciye değil, bir sanal makineye göre tasarlanmış basit komutlardan oluşan ara koddur. Sanal makine (virtual machine), bu komutları okuyup yürüten yazılımdır; gerçek bir işlemcinin işini yazılımla taklit eder.
Python'un ürettiği bytecode, dis modülüyle görülebilir. Aşağıdaki program alan = en * boy + 2 satırını derler ve bytecode'unu yazdırır:
import dis
dis.dis(compile("alan = en * boy + 2", "<kod>", "exec")) 0 RESUME 0
1 LOAD_NAME 0 (en)
LOAD_NAME 1 (boy)
BINARY_OP 5 (*)
LOAD_CONST 0 (2)
BINARY_OP 0 (+)
STORE_NAME 2 (alan)
RETURN_CONST 1 (None)Çıktıdaki her satır bir sanal makine komutudur. LOAD_NAME bir değişkenin değerini sanal makinenin yığınına koyar. BINARY_OP 5 (*) yığının üstündeki iki değeri çarpar, LOAD_CONST 2 sabitini yığına ekler, BINARY_OP 0 (+) toplar. STORE_NAME de sonucu alan adına bağlar. Komut adları Python sürümüne göre biraz değişebilir; burada Python 3.13 kullanılmıştır. Sıralama ise her sürümde aynıdır: önce en ile boy çarpılır, sonra 2 eklenir. Bu sıra işlem önceliğinden gelir.
Bytecode'u komut komut yürütmek, makine kodunu doğrudan çalıştırmaktan yavaştır. Her komut için sanal makinenin “bu komut ne demek?” sorusunu yanıtlaması gerekir. Bu yavaşlığı azaltmanın yolu JIT derlemedir.
JIT derleyici (just-in-time compiler, tam zamanında derleyici), program çalışırken sık çalışan kod parçalarını makine koduna çeviren derleyicidir. Çevrilen parça sonraki çalışmalarda yorumlanmaz, doğrudan işlemcide çalışır.
Şekil 4.7 bu fikri JavaScript üzerinden gösterir. Chrome tarayıcısındaki V8 motoru kodu önce yorumlar. Bir fonksiyonun binlerce kez çağrıldığını görünce onu makine koduna derler. JVM de aynı yolu izler: bytecode önce yorumlanır, sık çalışan metotlar JIT ile makine koduna çevrilir. Bu yüzden uzun süre çalışan Java programları, derlenmiş C programlarına yakın hızlara ulaşabilir.
Bu örnekler önemli bir noktayı ortaya koyar: “derlenen” ya da “yorumlanan” bir dilin değil, o dilin uygulamasının özelliğidir. Python için makine koduna derleyen uygulamalar da yazılmıştır, C için yorumlayıcılar da. Gerçek sistemlerin çoğu iki yöntemi birleştirir. Bir dile “yorumlanan” demek, o dilin en yaygın uygulamasının nasıl çalıştığını söylemenin kısa yoludur.
“Python yorumlanan bir dil olduğu için hiçbir derleme yapılmaz” demek. CPython her çalıştırmada kaynak kodu bytecode'a derler. İçe aktarılan modüllerin bytecode'unu __pycache__ klasöründe .pyc dosyası olarak saklar; kaynak değişmediyse sonraki çalıştırmalarda bu dosyayı kullanır. Python'u yorumlanan yapan, bytecode'un makine koduna değil, bir sanal makinede komut komut yürütülmesidir.
4.2.6Derleyicinin aşamaları
Derleyici tek bir büyük adımda çalışmaz. İşini birbirini izleyen beş aşamaya böler; her aşamanın çıktısı bir sonrakinin girdisidir. Aşamaları tek bir satır üzerinde izlemek, derleyicinin hangi hatayı nerede yakaladığını da açıklar. İzlenecek satır şudur:
alan = en * boy + 2;1. Sözcük çözümleme (lexical analysis). Derleyici satırı bir karakter dizisi olarak görür: a, l, a, n, boşluk, = … İlk aşama bu karakterleri anlamlı parçalara ayırır. Bu parçalara belirteç (token) denir. Boşluklar ve açıklamalar bu aşamada atılır. Örnek satırdan sekiz belirteç çıkar ve her birinin türü bellidir (Şekil 4.8).
alan = en * boy + 2; satırı sekiz belirtece ayrılır: üç tanımlayıcı, üç operatör, bir sabit değer ve bir ayırıcı.Bu aşamada yakalanan hatalar, dilde hiçbir belirtece uymayan karakter dizileridir. Java'da # karakteri bir operatör değildir. Kapanmamış bir dizgi de belirtece dönüştürülemez. JDK 21'in derleyicisi bu iki durumu şöyle bildirir:
GecersizKarakter.java:4: error: illegal character: '#'
int alan = en # boy;
^
GecersizKarakter.java:4: error: not a statement
int alan = en # boy;
^
2 errorsKapanmamisDizgi.java:3: error: unclosed string literal
System.out.println("Merhaba);
^
1 error2. Söz dizimi çözümleme (parsing, syntax analysis). Belirteçler dilin kurallarına göre bir ağaca dizilir; bu ağaca söz dizimi ağacı (syntax tree) denir. Şekil 4.9 örnek satırın ağacını gösterir. Kökte atama (=) vardır; sol kolu alan, sağ kolu en * boy + 2 ifadesidir. Çarpma ağacın daha derinindedir, bu yüzden önce hesaplanır. İşlem önceliği bu ağacın biçimine böylece yerleşir.
Eksik noktalı virgül, kapanmamış parantez ve yanlış sırada yazılmış belirteçler bu aşamada yakalanır. Örneğin int 2x = 5; satırında sözcük çözümleme bir sorun görmez: 2 bir sayı, x bir tanımlayıcıdır. Ama int belirtecinden sonra bir sayının gelmesi kurala aykırıdır. Bu yüzden derleyici hatayı söz dizimi aşamasında bildirir:
GecersizSozcuk.java:3: error: not a statement
int 2x = 5;
^
GecersizSozcuk.java:3: error: ';' expected
int 2x = 5;
^
2 errors3. Anlam çözümleme (semantic analysis). Söz dizimi doğru olan bir ağacın anlamlı olup olmadığı denetlenir. Kullanılan her adın daha önce bildirilmiş olması gerekir. İşlenenlerin tipleri de birbirine uymalıdır. Örnek satırda en ve boy int olarak bildirilmiştir; int × int + int ifadesi int verir ve int tipindeki alan'a atanabilir (Şekil 4.10).
boy değişkeni bildirilmeden kullanılırsa javac şu iletiyi verir:
public class Tanimsiz {
public static void main(String[] args) {
int en = 3;
int alan = en * boy + 2;
}
}Tanimsiz.java:4: error: cannot find symbol
int alan = en * boy + 2;
^
symbol: variable boy
location: class Tanimsiz
1 errorİleti, bulunamayan simgenin bir değişken olduğunu ve adının boy olduğunu söyler. Tip uyuşmazlığı da bu aşamanın işidir. int tipindeki bir değişkene metin atamak söz dizimi açısından kusursuzdur, ama anlam açısından geçersizdir:
public class TipHatasi {
public static void main(String[] args) {
int x = 5;
x = "beş";
}
}TipHatasi.java:4: error: incompatible types: String cannot be converted to int
x = "beş";
^
1 errorİleti “String tipi int'e dönüştürülemez” demektir. Derleyici hatayı, program hiç çalışmadan, 4. satırdaki "beş" değerinin altında gösterir.
4. Ara kod üretimi ve iyileştirme (intermediate code, code optimization). Denetimden geçen ağaç, her biri tek bir işlem yapan basit komutlara dönüştürülür. Bu biçime üç adresli kod denir, çünkü her komutta en fazla üç ad bulunur:
t1 = en * boy
t2 = t1 + 2
alan = t2Burada t1 ve t2 derleyicinin uydurduğu geçici adlardır. Bu biçim üzerinde kod iyileştirme yapılır: gereksiz işlemler atılır, sonucu derleme sırasında bilinen işlemler önceden hesaplanır. en = 3 ve boy = 4 değişmeyen sabitlerse, 3 × 4 + 2 = 14 hesabı program çalışmadan yapılır ve kodda yalnızca alan = 14 kalır. Bu iyileştirmeye sabit katlama (constant folding) denir (Şekil 4.11).
5. Kod üretimi (code generation). Son aşamada hedef işlemcinin komutları seçilir ve ikili koda çevrilir. Örnek satır için bir işlemcide LOAD, MUL, ADD ve STORE komutları üretilir (Şekil 4.12). Java derleyicisinin hedefi gerçek bir işlemci değil, JVM'dir. Bu yüzden javac makine komutları yerine bytecode üretir.
javac'ın ürettiği bytecode, JDK ile gelen javap -c komutuyla okunabilir. Aşağıdaki iki program arasındaki tek fark, ikincisinde en ve boy'un final (değişmez) olarak bildirilmesidir. İki programın bytecode'u nasıl ayrılır?
public class AlanIfade {
public static void main(String[] args) {
int en = 3;
int boy = 4;
int alan = en * boy + 2;
System.out.println(alan);
}
}public static void main(java.lang.String[]);
Code:
0: iconst_3
1: istore_1
2: iconst_4
3: istore_2
4: iload_1
5: iload_2
6: imul
7: iconst_2
8: iadd
9: istore_3
10: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
13: iload_3
14: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
17: return
}İlk programın bytecode'unda iconst_3 ve istore_1 3 değerini 1 numaralı yerel değişkene (en) yazar. iconst_4 ve istore_2 4'ü boy'a yazar. Sonraki beş komut (iload_1, iload_2, imul, iconst_2, iadd) ifadeyi hesaplar, istore_3 de sonucu alan'a yazar. Bu sıra, sunumdaki “iload iload imul iconst_2 iadd istore” dizisinin aynısıdır.
public class SabitKatlama {
public static void main(String[] args) {
final int en = 3;
final int boy = 4;
int alan = en * boy + 2;
System.out.println(alan);
}
}public static void main(java.lang.String[]);
Code:
0: bipush 14
2: istore_1
3: getstatic #7 // Field java/lang/System.out:Ljava/io/PrintStream;
6: iload_1
7: invokevirtual #13 // Method java/io/PrintStream.println:(I)V
10: return
}İkinci programda çarpma ve toplama komutları hiç yoktur. bipush 14 komutu 14 değerini doğrudan yığına koyar. en ve boy final olduğu için derleyici değerlerinin değişmeyeceğini bilir ve 3 × 4 + 2 işlemini derleme sırasında yapar. Bu, sabit katlamanın gerçek bir örneğidir. İki program da çalışınca 14 yazar.
İlk üç aşama hata arar ve bulduğu hatayı satır numarasıyla bildirir. Son iki aşama çalışacak kodu üretir. Sıfıra bölme gibi değere bağlı hatalar hiçbir aşamada görünmez, çünkü derleyici programı çalıştırmaz; yalnızca metnine bakar.
| Aşama | Girdi → çıktı | Yakalanan hata (javac iletisi) |
|---|---|---|
| Sözcük çözümleme | karakterler → belirteçler | Geçersiz karakter (illegal character), kapanmamış dizgi (unclosed string literal) |
| Söz dizimi çözümleme | belirteçler → söz dizimi ağacı | Eksik ; (';' expected), kapanmamış parantez |
| Anlam çözümleme | ağaç → denetlenmiş ağaç | Tanımsız ad (cannot find symbol), tip uyuşmazlığı (incompatible types) |
| Ara kod ve iyileştirme | ağaç → basit komutlar | — |
| Kod üretimi | basit komutlar → makine kodu ya da bytecode | — |
Derleyicinin bildirdiği satır numarasını her zaman hatanın kendisi sanmak. Derleyici hatayı fark ettiği yeri bildirir; bu yer bazen asıl hatanın bir satır sonrasıdır. Bir satırın sonundaki noktalı virgül unutulduğunda bazı derleyiciler hatayı bir sonraki satırın başında görür. İleti bir satırı gösteriyorsa, bir önceki satıra da bakmak gerekir.
4.2.7Bağlayıcı ve yükleyici
Gerçek programlar tek bir dosyadan oluşmaz. Bir hesap makinesi programında kullanıcı arayüzü bir dosyada, hesap fonksiyonları başka bir dosyada durabilir. Programın ekrana yazması, dosya açması ya da karekök alması için ayrıca hazır kütüphane kodu kullanılır. Derleyici her kaynak dosyayı ayrı ayrı çevirir. Bu parçaların tek bir programda birleşmesi ve çalışmak üzere belleğe alınması iki ayrı aracın işidir.
Nesne dosyası (object file), tek bir kaynak dosyanın derlenmesiyle oluşan, tek başına çalışmayan makine kodu dosyasıdır (.o ya da .obj). Bağlayıcı (linker), nesne dosyalarını ve kullanılan kütüphane kodunu tek bir çalıştırılabilir dosyada (executable file) birleştiren programdır. Yükleyici (loader), işletim sisteminin çalıştırılabilir dosyayı belleğe alıp çalıştırmayı başlatan parçasıdır.
Şekil 4.13 bir C programının yolunu gösterir. main.c ana programı, hesap.c hesap fonksiyonlarını içerir. Derleyici bunları main.o ve hesap.o nesne dosyalarına çevirir. main.o içinde hesap.c'deki bir fonksiyona yapılan çağrı vardır, ama bu fonksiyonun adresi henüz bilinmez. Derleyici yalnızca “burada adı şu olan bir fonksiyon çağrılıyor” notunu bırakır. Bağlayıcı bu notları çözer. Çağrılan fonksiyonu hesap.o'da, printf gibi hazır fonksiyonları kütüphanede bulur. Her çağrıyı doğru adrese bağlar ve sonunda program.exe dosyasını yazar.
Çağrılan bir fonksiyon hiçbir parçada bulunamazsa bağlayıcı bağlama hatası (linking error) verir. Bu hata derleme hatasından farklıdır. Her dosya tek başına kusursuz derlenmiş olabilir; eksik olan, parçalar arasındaki bağdır. Örneğin main.c'de çağrılan ortalama fonksiyonu hesap.c'de ortalma diye yanlış yazılmışsa iki dosya ayrı ayrı derlenir, ama bağlayıcı ortalama adını hiçbir yerde bulamaz.
Program çift tıklandığında ya da komut satırından çağrıldığında yükleyici devreye girer. Dosyayı diskten belleğe kopyalar. Programın ihtiyaç duyduğu paylaşılan kütüphaneleri de yükleyip bağlar: Windows'ta .dll, Linux'ta .so dosyaları. Son olarak işlemciyi programın ilk komutuna yönlendirir. Paylaşılan bir kütüphaneyi birçok program ortak kullanır; aynı kodun her programa ayrı ayrı kopyalanmasına gerek kalmaz.
Java'da ayrı bir bağlama adımı yoktur. Her sınıf kendi .class dosyasına derlenir. JVM'nin sınıf yükleyicisi (class loader), program çalışırken bir sınıfa ilk kez ihtiyaç duyulduğu anda o sınıfın dosyasını bulup yükler. Bir sınıf dosyası eksikse hata derleme ya da bağlama sırasında değil, çalışma sırasında ortaya çıkar.
Bir nesne dosyasını (.o, .obj) çalıştırılabilir program sanmak. Nesne dosyasında makine kodu vardır ama başka dosyalara ve kütüphanelere yapılan çağrılar henüz bağlanmamıştır. Çalışabilmesi için bağlayıcıdan geçmesi gerekir.
4.2.8Derleme mi, yorumlama mı?
Derleme ile yorumlama arasındaki seçim bir değiş tokuştur. Bir yanda hız ve erken hata denetimi, öbür yanda hızlı deneme ve esneklik vardır. Tablo 4.5 üç yaklaşımı beş ölçüte göre karşılaştırır.
| Ölçüt | Derlenen (C) | Yorumlanan (Python) | Hibrit (Java) |
|---|---|---|---|
| Çalışma hızı | Genellikle en hızlı: doğrudan makine kodu | Daha yavaş: komutlar çalışırken çözülür | JIT sayesinde derlenene yakın |
| Hata bildirimi | Söz dizimi ve tip hataları çalıştırmadan önce | Tip hataları çoğunlukla ancak o satır çalışınca | Çoğu derlemede, kalanı çalışırken |
| Dağıtım | Her işletim sistemi ve işlemci için ayrı dosya | Kaynak kod + hedefte kurulu yorumlayıcı | Tek .class / .jar + her platformda JVM |
| Geliştirme döngüsü | Her değişiklikten sonra yeniden derleme | Kaydet ve hemen çalıştır | Derle, sonra çalıştır |
| Başlangıç | Hemen başlar, ek yazılım gerekmez | Önce yorumlayıcı yüklenir | Önce JVM başlatılır |
Bir oyun motoru ya da bir video kodlayıcı her milisaniyeyi önemser; derlenmiş C ya da C++ bu yüzden tercih edilir. Bir veri analisti bir veri kümesine farklı sorular sorarken her denemeden sonra derleme beklemek istemez; Python'un “kaydet ve çalıştır” döngüsü ona uygundur. Bir bankanın sunucu yazılımı yıllarca çalışır ve farklı donanımlara taşınır. Java'nın taşınabilir bytecode'u ve JIT ile kazandığı hız bu ihtiyaca uyar. Aynı dil için hem derleyici hem yorumlayıcı yazılabilmesi de bu değiş tokuşun sonucudur.
Bir öğrenci aynı programı üç farklı bilgisayarda çalıştırmak istiyor: Windows dizüstü, Linux sunucu ve macOS masaüstü. Program C, Python ve Java ile yazılmış olsaydı her durumda ne dağıtması gerekirdi?
- C: Her platform için ayrı ayrı derlenmiş üç çalıştırılabilir dosya. Windows'ta üretilen
.exeLinux'ta çalışmaz. Kaynak kod da dağıtılabilir; o zaman her bilgisayarda bir C derleyicisi bulunmalıdır. - Python: Kaynak kod dosyası (
.py). Üç bilgisayarda da Python yorumlayıcısının kurulu olması gerekir. - Java: Tek bir
.classya da.jardosyası. Üç bilgisayarda da JVM bulunmalıdır; aynı dosya üçünde de değişmeden çalışır.
4.3Programlama dilleri
Bugün kullanılan programlama dillerinin sayısı yüzlerle ölçülür. Hepsini tek tek tanımak ne mümkündür ne de gereklidir. Dilleri birkaç ölçüte göre gruplamak, yeni bir dille karşılaşıldığında onu yerine oturtmayı kolaylaştırır. Donanıma yakınlık, ortaya çıkış dönemi, kullanım amacı, düşünme biçimi ve tip denetiminin zamanı bu ölçütlerin başlıcalarıdır.
4.3.1Dillerin seviyesi
Düşük seviyeli dil (low-level language), komutları işlemcinin komutlarına bire bir ya da bire yakın karşılık gelen dildir: makine dili ve assembly. Yüksek seviyeli dil (high-level language), problemi insanın ifade biçimine yakın yazdıran, donanım ayrıntılarını gizleyen dildir: C, Java, Python.
Şekil 4.14 dilleri bir merdiven üzerinde dizer. En altta makine dili vardır: 10110000 01100001. Bu iki bayt, Intel uyumlu işlemcilerde “AL yazmacına 61h (onaltılı 61, onluk 97) değerini yükle” komutudur. Assembly aynı komutu MOV AL, 61h diye yazar. Bir basamak yukarıda C vardır: x = x + 1; satırı yazmaçlardan söz etmez, bir değişkenden söz eder. Java ve C# daha da yukarıdadır; bellek yönetimini programcıdan alır. Python en üsttedir: print(x) yazmak için bir sınıf ya da tip bildirimi gerekmez.
Merdivende yukarı çıkmak bir kazanç ve bir bedel getirir. Kazanç, aynı işi daha az ve daha anlaşılır satırla yazmaktır. Bir dizgiyi ekrana yazmak Python'da tek satırdır; assembly'de onlarca satır sürebilir. Bedel, donanım üzerindeki denetimin azalmasıdır. Bir sürücü yazılımı belirli bir bellek adresine belirli bir anda yazmak zorundaysa C ya da assembly gibi alt basamaklardaki diller gerekir. Bu yüzden seviye bir kalite ölçüsü değildir; bir dilin soyutlama düzeyini anlatır. Yüksek seviyeli bir dil düşük seviyeli bir dilden “daha iyi” değildir, başka işlere daha uygundur.
10110000 01100001 makine komutundaki ikinci bayt hangi sayıdır? Bu sayı assembly'de neden 61h diye yazılır?
- İkinci bayt
01100001'dir. Basamak değerleriyle: 0·128 + 1·64 + 1·32 + 0·16 + 0·8 + 0·4 + 0·2 + 1·1 = 64 + 32 + 1 = 97. - Bayt dört bitlik iki gruba ayrılır:
0110= 6 ve0001= 1. Her dört bit tek bir onaltılı basamaktır; sayı onaltılı tabanda 61'dir. Denetim: 6 · 16 + 1 = 97. - Sondaki h harfi sayının onaltılı (hexadecimal) olduğunu gösterir. Assembly programcıları onaltılı yazımı sever, çünkü her basamak tam dört bite karşılık gelir ve ikili koda dönüşüm kolaydır. 97, ASCII tablosunda küçük “a” harfinin kodudur.
4.3.2Programlama dillerinin gelişimi
İlk bilgisayarlar doğrudan makine diliyle programlandı. Programcılar komutları ikili sayılar olarak yazıyor, bazen anahtarları elle tek tek çeviriyordu. 1940'ların sonunda assembly ortaya çıktı: komutlara akılda kalır adlar verildi ve bu adları ikiliye çeviren assembler programları yazıldı. Şekil 4.15 sonraki yetmiş yılı bir zaman çizelgesinde özetler.
Gelişim üç döneme ayrılabilir.
- 1940–1960. Makine dili ve assembly dönemi. 1957'de IBM'de John Backus'un ekibi FORTRAN'ı geliştirdi. FORTRAN, bilimsel formülleri neredeyse matematikteki gibi yazmayı sağlayan ilk yaygın yüksek seviyeli dildir. 1959'da iş ve muhasebe uygulamaları için COBOL tasarlandı. 1964'te Dartmouth Koleji'nde öğrencilerin programlama öğrenmesi için BASIC geliştirildi.
- 1970–1990. 1972'de Bell Laboratuvarları'nda Dennis Ritchie C dilini geliştirdi. UNIX işletim sistemi büyük ölçüde C ile yeniden yazıldı. C bir yandan donanıma yakın, öbür yandan farklı işlemcilere taşınabilir olduğu için sistem programlamanın temel dili oldu. 1983'te Bjarne Stroustrup, C'ye sınıf ve nesne kavramlarını ekleyen C++'ı duyurdu.
- 1990 sonrası. 1991'de Python, 1995'te Java ve JavaScript, 2000'de C# ortaya çıktı. Bu dillerin ortak noktası, bellek yönetimini programcıdan alan yönetilen ortamlar ve web'e uygunluktur. Zengin kütüphaneleri ve açık kaynak ekosistemleri de yaygınlaşmalarında etkili oldu. Daha yakın dönemde Go (2009) eşzamanlı sunucu programlarına, Rust (1.0 sürümü 2015) ise güvenli sistem programlamaya odaklandı.
Bu tarihte bir eğilim göze çarpar: her yeni kuşak, öncekinin programcıya bıraktığı bir işi üstlendi. Assembly ikili kodlamayı, FORTRAN yazmaç yönetimini, Java bellek yönetimini programcının elinden aldı. Programcıya kalan iş, problemin kendisine biraz daha yaklaştı.
4.3.3Kullanım alanları ve dil seçimi
Programlar çok farklı ortamlarda çalışır ve her ortamın ihtiyaçları başkadır. Başlıca kullanım alanları şunlardır:
- Web: tarayıcıda çalışan arayüzler (istemci), isteklere yanıt veren sunucu programları ve web servisleri.
- Mobil: telefon ve tablet uygulamaları.
- Veri ve yapay zekâ: veri analizi, istatistiksel modelleme, makine öğrenmesi ve otomasyon.
- Sistem: işletim sistemleri, aygıt sürücüleri ve bir ev aletinin içindeki gömülü yazılım.
- Bilimsel hesaplama: simülasyon ve mühendislik çözümleri; bir köprünün yük altındaki davranışı ya da bir devrenin frekans yanıtı.
- Oyun: oyun motorları, grafik ve kullanıcıyla etkileşim.
Her alanda bazı diller öne çıkar (Şekil 4.16). Tarayıcıda çalışan arayüzler için JavaScript (ve onun tipli bir uzantısı olan TypeScript) neredeyse tek seçenektir, çünkü tarayıcıların doğrudan çalıştırdığı dil budur. Android uygulamaları Kotlin ya da Java ile, iOS uygulamaları Swift ile yazılır; bu platformların resmî araçları bu dilleri destekler. Veri analizinde Python ve R, zengin kütüphaneleri sayesinde yaygındır. Gömülü sistemlerde bellek ve işlemci kısıtlı olduğundan C, C++ ve Rust tercih edilir. Kurumsal sunucularda Java, C# ve Go sık görülür.
Dil seçerken önce problem alanı ve programın çalışacağı platform değerlendirilir. Sonra şu sorular sorulur. Gereken işi yapan hazır kütüphaneler var mı? Dilin topluluğu soruların yanıtını bulmaya yetecek kadar büyük mü? Program yıllarca bakım görecekse o dili bilen geliştirici bulmak kolay mı? Hız ve bellek kullanımı ne kadar önemli? Güvenlik açısından dilin bir zayıflığı var mı? Ekip hangi dili zaten biliyor? Program nasıl dağıtılacak?
Bir elektrik-elektronik mühendisliği öğrenci grubu iki proje yapacak: (a) bir mikrodenetleyici üzerinde çalışıp sıcaklık sensörünü okuyan ve fanı açıp kapatan bir program, (b) sensörden bir hafta boyunca toplanan verinin grafiğini çizip ortalamasını ve en yüksek değerini bulan bir çözümleme. Hangi diller uygundur?
- (a) Mikrodenetleyicinin belleği birkaç kilobayt olabilir, işletim sistemi bulunmayabilir. Programın donanım yazmaçlarına doğrudan erişmesi gerekir. Bu koşullar düşük seviyeye yakın, derlenen bir dili gerektirir: C ya da C++. Rust da giderek yaygınlaşan bir seçenektir.
- (b) Program bir dizüstü bilgisayarda bir kez çalışacak; hız ikinci plandadır. Grafik ve istatistik için hazır kütüphaneler ve hızlı deneme önemlidir. Python (ya da R) uygundur.
- İki projede farklı dillerin seçilmesi bir çelişki değildir. Seçim, problemin ve platformun ihtiyaçlarından çıkar.
Bir dili yalnızca popüler olduğu için seçmek. En popüler dil bir mikrodenetleyiciye sığmayabilir ya da gereken kütüphane o dilde bulunmayabilir. Popülerlik, topluluk ve kaynak bolluğu açısından bir artıdır, ama tek başına doğru seçim için yeterli değildir.
4.3.4Dil türleri
Diller kullanım amacına göre de gruplanır. Bir dil birden fazla gruba girebilir; Python hem genel amaçlı hem de bir betik dilidir.
| Tür | Özellik | Örnek diller | Örnek satır |
|---|---|---|---|
| Genel amaçlı | Her tür problem için değişken, karar, döngü ve fonksiyon sunar | Java, C, Python | for (int i = 0; i < 3; i++) |
| Alana özgü | Tek bir işe odaklanır | SQL, Excel formülleri | SELECT ad FROM ogrenci WHERE ort >= 60; |
| Betik | Tekrarlanan işleri otomatikleştirir, genellikle yorumlanır | Python, JavaScript, Bash, PowerShell | Copy-Item *.txt yedek\ |
| İşaretleme | İçeriğin yapısını tanımlar; karar ve döngü yoktur | HTML, XML, Markdown | <h1>Başlık</h1> |
Genel amaçlı diller (general-purpose languages) değişken, karar, döngü ve fonksiyon gibi yapıların tamamını sunar. Bu yapılarla, ilke olarak, bilgisayarın yapabileceği her hesap yazılabilir. Tablodaki for satırı Java'da üç tur dönen bir döngünün başıdır.
Alana özgü diller (domain-specific languages) tek bir işi çok iyi yapar. SQL veri tabanını sorgulamak için tasarlanmıştır. Tablodaki satır, öğrenci tablosundan ortalaması 60 ve üzeri olanların adlarını getirir. Satır, sonucun nasıl bulunacağını değil, ne istendiğini söyler; tabloyu nasıl tarayacağına veri tabanı yönetim sistemi karar verir. Excel'deki =ORTALAMA(B2:B30) gibi formüller de alana özgü bir dildir: hücrelerde hesap yapar.
Betik dilleri (scripting languages) tekrarlanan işleri otomatikleştirmek için kullanılır. Tablodaki PowerShell komutu, bulunduğu klasördeki bütün .txt dosyalarını yedek klasörüne kopyalar. Yüzlerce dosyayı elle kopyalamak yerine bu tek satır çalıştırılır. Betik dilleri genellikle yorumlanır; yazılan satır hemen denenebilir.
İşaretleme dilleri (markup languages) bir belgenin yapısını tanımlar. HTML'deki <h1>Başlık</h1> “bu metin birinci düzey başlıktır” der. İşaretleme dillerinde karar ve döngü yoktur; bir koşula göre farklı iş yapamaz ve bir işi tekrarlayamaz. Bu yüzden programlama dili sayılmazlar. Bir web sayfasında HTML yapıyı, CSS görünümü, JavaScript ise davranışı tanımlar; bu üçlüden yalnızca JavaScript bir programlama dilidir.
Bu gruplara blok tabanlı görsel diller de eklenebilir. Scratch gibi dillerde komutlar yazılmaz; renkli bloklar sürüklenip birbirine takılır. Bloklar yalnızca kurala uygun biçimde birleşebildiği için söz dizimi hatası yapılamaz. Bu özellik, programlamayı öğretmeye yeni başlanırken dikkati söz diziminden algoritmaya çevirir.
HTML'yi bir programlama dili saymak. HTML bir sayfadaki başlığı, paragrafı ve bağlantıyı tanımlar ama hiçbir hesap yapmaz: iki sayıyı toplayamaz, bir koşulu sınayamaz, bir işi tekrarlayamaz. Bir web sayfasının “programlanan” kısmı, onunla birlikte çalışan JavaScript kodudur.
4.3.5Programlama paradigmaları
Aynı problemi çözmenin birden çok düşünme biçimi vardır. Biri adımları sırayla sayar, biri problemi birbiriyle konuşan nesneler olarak görür, biri de veriyi dönüştüren fonksiyonlar zinciri kurar. Bu düşünme biçimlerine programlama paradigması denir.
Programlama paradigması (programming paradigm), bir programın nasıl düzenleneceğine ilişkin temel yaklaşımdır. Buyurgan yaklaşım (imperative programming) işlemlerin nasıl yapılacağını adım adım belirtir ve programın durumunu bu adımlarla değiştirir. Nesne yönelimli yaklaşım (object-oriented programming) veri ile o veri üzerinde çalışan davranışı nesnelerde birleştirir. Fonksiyonel yaklaşım (functional programming) programı, değiştirilemez veri üzerinde çalışan fonksiyonların birleşimi olarak kurar.
Şekil 4.17 üç yaklaşımı aynı görev üzerinde karşılaştırır: notlar = {70, 90, 80} dizisinin toplamını bulmak. Aşağıdaki Java programı üç yaklaşımı da içerir.
import java.util.Arrays;
public class Paradigmalar {
static class Ogrenci {
private final int[] notlar;
Ogrenci(int[] notlar) { this.notlar = notlar; }
int toplam() {
int t = 0;
for (int n : notlar) t += n;
return t;
}
}
public static void main(String[] args) {
int[] notlar = {70, 90, 80};
// buyurgan
int toplam = 0;
for (int n : notlar)
toplam += n;
System.out.println(toplam);
// nesne yönelimli
Ogrenci ogrenci = new Ogrenci(notlar);
System.out.println(ogrenci.toplam());
// fonksiyonel
System.out.println(Arrays.stream(notlar).sum());
System.out.println(Arrays.toString(notlar));
}
}240
240
240
[70, 90, 80]Buyurgan çözüm, toplam adlı bir değişkeni 0 ile başlatır ve döngünün her turunda bu değişkeni değiştirir. Toplamın değerleri sırasıyla 0, 70, 160 ve 240'tır. Program bir tarif gibi okunur: “önce şunu yap, sonra bunu yap.” C ve assembly temelde buyurgan dillerdir.
Nesne yönelimli çözümde notlar bir Ogrenci nesnesinin içindedir. Toplamı hesaplama işi de aynı nesneye aittir: ogrenci.toplam(). Nesneyi kullanan kod, toplamın nasıl hesaplandığını bilmek zorunda değildir; yalnızca nesneye sorar. Program büyüdükçe bu düzen işe yarar. Öğrenciyle ilgili her şey tek bir yerde durur, değişiklik gerektiğinde nereye bakılacağı bellidir. Java, C++ ve C# nesne yönelimli dillerdir.
Fonksiyonel çözümde Arrays.stream(notlar).sum() ifadesi diziyi bir değer akışına çevirir ve akışı toplar. Ara durum tutan bir değişken yoktur ve dizinin kendisi değişmez. Programın son satırı bunu doğrular: dizi hâlâ [70, 90, 80]'dir. Fonksiyonel yaklaşım, aynı girdiye her zaman aynı çıktıyı veren ve dışarıdaki hiçbir şeyi değiştirmeyen fonksiyonları öne çıkarır. Böyle fonksiyonları sınamak ve birleştirmek kolaydır. Haskell tamamen fonksiyonel bir dildir; Python, JavaScript ve Java ise fonksiyonel öğeler de sunar.
Pek çok bugünkü dil birden fazla paradigmayı destekler. Java'da üç çözüm de yazılabilir; Python'da da öyle:
notlar = [70, 90, 80]
toplam = 0
for n in notlar:
toplam += n
print(toplam)
print(sum(notlar))
from functools import reduce
print(reduce(lambda a, b: a + b, notlar, 0))240
240
240Python'daki reduce fonksiyonu, iki sayıyı toplayan küçük bir fonksiyonu dizinin öğelerine 0'dan başlayarak sırayla uygular: ((0 + 70) + 90) + 80 = 240. Hangi yaklaşımın seçileceği problemin yapısına ve ekibin alışkanlığına bağlıdır. Büyük programlar çoğu zaman üç yaklaşımı birlikte kullanır.
Bir dizideki sayıların en büyüğü bulunacak. Buyurgan ve fonksiyonel yaklaşım bu işi nasıl ifade eder? Dizi {12, 45, 7, 30} olsun.
- Buyurgan:
ebadlı bir değişken dizinin ilk öğesiyle (12) başlatılır. Her öğeebile karşılaştırılır; daha büyükseebgüncellenir. Durum adım adım değişir: 12 → 45 → 45 → 45. Sonuç 45'tir. - Fonksiyonel: Java'da
Arrays.stream(dizi).max().getAsInt(), Python'damax(dizi). İfade “dizinin en büyüğü” der; karşılaştırmaların sırası ve ara değişken programcıya görünmez. Dizi değişmez, sonuç yine 45'tir. - İki çözümde de bilgisayar aynı karşılaştırmaları yapar. Fark programın okunuşundadır: biri “nasıl”ı, öbürü “ne”yi öne çıkarır.
4.3.6Statik ve dinamik tipleme
Her değerin bir tipi vardır: 5 bir tam sayı, "beş" bir dizgi, true bir mantıksal değerdir. Diller, tiplerin ne zaman denetlendiğine göre ikiye ayrılır.
Statik tipleme (static typing), değişkenin tipinin kaynak kodda belli olduğu ve derleme sırasında denetlendiği tipleme biçimidir: Java, C, C#. Dinamik tipleme (dynamic typing), tipin değişkende değil değerde taşındığı ve çalışma sırasında denetlendiği biçimdir: Python, JavaScript.
Java'da bir değişken bildirilirken tipi de yazılır: int x = 5;. Bu andan sonra x yalnızca tam sayı tutabilir. x = "beş"; satırı derleyicinin anlam çözümleme aşamasında yakalanır: “incompatible types: String cannot be converted to int”. Program hiç çalışmadan hata bulunur.
Python'da değişkenin tipi bildirilmez. Aynı ad önce bir sayıyı, sonra bir metni gösterebilir. Tip, değerin kendisinde taşınır:
x = 5
print(type(x).__name__)
x = "beş"
print(type(x).__name__)
print("a" + 1)int
str
Traceback (most recent call last):
File "dinamik.py", line 5, in <module>
print("a" + 1)
~~~~^~~
TypeError: can only concatenate str (not "int") to strİlk iki satırın çıktısı x'in önce int, sonra str (dizgi) tipinde bir değeri gösterdiğini söyler. İkisi de geçerlidir. Beşinci satırdaki "a" + 1 ise bir dizgiye sayı eklemeye çalışır ve TypeError verir. Önemli nokta hatanın ne zaman çıktığıdır. Python dosyayı ayrıştırırken bu satırda bir sorun görmez, çünkü satırın söz dizimi doğrudur. Hata ancak satır çalıştığında, önceki iki print satırının çıktısı ekrana yazıldıktan sonra ortaya çıkar. Bu satır programın nadiren çalışan bir dalında olsaydı, hata aylarca fark edilmeyebilirdi.
Bazı diller tip uyuşmazlığında hata vermek yerine tipleri sessizce dönüştürür. JavaScript bunun bilinen örneğidir. Aşağıdaki satırlar Node.js ile çalıştırılmıştır:
console.log("5" * 2);
console.log("5" + 2);
console.log(typeof ("5" * 2), typeof ("5" + 2));
console.log("5" - 2);10
52
number string
3"5" * 2 ifadesinde çarpma yalnızca sayılarla yapılabildiği için JavaScript "5" dizgisini 5 sayısına çevirir ve 10 bulur. "5" + 2 ifadesinde ise + dizgi birleştirme anlamına da geldiği için 2 dizgiye çevrilir ve "52" elde edilir. Üçüncü satır iki sonucun tiplerini gösterir: biri sayı, öbürü dizgidir. Aynı iki değer, işleme göre farklı tiplere dönüşmüştür. Bu tür sessiz dönüşümler kısa betiklerde kolaylık sağlar, büyük programlarda ise bulunması zor mantık hatalarına yol açar.
| Özellik | Statik (Java) | Dinamik (Python) |
|---|---|---|
| Tip nerede durur? | Değişkende, bildirimde yazılır | Değerde, bildirim yok |
| Tip hatası ne zaman bulunur? | Derleme sırasında, program çalışmadan | O satır çalıştığında |
| Aynı ad farklı tipte değer tutabilir mi? | Hayır | Evet |
| Güçlü yön | Hatalar erken bulunur, IDE daha çok yardım eder | Kısa kod, hızlı deneme |
Dinamik tiplemeyi “tip yok” sanmak. Python'da her değerin kesin bir tipi vardır ve Python tip kurallarını uygular: "a" + 1 hata verir. Dinamik tiplemede eksik olan tip kuralları değil, bu kuralların program çalışmadan denetlenmesidir.
4.3.7Sözde koddan dillere
Bir algoritma önce sözde kodla yazılıp sonra bir dile çevrildiğinde, sözde koddaki her yapının dilde bir karşılığı bulunur. Tablo 4.8 en sık kullanılan yapıların Java ve Python karşılıklarını verir.
| Sözde kod | Java | Python |
|---|---|---|
oku x | x = giris.nextInt(); | x = int(input()) |
yaz "Toplam: ", t | System.out.println("Toplam: " + t); | print("Toplam:", t) |
x ← x + 1 | x = x + 1; | x = x + 1 |
eğer x > 0 ise … değilse … | if (x > 0) { … } else { … } | if x > 0: … else: … |
her i ← 1 … n için | for (int i = 1; i <= n; i++) | for i in range(1, n + 1): |
koşul doğru iken tekrarla | while (kosul) { … } | while kosul: |
Sözde kodda atama için ← kullanılır, çünkü = işareti matematikte eşitlik anlamına gelir. Java ve Python atamayı = ile, eşitlik karşılaştırmasını == ile yazar. Java'daki giris, klavyeden okuyan bir Scanner nesnesidir. Python'da input() her zaman dizgi döndürür; sayı isteniyorsa int(...) ile çevrilmesi gerekir.
İki dil arasındaki en görünür fark blokların sınırlarıdır. Java bir bloğu süslü parantezlerle ({ }) belirler, her deyimi de ; ile bitirir. Python bunların ikisini de istemez; bloğu girinti belirler. Bu yüzden Python'da girinti yalnızca görünüş değildir, programın anlamının parçasıdır:
x = -3
if x > 0:
print("pozitif")
print("bitti")
if x > 0:
print("pozitif")
print("bitti")bittix = −3 olduğu için iki koşul da yanlıştır. İlk if bloğunda print("bitti") girintisizdir, yani bloğun dışındadır ve koşuldan bağımsız çalışır. İkinci blokta aynı satır girintilidir, bloğun içindedir ve koşul yanlış olduğu için çalışmaz. İki parça arasındaki tek fark dört boşluktur; çıktıda yalnızca bir “bitti” görünür.
1'den n'ye kadar olan sayıların toplamını bulan sözde kod, n = 5 için Java ve Python'a nasıl çevrilir?
toplam ← 0
her i ← 1 … n için
toplam ← toplam + i
yaz "Toplam: ", toplamJava karşılığı (n = 5 sabit alınmıştır):
public class Bilesik {
public static void main(String[] args) {
int toplam = 0;
for (int i = 1; i <= 5; i++) {
toplam = toplam + i;
}
System.out.println("Toplam: " + toplam);
}
}Python karşılığı:
n = 5
toplam = 0
for i in range(1, n + 1):
toplam = toplam + i
print("Toplam:", toplam)
i = 1
while i <= 3:
print(i)
i = i + 1
print("bitti")Python dosyası ayrıca bir while döngüsüyle 1, 2, 3 yazar. Çıktılar:
Toplam: 15Toplam: 15
1
2
3
bittiToplam her turda şöyle değişir: 0 + 1 = 1, 1 + 2 = 3, 3 + 3 = 6, 6 + 4 = 10, 10 + 5 = 15. Python'daki range(1, n + 1) ifadesi 1'den başlar ve n + 1'e ulaşmadan durur; yani 1, 2, 3, 4, 5 değerlerini üretir. range(1, n) yazılsaydı 5 atlanır ve toplam 10 çıkardı.
4.4Problem çözme
Program yazmak klavyenin başında başlamaz. Önce problemin ne olduğu anlaşılır, sonra elle birkaç örnek çözülür, en son kod yazılır. Bu sıra atlandığında ortaya çoğu zaman “çalışan ama yanlış işi yapan” programlar çıkar.
4.4.1Problemden programa
Örnek problem basittir: bir dikdörtgenin alanını hesaplayan bir program. Şekil 4.18 bu problemin programa dönüşmesini beş aşamada gösterir. Aşamaların her birinin çıktısı bir sonrakinin girdisidir.
- Problemi tanımla. Ne istendiği netleşir: dikdörtgenin alanı. Programın girdisi en ve boy, çıktısı alandır. Bir de kısıt vardır: bir uzunluk sıfır ya da negatif olamaz, yani en ve boy 0'dan büyük olmalıdır. Kısıtlar çoğu zaman problemde açıkça yazmaz; problemi tanımlayan kişinin onları bulması gerekir.
- Örnekleri dene. Elle birkaç örnek çözülür: 3 × 4 = 12, 5 × 5 = 25. Bir de sınır durumu düşünülür: en 0 olursa ne olmalı? Kısıta göre bu girdi geçersizdir. Elle çözülen örnekler, sonradan programın doğru çalışıp çalışmadığını sınamak için beklenen sonuçları da verir.
- Algoritmayı kur. Dilden bağımsız adımlar sözde kodla yazılır: en'i oku, boy'u oku, alanı hesapla, alanı yaz.
- Koda dönüştür. Algoritma seçilen dile çevrilir. Java'da hesap satırı
alan = en * boy;, yazdırma satırıSystem.out.println(alan);olur. - Test et. Program elle çözülen örneklerle çalıştırılır. 3 ve 4 için 12 çıkar: doğru. −2 ve 4 için −8 çıkar: bir alan negatif olamayacağına göre bu yanlıştır. Hata kodda değil, algoritmadadır; algoritma kısıtı hiç denetlemiyordur. Algoritmaya dönülür ve “en ve boy 0'dan büyük değilse hata ver” adımı eklenir.
Son aşamadaki geri dönüş sürecin doğal bir parçasıdır. Aşamalar düz bir çizgi değil, bir döngü oluşturur: test, kodlamada ya da algoritmada bir eksik bulursa ilgili aşamaya dönülür. Düzeltilmiş algoritma şudur:
oku en
oku boy
eğer en > 0 ve boy > 0 ise
alan ← en × boy
yaz "Alan: ", alan
değilse
yaz "Geçersiz ölçü"Bu algoritmanın Java karşılığı aşağıdadır. && işareti Java'da “ve” bağlacıdır: iki koşul da doğruysa sonuç doğrudur.
import java.util.Scanner;
public class Alan {
public static void main(String[] args) {
Scanner giris = new Scanner(System.in);
int en = giris.nextInt();
int boy = giris.nextInt();
if (en > 0 && boy > 0) {
int alan = en * boy;
System.out.println("Alan: " + alan);
} else {
System.out.println("Geçersiz ölçü");
}
}
}Program dört girdiyle çalıştırılmıştır: (3, 4), (−2, 4), (0, 5) ve (5, 2). Çıktılar sırasıyla şunlardır:
Alan: 12Geçersiz ölçüGeçersiz ölçüAlan: 10Problemi tanımlamadan kod yazmaya başlamak. Yanlış tanımlanan bir problem, doğru çalışan ama yanlış işi yapan bir programa dönüşür. “Not ortalaması hesaplayan program” isteği birkaç soruyu yanıtsız bırakır. Notlar ağırlıklı mı? Kaç not girilecek? Geçersiz not (−5, 120) girilirse ne olacak? Bu sorular sorulmadan yazılan program, ilk gerçek kullanımda değiştirilmek zorunda kalır.
4.4.2Alan algoritmasını izleme
Bir algoritmanın doğru çalıştığından emin olmanın en güvenilir yolu, onu kâğıt üzerinde satır satır izlemektir. Her adımda değişkenlerin değeri bir tabloya yazılır. Bu tabloya izleme tablosu (trace table) denir. Değişkenler bellekte birer kutu gibi düşünülebilir: oku en satırı klavyeden gelen sayıyı “en” adlı kutuya yazar. Henüz değer verilmemiş kutular tabloda “—” ile gösterilir.
Tablo 4.9 algoritmanın en = 3, boy = 4 girdisiyle çalışmasını izler.
| Adım | Satır | en | boy | alan | Açıklama / çıktı |
|---|---|---|---|---|---|
| 1 | oku en | 3 | — | — | 3 okunur ve en kutusuna yazılır |
| 2 | oku boy | 3 | 4 | — | 4 okunur |
| 3 | eğer en > 0 ve boy > 0 | 3 | 4 | — | 3 > 0 ve 4 > 0 → doğru; “ise” dalına girilir |
| 4 | alan ← en × boy | 3 | 4 | 12 | 3 × 4 = 12 |
| 5 | yaz "Alan: ", alan | 3 | 4 | 12 | Çıktı: Alan: 12; “değilse” dalı atlanır |
Tablo 4.10 aynı algoritmayı en = −2, boy = 4 girdisiyle izler. Bu, test aşamasında yanlış sonuç veren girdidir.
| Adım | Satır | en | boy | alan | Açıklama / çıktı |
|---|---|---|---|---|---|
| 1 | oku en | −2 | — | — | −2 okunur |
| 2 | oku boy | −2 | 4 | — | 4 okunur |
| 3 | eğer en > 0 ve boy > 0 | −2 | 4 | — | −2 > 0 yanlış; “ve” ile bağlı koşulun tamamı yanlış → “değilse” dalı |
| 4 | yaz "Geçersiz ölçü" | −2 | 4 | — | Çıktı: Geçersiz ölçü; alan hiç hesaplanmadı |
İkinci izlemede alan kutusu baştan sona boş kalır. Program “ise” dalına hiç girmemiştir. Testte bulunan hata böylece düzeltilmiştir: −2 için artık −8 değil, bir uyarı yazılır.
Alan algoritmasına çevre hesabı eklenmek isteniyor: çevre = 2 × (en + boy). Algoritma nasıl değişir? en = 6, boy = 1,5 için çıktılar nedir?
- “ise” dalına iki satır eklenir:
cevre ← 2 × (en + boy)veyaz "Çevre: ", cevre. Kısıt denetimi aynı kalır; geçersiz ölçüde çevre de hesaplanmaz. - Koşul: 6 > 0 ve 1,5 > 0 → doğru.
- alan = 6 × 1,5 = 9.
- cevre: önce parantez, 6 + 1,5 = 7,5; sonra 2 × 7,5 = 15.
- Çıktı: “Alan: 9” ve “Çevre: 15”. Parantez yazılmasaydı çarpma önce yapılır ve 2 × 6 + 1,5 = 13,5 bulunurdu.
Bu girdide en ve boy kesirli olabildiği için Java karşılığında int yerine double kullanılması gerekir.
4.5Yazılım geliştirme
Dikdörtgen alanı gibi küçük bir program tek kişinin bir saatlik işidir. Bir bankanın mobil uygulaması ya da bir hastanenin randevu sistemi ise onlarca kişinin aylarca, hatta yıllarca süren işidir. Bu ölçekte işin düzenlenmesi en az kodun kendisi kadar önemlidir.
4.5.1Yazılım geliştirme süreci
Problemden programa giden yol büyük ölçekte altı aşamaya ayrılır (Şekil 4.19). Aşamalar bir “öğrenci not ortalamasını hesaplayan uygulama” üzerinden şöyle açıklanabilir.
- Gereksinim. Kullanıcı ne istiyor? Öğrenci her dersin notunu ve kredisini girecek, uygulama ağırlıklı ortalamayı gösterecek. Not 0–100 arasında olmalı. Uygulama telefonda çalışacak. Gereksinimler kullanıcıyla konuşularak yazılır ve sonraki aşamaların hepsi bu yazıya dayanır.
- Tasarım. Ekranlar, veri ve algoritmalar planlanır. Hangi ekranda ne girilecek? Notlar nerede saklanacak? Ortalama hangi formülle hesaplanacak? Akış diyagramı ve sözde kod bu aşamanın araçlarıdır.
- Kodlama. Tasarım bir programlama diliyle yazılır.
- Test. Beklenen sonuçlar gerçek sonuçlarla karşılaştırılır. 60 puanlı 3 kredilik ve 90 puanlı 2 kredilik iki ders için beklenen ortalama (60 · 3 + 90 · 2) / 5 = 360 / 5 = 72'dir. Uygulama 75 gösteriyorsa (iki notun ağırlıksız ortalaması) bir hata vardır ve kodlamaya geri dönülür.
- Dağıtım. Program kullanıcıya ulaştırılır: uygulama mağazasında yayımlanır ya da kurulum dosyası paylaşılır.
- Bakım. Kullanım başladıktan sonra yeni hatalar bulunur, yeni istekler gelir: “harf notunu da göstersin”. Bu istekler yeni gereksinimlerdir ve döngü yeniden başlar.
Aşamalar projeye göre ardışık ya da yinelemeli yürütülür. Kullanıcıdan gelen geri bildirim gereksinimi ve tasarımı değiştirebilir. Bir yazılımın ömrü boyunca harcanan emeğin büyük kısmı ilk sürüme değil, bakıma gider. Bu yüzden kodun ilk günden okunabilir ve değiştirilebilir yazılması önemlidir.
Test aşamasını “programın çalıştığını görmek” sanmak. Testin amacı programın doğru çalıştığını kanıtlamak değil, yanlış çalıştığı durumları bulmaktır. İyi bir test, sonucu önceden elle hesaplanmış normal girdileri, sınır durumlarını (0, boş liste, en küçük ve en büyük değer) ve geçersiz girdileri içerir.
4.5.2Geliştirme yaklaşımları
Altı aşamanın nasıl düzenleneceğine dair farklı yaklaşımlar vardır (Şekil 4.20).
Şelale modelinde (waterfall model) aşamalar sırayla ve birer kez yapılır. Gereksinimler tamamen yazılmadan tasarıma, tasarım bitmeden kodlamaya geçilmez. Her aşamanın sonunda bir belge üretilir. Bu düzenin güçlü yanı açıklığıdır: kimin ne zaman ne yapacağı bellidir. Zayıf yanı değişikliğe kapalı olmasıdır. Kodlama sırasında gereksinimde bir eksik fark edilirse geri dönmenin maliyeti yüksektir.
Çevik yaklaşımda (agile) iş kısa döngülere bölünür; her döngü birkaç haftalıktır. Her döngüde biraz planlama, biraz kodlama ve test yapılır. Döngü sonunda kullanıcıya çalışan bir sürüm gösterilir ve geri bildirim alınır. Böylece yanlış anlaşılan bir gereksinim aylar sonra değil, birkaç hafta içinde ortaya çıkar. Bu yaklaşımın bedeli, kullanıcıyla sürekli iletişim kurma gereğidir.
Prototiplemede (prototyping) gerçek ürüne başlamadan önce hızlıca bir taslak hazırlanır. Taslakta ekranlar vardır ama arkalarında gerçek hesap ya da veri tabanı bulunmayabilir. Kullanıcı ne istediğini tam anlatamıyorsa, taslağı gördüğünde fikri netleşir. Taslağın amacı gereksinimi görünür kılmaktır. Ürünün kendisi sanılıp doğrudan kullanıma sunulmamalıdır.
| Yaklaşım | Güçlü yön | Dikkat |
|---|---|---|
| Şelale modeli | Aşamalar ve belgeler nettir | Değişikliğe dönüş maliyetlidir |
| Çevik | Kısa döngülerle geri bildirim alır | Sürekli iletişim gerektirir |
| Prototipleme | Belirsiz gereksinimi görünür kılar | Prototip ürün sanılmamalıdır |
Üç proje için hangi yaklaşım uygundur? (a) Bir uçağın iniş takımını denetleyen yazılım; gereksinimler uluslararası standartlarla önceden kesin olarak belirlenmiş. (b) Bir kafenin sipariş uygulaması; sahibi “ekranları görünce karar veririm” diyor. (c) Bir öğrenci kulübünün etkinlik uygulaması; özellikler her dönem üyelerin isteğiyle değişiyor.
- (a) Şelale modeli. Gereksinimler sabittir ve her aşamanın belgelenmesi denetim için zorunludur. Değişiklik beklenmediği için modelin zayıf yanı burada sorun olmaz.
- (b) Prototipleme. Kullanıcı ne istediğini ancak somut ekranları görünce söyleyebilir. Hızlı bir taslak bu belirsizliği giderir.
- (c) Çevik yaklaşım. İstekler sürekli değişir; kısa döngüler ve her döngü sonunda alınan geri bildirim buna uyar.
4.5.3Geliştirme araçları
Bir geliştiricinin günlük işi bir döngüdür: kodu yaz, çevir ve çalıştır, sonucu sına, hata varsa bul, düzelt ve yeniden dene. Bu döngünün her adımında farklı bir araç devreye girer (Şekil 4.21).
- Kod düzenleyici ve IDE. Kod bir metin düzenleyicide yazılabilir, ama programcılar çoğunlukla bir tümleşik geliştirme ortamı (IDE, integrated development environment) kullanır. IDE yazılan kodu renklendirir, adları tamamlar, söz dizimi hatalarını yazarken altını çizerek gösterir ve projenin dosyalarını düzenler. IntelliJ IDEA, Eclipse ve Visual Studio Code yaygın örneklerdir.
- Derleyici ve çalışma ortamı. Programı çevirir ve çalıştırır: javac ve JVM, Python yorumlayıcısı, C derleyicisi.
- Hata ayıklayıcı (debugger). Programı istenen satırda durdurur, değişkenlerin o anki değerlerini gösterir ve satır satır ilerletir.
- Sürüm kontrolü. Dosyalardaki her değişikliği kaydeder; ekip çalışmasını mümkün kılar.
- Test araçları. Beklenen davranışı otomatik olarak doğrular. Programcı bir kez “bu girdiye bu çıktı beklenir” diye yazar, araç her değişiklikten sonra bütün testleri saniyeler içinde yeniden çalıştırır. Java'da JUnit bu işin bilinen aracıdır.
- Paket yöneticisi (package manager). Programın kullandığı hazır kütüphaneleri (bağımlılıkları) indirir, sürümlerini izler ve günceller: Python'da pip, JavaScript'te npm, Java'da Maven ve Gradle.
IDE'nin altını çizmediği kodu hatasız sanmak. IDE söz dizimi ve tip hatalarını yazarken gösterebilir, çünkü derleyicinin ilk aşamalarını arka planda çalıştırır. Çalışma zamanı ve mantık hatalarını ise göremez. Kırmızı çizgi olmaması yalnızca derlemenin başarılı olacağını söyler.
4.5.4Sürüm kontrolü
Bir öğrenci ödevini yazarken dosyanın kopyalarını odev_son.java, odev_son2.java, odev_GERCEKSON.java diye saklıyorsa, sürüm kontrolünü elle yapmaya çalışıyordur. Bu yöntem birkaç gün içinde karışır. Hangi kopyada hangi değişikliğin yapıldığı unutulur; iki kişi aynı dosyada çalışıyorsa iş içinden çıkılmaz hâle gelir.
Sürüm kontrolü (version control), dosyalardaki değişikliklerin geçmişini saklayan ve birden çok kişinin aynı dosyalar üzerinde çalışmasını düzenleyen sistemdir. Kayıt (commit), dosyaların belirli bir andaki hâlinin kimin, ne zaman ve neyi değiştirdiği bilgisiyle birlikte saklanmasıdır. Dal (branch), ana koddan ayrılıp bağımsız olarak ilerleyen bir değişiklik çizgisidir. Birleştirme (merge), bir dalın değişikliklerinin başka bir dala aktarılmasıdır.
Şekil 4.22 dikdörtgen programının geçmişini gösterir. K1 ilk sürümdür. K2'de alan hesabı, K3'te çevre hesabı eklenmiştir. K2'den sonra bir geliştirici yeni bir hesap denemek için bir dal açmış (D1) ve ana kodu bozmadan orada çalışmıştır. Deneme başarılı olunca dal ana dala birleştirilmiştir (K4). Deneme başarısız olsaydı dal silinir, ana dal hiç etkilenmezdi. K3'teki bir değişiklik hata getirdiyse proje tek komutla K2'deki hâline döndürülebilir.
En yaygın sürüm kontrol aracı Git'tir. Git'te yukarıdaki geçmiş birkaç komutla oluşur. git init bir klasörü depo hâline getirir. git add değişen dosyaları bir sonraki kayda hazırlar. git commit -m "alan eklendi" kaydı açıklamasıyla oluşturur. git branch deneme yeni bir dal açar; git merge deneme o dalı bulunulan dala birleştirir. git log ise bütün kayıtları kim, ne zaman ve hangi açıklamayla bilgileriyle listeler.
Ekip çalışmasında her geliştiricinin bilgisayarında deponun bir kopyası bulunur. GitHub ve GitLab gibi hizmetler depoyu ağ üzerinde saklar ve kopyalar arasında ortak bir buluşma noktası olur. İki kişi aynı dosyanın farklı satırlarını değiştirdiyse Git değişiklikleri kendiliğinden birleştirir. Aynı satırı değiştirdilerse bir çakışma (merge conflict) oluşur. Git iki sürümü yan yana gösterir ve hangisinin kalacağını bir kişinin seçmesini ister.
Ayşe ve Mehmet aynı depodan çalışıyor. Programın 12. satırı double kdv = fiyat * 0.18;. Ayşe bu satırı fiyat * 0.20 yapıp kaydediyor. Mehmet de aynı satırı fiyat * KDV_ORANI yapıyor ve dosyanın başına sabiti ekliyor. İkisi değişikliklerini birleştirmek istediğinde ne olur?
- Dosyanın başına eklenen sabit satırı yalnızca Mehmet'in değişikliğidir; Git bunu sorunsuz alır.
- 12. satırı ikisi de değiştirmiştir. Git hangisinin doğru olduğunu bilemez ve çakışma bildirir. Satırın iki sürümü de dosyada işaretlenerek gösterilir.
- İki geliştirici konuşur ve satırın son hâline karar verir:
fiyat * KDV_ORANI, sabitin değeri de 0.20. Seçilen hâl kaydedilir ve birleştirme tamamlanır.
Sürüm kontrolünü yalnızca büyük ekiplerin aracı sanmak. Tek başına çalışan bir öğrenci de geçmişe dönme imkânından yararlanır. Bir değişiklik programı bozduğunda, en son çalışan sürüme dönmek bir komut sürer. Kayıtların açıklamaları da kodun neden değiştiğini sonradan hatırlamayı sağlar.
4.6Temel programlama yapıları
Diller birbirinden ne kadar farklı görünürse görünsün, hepsi aynı birkaç yapı taşı üzerine kurulur. Değerler adlandırılmış yerlerde saklanır ve ifadelerle hesaplanır. Koşullara göre farklı yollar seçilir, işler tekrarlanır. Sık yapılan işlere ad verilir, çok sayıda değer tek bir ad altında toplanır. Yapılar önce sözde kodla, sonra Java ve Python karşılıklarıyla verilir.
4.6.1Değişkenler ve veri tipleri
Değişken (variable), bellekte bir değeri tutan adlandırılmış bir yerdir; değeri program boyunca değişebilir. Veri tipi (data type), bir değerin bellekte nasıl temsil edileceğini ve üzerinde hangi işlemlerin yapılabileceğini belirler.
Değişken, üzerinde adı yazılı bir kutu gibi düşünülebilir (Şekil 4.23). yas kutusunda 19, boy kutusunda 1.75, ad kutusunda “Ali”, gecti kutusunda doğru değeri vardır. Kutuya yeni bir değer yazıldığında eskisi silinir. Kutunun tipi, içine ne konabileceğini sınırlar: tam sayı kutusuna metin konamaz.
| Tip | Ne tutar? | Örnek değer | Java | Yapılabilen işlemler |
|---|---|---|---|---|
| Tam sayı | Kesirsiz sayı | 19, −3, 0 | int | Dört işlem, kalan (mod) |
| Kayan noktalı sayı | Kesirli sayı | 1.75, 81.67 | double | Dört işlem |
| Dizgi | Karakter dizisi, metin | "Ali" | String | Birleştirme, uzunluk, karakter alma |
| Mantıksal | Doğru ya da yanlış | true, false | boolean | ve, veya, değil |
Kodda ondalık ayırıcı noktadır: Java ve Python 1,75 yazımını sayı olarak kabul etmez. Bu notta düz metinde Türkçe yazım kuralına uyularak virgül (1,75), kod ve program çıktılarında nokta (1.75) kullanılır.
Veri tipi seçimi değerin anlamından çıkar. Bir öğrencinin adı harflerden oluştuğu için dizgidir. Not ortalaması 81,67 gibi kesirli olabileceği için kayan noktalı sayıdır. “Dersi geçti mi?” sorusunun yanıtı yalnızca evet ya da hayır olabileceği için mantıksaldır. Öğrenci sayısı kesirli olamayacağı için tam sayıdır.
Değişkenlere anlamlı adlar vermek programın amacını görünür kılar. a = b * c satırı hiçbir şey anlatmaz; alan = en * boy satırı kendini açıklar. Adlar programın okunabilirliğinin en ucuz ve en etkili aracıdır.
Kesirli olabilecek bir değeri tam sayı tipinde tutmak. Java'da int ort = 245 / 3; satırı hata vermez; 81 değerini saklar ve kesirli kısım sessizce kaybolur. Ortalama, oran, ölçü gibi değerler için double kullanılır. Bölme işleminin işlenenlerinden en az biri de kesirli olmalıdır: 245 / 3.0.
4.6.2İşlem önceliği
Bir ifadede birden çok işlem varsa hangisinin önce yapılacağını işlem önceliği (operator precedence) belirler. Programlama dillerindeki kurallar okulda öğrenilen matematik kurallarıyla aynıdır:
- Parantez içi önce hesaplanır.
- Çarpma, bölme ve mod (kalan) toplamadan ve çıkarmadan önce yapılır.
- Toplama ve çıkarma en son yapılır.
- Aynı düzeydeki işlemler soldan sağa yapılır.
Mod işlemi (Java ve Python'da %) bölmeden kalanı verir: 7 mod 3, 7'nin 3'e bölümünden kalan olan 1'dir. Tablo 4.13 bu kuralların beş ifadeye uygulanmasını gösterir.
| İfade | Hesaplama sırası | Sonuç |
|---|---|---|
| 2 + 3 × 4 | Önce 3 × 4 = 12, sonra 2 + 12 | 14 |
| (2 + 3) × 4 | Önce parantez 2 + 3 = 5, sonra 5 × 4 | 20 |
| 20 / 4 × 2 | Aynı düzey, soldan sağa: 20 / 4 = 5, sonra 5 × 2 | 10 |
| 10 − 4 − 3 | Soldan sağa: 10 − 4 = 6, sonra 6 − 3 | 3 |
| 7 mod 3 + 1 | Önce 7 mod 3 = 1, sonra 1 + 1 | 2 |
Derleyici işlem önceliğini ifadenin ağacına yerleştirir. Şekil 4.24 2 + 3 × 4 ifadesinin ağacını gösterir: çarpma daha derinde olduğu için önce yapılır, toplama kökte olduğu için en son yapılır.
Tablodaki ifadeler Java'da da aynı sonuçları verir. Aşağıdaki program tablonun beş ifadesini ve soldan sağa kuralını sınayan iki ek ifadeyi yazdırır:
public class Oncelik {
public static void main(String[] args) {
System.out.println(2 + 3 * 4);
System.out.println((2 + 3) * 4);
System.out.println(20 / 4 * 2);
System.out.println(10 - 4 - 3);
System.out.println(7 % 3 + 1);
System.out.println(20 / (4 * 2));
System.out.println(10 - (4 - 3));
}
}14
20
10
3
2
2
9Son iki satır soldan sağa kuralının neden önemli olduğunu gösterir. 20 / 4 × 2 ifadesi 10 verir; parantezle 20 / (4 × 2) yazılınca sonuç 2'dir. Java'da bu ikinci sonuç aslında 2,5 olmalıydı: iki tam sayının bölümü tam sayı olduğu için 20 / 8 işleminin kesirli kısmı atılır. Benzer biçimde 10 − 4 − 3 = 3 iken 10 − (4 − 3) = 9'dur.
Python'da aynı ifadeler bir fark dışında aynı sonuçları verir:
185.0
81.67
52.33
14 20 10.0 3 2
10Çıktının dördüncü satırında 20 / 4 * 2 ifadesi 10.0 verir, çünkü Python'da / işleci her zaman kayan noktalı sonuç üretir. Tam sayı bölmesi için // işleci vardır ve son satırdaki gibi 10 verir. İlk satırdaki 185.0 ise eksik parantezin sonucudur. Bu hata Python'da da aynen ortaya çıkar.
Bir öğrencinin vize notu 60, final notu 80; vizenin ağırlığı %40, finalin %60. Programcı ort ← vize × 0,4 + final × 0,6 yazıyor. Sonuç doğru mudur? Aynı hesap ort ← vize + final × 0,6 / 2 diye yazılsaydı ne çıkardı?
- İlk ifadede iki çarpma önce yapılır: 60 × 0,4 = 24 ve 80 × 0,6 = 48. Sonra toplama: 24 + 48 = 72. Parantez gerekmez, çünkü çarpma zaten önce yapılır. Sonuç doğrudur.
- İkinci ifadede önce çarpma ve bölme, soldan sağa: 80 × 0,6 = 48, 48 / 2 = 24. Sonra toplama: 60 + 24 = 84. Program hata vermez ama 84 yazar; bu bir mantık hatasıdır.
Ortalama formülünde parantezi unutmak: ort ← a + b / 2. Bu ifade önce b'yi ikiye böler, sonra a'yı ekler. a = 70, b = 90 için sonuç 70 + 45 = 115 çıkar; doğrusu (70 + 90) / 2 = 80'dir. Program hata vermez. Bu yüzden böyle hatalar yalnızca test edilerek bulunur. Önceliğinden emin olunmayan her yerde parantez kullanmak hem hatayı önler hem de ifadeyi okuyana niyeti açıkça gösterir.
4.6.3Karar ve döngü
Bir program yalnızca yukarıdan aşağı sırayla çalışsaydı, her girdi için aynı adımları yapardı. Gerçek problemler ise koşullara göre farklı davranmayı ve aynı işi tekrar tekrar yapmayı gerektirir. Bunu sağlayan iki yapı karar ve döngüdür (Şekil 4.25).
Karar yapısı (selection), bir koşulun doğru ya da yanlış olmasına göre farklı işlem yollarından birini seçer. Döngü (loop), bir işlem grubunu bir koşul doğru olduğu sürece tekrarlar. Tekrarlanan işlem grubuna döngünün gövdesi (body), gövdenin her çalışmasına bir tur (iteration) denir.
Karar yapısında her çalıştırmada iki yoldan yalnızca biri izlenir. Alan algoritmasında koşul doğruysa alan hesaplanır, yanlışsa uyarı yazılır; ikisi aynı çalıştırmada birlikte olmaz. Döngüde ise akış, gövdenin sonunda koşula geri döner. Koşul her turun başında yeniden denetlenir. Koşul yanlış olunca döngüden çıkılır ve döngüden sonraki satıra geçilir.
Bir döngünün bitebilmesi için gövdenin içinde koşulu etkileyen bir değişiklik olmalıdır. Koşul i ≤ 3 ise gövde i'yi bir gün 3'ün üstüne çıkarmalıdır. Böyle bir değişiklik yoksa koşul hep doğru kalır ve döngü hiç bitmez.
4.6.4Döngü izleme: sonsuz döngü
Aşağıdaki sözde kod 1'den 3'e kadar sayıları yazmak için yazılmıştır. Programcı ilk sürümde 4. satırı unutmuştur:
i ← 1
i ≤ 3 iken tekrarla
yaz i
i ← i + 1 // ilk sürümde yoktu
yaz "bitti"Sonsuz döngü (infinite loop), koşulu hiçbir zaman yanlış olmadığı için hiç bitmeyen döngüdür.
Tablo 4.14 ilk sürümün ilk birkaç adımını izler. i ← 1 ile döngü değişkeni hazırlanır. Koşul 1 ≤ 3 doğrudur, gövdeye girilir ve 1 yazılır. Gövdede başka satır olmadığı için koşula dönülür. i hâlâ 1'dir, koşul yine doğrudur. Durum bir önceki turla birebir aynıdır.
| Adım | Satır | i | Koşul | Gövde çalışma sayısı | Çıktı |
|---|---|---|---|---|---|
| 1 | i ← 1 | 1 | — | 0 | |
| 2 | i ≤ 3 iken | 1 | 1 ≤ 3 → doğru | 0 | |
| 3 | yaz i | 1 | — | 1 | 1 |
| 4 | i ≤ 3 iken | 1 | 1 ≤ 3 → doğru | 1 | |
| 5 | yaz i | 1 | — | 2 | 1 |
| … | … | 1 | hep doğru | sınırsız artar | 1, 1, 1, … |
Koşulu yanlış yapacak bir değişiklik olmadığı için döngü hiç bitmez (Şekil 4.26). Ekran 1'lerle dolar. Konsolda çalışan bir program ancak Ctrl+C tuşlarıyla durdurulabilir.
Düzeltilmiş sürümde gövdeye i ← i + 1 satırı eklenir. Tablo 4.15 bu sürümü baştan sona izler.
| Adım | Koşul denetimi | Gövde | i (turdan sonra) | Gövde çalışma sayısı |
|---|---|---|---|---|
| Başlangıç | — | i ← 1 | 1 | 0 |
| 1. tur | 1 ≤ 3 → doğru | 1 yazılır, i ← 2 | 2 | 1 |
| 2. tur | 2 ≤ 3 → doğru | 2 yazılır, i ← 3 | 3 | 2 |
| 3. tur | 3 ≤ 3 → doğru (eşitlik) | 3 yazılır, i ← 4 | 4 | 3 |
| Çıkış | 4 ≤ 3 → yanlış | Döngüden çıkılır; “bitti” yazılır | 4 | 3 |
Döngü değişkeni artık her turda ilerler. Üçüncü turda koşul 3 ≤ 3 yine doğrudur, çünkü ≤ eşitliği de kapsar. Bu ayrıntı atlanırsa sayıların yalnızca 1 ve 2'ye kadar yazılacağı sanılabilir. Döngü bittiğinde gövde 3 kez çalışmış, koşul ise 4 kez denetlenmiştir: üç doğru ve bir yanlış. Bir döngüde koşul her zaman gövdeden bir fazla denetlenir. Son denetim döngüyü bitiren denetimdir. Java karşılığı ve çıktısı:
public class Dongu {
public static void main(String[] args) {
int i = 1;
while (i <= 3) {
System.out.println(i);
i = i + 1;
}
System.out.println("bitti");
}
}1
2
3
bittiAşağıdaki döngü kaç tur döner, ne yazar? k ← 1; k < 20 iken tekrarla: yaz k; k ← k × 3. Gövdedeki k ← k × 3 satırı unutulsaydı ne olurdu?
- 1. tur: 1 < 20 doğru; 1 yazılır, k = 3.
- 2. tur: 3 < 20 doğru; 3 yazılır, k = 9.
- 3. tur: 9 < 20 doğru; 9 yazılır, k = 27.
- 27 < 20 yanlış; döngü biter. Üç tur döner ve ekrana 1, 3, 9 yazılır; koşul 4 kez denetlenir.
- Gövdedeki çarpma satırı unutulsaydı k hep 1 kalırdı. Koşul hep doğru olacağı için döngü sonsuza kadar 1 yazardı.
Döngü değişkenini yanlış yönde değiştirmek. i ← 1; i ≤ 3 iken tekrarla: yaz i; i ← i − 1 döngüsünde gövde i'yi değiştirir, ama koşulu yanlış yapacak yönde değil. i 1, 0, −1, −2 diye azalır ve hep 3'ten küçük kalır. Gövdede bir değişiklik olması yetmez; değişiklik koşulu bir gün yanlış yapmalıdır.
4.6.5Fonksiyonlar ve diziler
Programlar büyüdükçe iki ihtiyaç ortaya çıkar. Birincisi, aynı işin programın birçok yerinde yapılmasıdır; dikdörtgen alanı belki on yerde hesaplanır. İkincisi, çok sayıda benzer değerin saklanmasıdır; bir sınıfın 40 öğrencisinin notları için 40 ayrı değişken tanımlamak anlamsızdır. Fonksiyon birinci ihtiyacı, dizi ikincisini karşılar (Şekil 4.27).
Fonksiyon (function), belirli bir görevi yapan ve bir adla çağrılan kod bloğudur. Girdi olarak parametre (parameter) alabilir ve bir sonuç döndürebilir (return). Fonksiyonu çağırırken verilen değerlere argüman (argument) denir. Dizi (array), aynı tipteki çok sayıda değeri tek bir ad altında, sıra numarasıyla saklayan yapıdır. Bir öğenin sıra numarasına indis (index) denir.
Fonksiyon bir görevi adlandırır. kare(x) fonksiyonu “bir sayının karesini al” görevinin adıdır. Fonksiyonu kullanan kod, karenin nasıl hesaplandığıyla ilgilenmez; yalnızca kare(4) yazar ve 16 alır. Formül tek bir yerde durur. Formülde bir hata varsa bir kez düzeltilir ve düzeltme fonksiyonun çağrıldığı her yere yansır.
Dizide değerler sıra numaralarıyla seçilir. Java, C ve Python'da sıra numarası 0'dan başlar. Dört öğeli notlar dizisinde ilk öğe notlar[0] = 85, son öğe notlar[3] = 64'tür. Son indis her zaman uzunluk − 1'dir. Bu kural ilk bakışta tuhaf gelse de bir mantığı vardır: indis, öğenin dizinin başından kaç adım uzakta olduğunu söyler; ilk öğe başlangıçtan sıfır adım uzaktadır.
Bu iki yapı programı küçük, ayrı ayrı sınanabilir parçalara böler. Not ortalaması hesaplayan bir program, notları okuyan, ortalamayı hesaplayan ve harf notunu bulan üç fonksiyona ayrılabilir; notlar da tek bir dizide tutulur.
4.6.6Fonksiyon çağrısını izleme
Aşağıdaki sözde kod, alan fonksiyonunu tanımlar ve iki kez çağırır:
fonksiyon alan(en, boy)
sonuc ← en × boy
döndür sonuc
son
a ← alan(3, 4)
b ← alan(2, 5)
yaz a + bFonksiyon çağrıları bellekte çağrı yığını (call stack) adı verilen bir yapıyla izlenir. Her çağrı için yığının üstüne yeni bir çerçeve (stack frame) eklenir. Çerçevede fonksiyonun parametreleri ve kendi değişkenleri durur. Fonksiyon bitince çerçevesi yığından kaldırılır. Yığının en üstündeki çerçeve, o anda çalışan fonksiyonundur. Tablo 4.16 programı adım adım izler.
| Adım | Olay | Ana program | alan çerçevesi |
|---|---|---|---|
| 1 | Tanım (satır 1–4) kaydedilir, çalışmaz; program 5. satırdan başlar | a = —, b = — | yok |
| 2 | alan(3, 4) çağrılır; yeni çerçeve, 3 → en, 4 → boy | a = —, b = — | en = 3, boy = 4 |
| 3 | Gövde: sonuc ← 3 × 4 = 12 | a = —, b = — | en = 3, boy = 4, sonuc = 12 |
| 4 | döndür sonuc: 12 çağıran yere gönderilir | a = —, b = — | kaldırılacak |
| 5 | Çerçeve kalkar; dönen değer a'ya atanır | a = 12, b = — | yok |
| 6 | alan(2, 5) çağrılır; yepyeni bir çerçeve | a = 12, b = — | en = 2, boy = 5 |
| 7 | Gövde: sonuc ← 2 × 5 = 10 | a = 12, b = — | en = 2, boy = 5, sonuc = 10 |
| 8 | 10 döndürülür, çerçeve kapanır | a = 12, b = — | kaldırılacak |
| 9 | Dönen değer b'ye atanır | a = 12, b = 10 | yok |
| 10 | yaz a + b → 22 | a = 12, b = 10 | yok |
İzlemeden üç sonuç çıkar. Birincisi, fonksiyon tanımı tek başına hiçbir şey yapmaz; ancak çağrıldığında çalışır. İkincisi, her çağrı kendi çerçevesini açar. Birinci çağrının sonuc = 12 değeri ikinci çağrının çerçevesinde görünmez, çünkü birinci çerçeve çoktan kaldırılmıştır. Fonksiyonun içindeki değişkenler yalnızca o çağrı sürdükçe yaşar. Üçüncüsü, ana program fonksiyonun içini görmez; yalnızca döndürülen değeri alır. Bir kez yazılan fonksiyon iki kez kullanılmış, alan formülü tek bir yerde kalmıştır (Şekil 4.28).
Java karşılığında fonksiyonlara metot denir. static int alan(int en, int boy) başlığı, metodun iki tam sayı aldığını ve bir tam sayı döndürdüğünü söyler:
public class AlanFonksiyon {
static int alan(int en, int boy) {
int sonuc = en * boy;
return sonuc;
}
public static void main(String[] args) {
int a = alan(3, 4);
int b = alan(2, 5);
System.out.println(a + b);
}
}22fonksiyon cevre(en, boy): döndür 2 × (en + boy) tanımlı. Ana program c ← cevre(3, 4) + cevre(1, 1) satırını çalıştırıyor. Kaç çerçeve açılır, c kaç olur?
- Önce
cevre(3, 4)çağrılır: çerçeve açılır, en = 3, boy = 4. 2 × (3 + 4) = 14 döndürülür, çerçeve kapanır. - Sonra
cevre(1, 1)çağrılır: yeni bir çerçeve, en = 1, boy = 1. 2 × (1 + 1) = 4 döndürülür, çerçeve kapanır. - İki dönen değer toplanır: c = 14 + 4 = 18. Toplam iki çerçeve açılmıştır; ikisi aynı anda yığında bulunmaz.
Fonksiyonun içinde tanımlanan bir değişkeni fonksiyonun dışında kullanmaya çalışmak. alan fonksiyonundaki sonuc değişkeni yalnızca fonksiyonun çerçevesinde vardır; ana programda yaz sonuc yazmak Java'da “cannot find symbol” derleme hatası verir. Fonksiyonun ürettiği değer dışarıya yalnızca döndür ile çıkar.
4.6.7Dizi ile ortalama
Aşağıdaki sözde kod dört notun ortalamasını bir dizi ve bir döngüyle hesaplar:
notlar ← [85, 72, 90, 64]
toplam ← 0
her i ← 0, 1, 2, 3 için
toplam ← toplam + notlar[i]
ort ← toplam / 4
yaz "Ortalama: ", ortToplamı tutan değişkene biriktirici denir. Biriktirici döngüden önce 0 ile başlatılır, her turda bir değer daha eklenir. Başlangıç değeri verilmeseydi toplamın ilk değeri belirsiz olurdu. Tablo 4.17 döngünün her turunu izler.
| Adım | i | notlar[i] | İşlem | toplam |
|---|---|---|---|---|
| Başlangıç | — | — | toplam ← 0 | 0 |
| 1. tur | 0 | 85 | 0 + 85 | 85 |
| 2. tur | 1 | 72 | 85 + 72 | 157 |
| 3. tur | 2 | 90 | 157 + 90 | 247 |
| 4. tur | 3 | 64 | 247 + 64 | 311 |
| Döngüden sonra | — | — | ort ← 311 / 4 | 311 (ort = 77,75) |
Döngü dört tur sonra biter. Son indis 3'tür, yani uzunluk − 1. Sonuç “Ortalama: 77.75” olarak yazılır (Şekil 4.29). Java programında döngü sınırı sabit 4 yerine dizinin uzunluğu olan notlar.length ile yazılmıştır. Böylece diziye not eklenince döngü kendiliğinden uyum sağlar. Bölme de 4.0 ile yapılır ki sonuç tam sayıya yuvarlanmasın:
public class Ortalama {
public static void main(String[] args) {
int[] notlar = {85, 72, 90, 64};
int toplam = 0;
for (int i = 0; i < notlar.length; i++) {
toplam = toplam + notlar[i];
}
double ort = toplam / 4.0;
System.out.println("Ortalama: " + ort);
}
}Ortalama: 77.75Python'da aynı hesap daha kısadır. sum ve len hazır fonksiyonları döngüyü programcıdan gizler:
notlar = [85, 72, 90, 64]
toplam = 0
for i in range(4):
toplam = toplam + notlar[i]
ort = toplam / 4
print("Ortalama:", ort)
print(sum(notlar) / len(notlar))Ortalama: 77.75
77.75Döngü bir tur fazla dönseydi ne olurdu? Dört öğeli dizide 4 numaralı hücre yoktur. Java bu durumu bir çalışma zamanı hatasıyla bildirir:
public class DiziDisi {
public static void main(String[] args) {
int[] notlar = {85, 72, 90, 64};
System.out.println(notlar[3]);
System.out.println(notlar[4]);
}
}64
Exception in thread "main" java.lang.ArrayIndexOutOfBoundsException: Index 4 out of bounds for length 4
at DiziDisi.main(DiziDisi.java:5)notlar[3] sorunsuz okunur ve 64 yazılır. notlar[4] istendiğinde JVM programı durdurur. İletideki “Index 4 out of bounds for length 4” ifadesi “uzunluğu 4 olan dizide 4 numaralı indis sınırların dışında” demektir. Döngü sınırının i <= notlar.length diye yazılması bu hatanın en sık nedenidir; doğrusu i < notlar.length'tir.
sıcaklık = [18, 21, 25, 19, 22] dizisinde 20'nin üzerindeki günler sayılacak. Bir sayaç ve bir döngüyle izleme nasıl yapılır? Kaç gün bulunur?
sayac ← 0. i = 0: sıcaklık[0] = 18; 18 > 20 yanlış, sayaç 0.- i = 1: 21 > 20 doğru, sayaç 1.
- i = 2: 25 > 20 doğru, sayaç 2.
- i = 3: 19 > 20 yanlış, sayaç 2.
- i = 4: 22 > 20 doğru, sayaç 3. Son indis 5 − 1 = 4'tür; döngü biter. Sonuç 3 gündür.
Bu örnekte döngünün içinde bir karar yapısı vardır. Döngü her öğeyi ziyaret eder, karar ise hangi öğenin sayılacağını seçer.
4.6.8Kütüphaneler ve API
Karekök almak, bir listeyi sıralamak ya da bir dosyayı okumak her programcının sık ihtiyaç duyduğu işlerdir. Her programcının bunları sıfırdan yazması hem zaman kaybı hem de hata kaynağı olurdu. Bu yüzden diller hazır, test edilmiş fonksiyonlarla birlikte gelir.
Kütüphane (library), başkalarının yazıp test ettiği hazır fonksiyonların topluluğudur. API (application programming interface, uygulama programlama arayüzü), bir kütüphanenin ya da bir hizmetin dışarıya sunduğu fonksiyonları, bu fonksiyonların girdilerini ve dönüş değerlerini tanımlar.
API bir sözleşme gibidir (Şekil 4.30). Karekök fonksiyonunun API'si üç şey söyler: fonksiyonun adı, bir sayı aldığı ve o sayının karekökünü döndürdüğü. Fonksiyonun içinde hangi yöntemin kullanıldığı, kayan nokta ayrıntılarının nasıl ele alındığı kullanıcıyı ilgilendirmez. Kütüphaneyi yazanlar içini daha hızlı bir yöntemle değiştirse bile, API aynı kaldıkça onu kullanan programlar değişmeden çalışmaya devam eder.
| Çağrı | Dil | Sonuç |
|---|---|---|
Math.sqrt(16) | Java | 4.0 |
Math.max(3, 8) | Java | 8 |
len("Giresun") | Python | 7 |
Tablodaki sonuçlar gerçek çalıştırmalardan alınmıştır. Java'da Math.sqrt sonucu her zaman double döndürür; bu yüzden 4 değil, 4.0 yazılır. Java'da bir dizginin uzunluğu bir fonksiyonla değil, dizginin kendi metoduyla bulunur: "Giresun".length().
public class Kutuphane {
public static void main(String[] args) {
System.out.println(Math.sqrt(16));
System.out.println(Math.max(3, 8));
System.out.println("Giresun".length());
}
}4.0
8
7import math
print(math.sqrt(16))
print(max(3, 8))
print(len("Giresun"))4.0
8
7API kavramı kütüphanelerle sınırlı değildir. Bir hava durumu uygulaması, bir meteoroloji hizmetinin ağ üzerinden sunduğu API'yi çağırır: şehir adını gönderir, sıcaklığı alır. Uygulamayı yazanlar o hizmetin sunucularında ne olduğunu bilmez, yalnızca API'nin belgesini okur.
Bir kütüphane fonksiyonunun dönüş tipini bilmeden kullanmak. Java'da Math.sqrt(16) 4.0 döndürür; sonucu int k = Math.sqrt(16); diye bir tam sayıya atamak derleme hatası verir, çünkü double değer int'e kendiliğinden dönüştürülmez. API belgesi her fonksiyonun ne aldığını ve ne döndürdüğünü yazar; kullanmadan önce bakmak gerekir.
4.7Hata ayıklama
Her programcı hata yapar; deneyimli programcıyı ayıran, hatayı bulmanın düzenli bir yolunu bilmesidir. Bir programdaki hatayı bulup giderme işine hata ayıklama (debugging) denir.
4.7.1Hata ayıklama teknikleri
Hata ayıklama bir tahmin oyunu değil, adım adım yürüyen bir araştırmadır. Aşağıdaki teknikler bu araştırmanın araçlarıdır.
- Hatayı yeniden üret. Aynı girdiyle hata her seferinde çıkıyor mu? Hatayı her istendiğinde ortaya çıkaran girdi bulunmadan aramaya başlamak boşa çabadır. Bu girdi, düzeltmeden sonra hatanın gerçekten gittiğini sınamak için de gerekir.
- Ara değerleri yazdır. Şüpheli satırdan önce ve sonra değişkenlerin değerleri ekrana yazdırılır. Beklenen ile gerçek değerin ilk ayrıldığı yer, hatanın yeridir.
- Kesme noktası koy. Hata ayıklayıcıda bir satıra kesme noktası (breakpoint) konur. Program o satıra gelince durur ve o andaki bütün değişkenler incelenir.
- Adım adım ilerle. Program durduktan sonra satır satır ilerletilir. Her satırdan sonra hangi değerin değiştiğine bakılır.
- Alanı daralt. Kodun yarısı geçici olarak devre dışı bırakılır ve hatanın hangi yarıda olduğu bulunur. Sonra o yarı yeniden ikiye bölünür. Yüz satırlık bir kodda hata yedi bölmede tek satıra iner, çünkü 27 = 128 ≥ 100.
- Anlatarak düşün. Kodu satır satır birine, hatta masadaki bir oyuncağa anlatmak çoğu zaman hatayı gösterir. Anlatırken “bu satır şunu yapıyor” denir ve bir anda satırın aslında başka bir şey yaptığı fark edilir.
Şekil 4.31 bir hata ayıklayıcının görünümünü taklit eder. Program 4. satırdaki kesme noktasında durmuştur ve henüz o satırı çalıştırmamıştır. Değişkenler panelinde üç not görünür. Satır ort ← n1 + n2 + n3 / 3'tür. “Adım” düğmesine basıldığında önce n3 / 3 = 30 hesaplanır, sonra 70 + 85 + 30 = 185. Ortalama 185 çıkar; beklenen değer ise 245 / 3 ≈ 81,67'dir. Hata ayıklayıcı, sorunun toplama değil bölme sırasında olduğunu bir bakışta gösterir.
Hatanın nedenini bulmadan belirtisini gizlemek. Ortalama 185 çıkıyor diye sonucu 2,27'ye bölmek bu girdide 81,5 civarı bir değer verir ama başka her girdide yanlış sonuç üretir. Düzeltme, hatanın temel nedenine yapılır: eksik parantez eklenir.
4.7.2İzleyerek hata bulma
Aşağıdaki sözde kod üç sayının en büyüğünü bulmak için yazılmıştır. eb değişkeni “en büyük” değerini tutar:
oku a, b, c
eb ← 0
eğer a > eb ise eb ← a
eğer b > eb ise eb ← b
eğer c > eb ise eb ← c
yaz "En büyük: ", ebİlk test için 7, 12 ve 5 seçilir; beklenen çıktı 12'dir. Tablo 4.19 programı izler.
| Satır | Koşul | eb |
|---|---|---|
eb ← 0 | — | 0 |
eğer a > eb | 7 > 0 → doğru | 7 |
eğer b > eb | 12 > 7 → doğru | 12 |
eğer c > eb | 5 > 12 → yanlış | 12 |
yaz | — | Çıktı: En büyük: 12 ✓ |
Çıktı beklenenle aynıdır. Ama tek bir başarılı test programın doğru olduğunu kanıtlamaz. İkinci test bir sınır durumunu dener: sayıların hepsi negatif olsun, −5, −2 ve −9. Beklenen çıktı −2'dir (Tablo 4.20).
| Satır | Koşul | eb |
|---|---|---|
eb ← 0 | — | 0 |
eğer a > eb | −5 > 0 → yanlış | 0 |
eğer b > eb | −2 > 0 → yanlış | 0 |
eğer c > eb | −9 > 0 → yanlış | 0 |
yaz | — | Çıktı: En büyük: 0 ✗ |
Üç karşılaştırmanın üçü de yanlıştır, çünkü hiçbir girdi 0'dan büyük değildir; eb hiç değişmez ve çıktı 0 olur (Şekil 4.32). Girdilerin hiçbiri 0 değildir. İzleme suçluyu gösterir: 2. satırdaki başlangıç değeri girdilerin dışından gelir. 0, “hiçbir sayıdan küçük olmayan” bir başlangıç değeri değildir; yalnızca pozitif sayılar için işe yarar.
Düzeltme, başlangıç değerini girdilerin birinden almaktır: eb ← a. eb artık bir girdiyle başladığı için sonuç her zaman girdilerden biri olur. a'nın kendisiyle karşılaştırılmasına da gerek kalmaz. Düzeltilmiş program Test 2 ile yeniden izlenir: eb = −5 ile başlar; −2 > −5 doğru, eb = −2; −9 > −2 yanlış. Çıktı −2'dir ✓. Bir düzeltmeden sonra önceki testlerin de yinelenmesi gerekir, çünkü düzeltme daha önce çalışan bir durumu bozmuş olabilir. Test 1 yeniden denenir ve yine 12 çıkar.
Aynı programın hatalı ve düzeltilmiş Java sürümleri aynı girdilerle çalıştırılmıştır:
import java.util.Scanner;
public class EnBuyukHatali {
public static void main(String[] args) {
Scanner giris = new Scanner(System.in);
int a = giris.nextInt(), b = giris.nextInt(), c = giris.nextInt();
int eb = 0;
if (a > eb) eb = a;
if (b > eb) eb = b;
if (c > eb) eb = c;
System.out.println("En büyük: " + eb);
}
}Girdi 7 12 5 ve −5 −2 −9 için çıktılar:
En büyük: 12En büyük: 0import java.util.Scanner;
public class EnBuyuk {
public static void main(String[] args) {
Scanner giris = new Scanner(System.in);
int a = giris.nextInt(), b = giris.nextInt(), c = giris.nextInt();
int eb = a;
if (b > eb) eb = b;
if (c > eb) eb = c;
System.out.println("En büyük: " + eb);
}
}Düzeltilmiş sürüm aynı iki girdi ve eşit değerli bir girdi (3, 3, 1) için:
En büyük: 12En büyük: -2En büyük: 3Düzeltilmiş en büyük programı için hangi test girdileri seçilmelidir? Her biri neyi sınar?
- En büyük sayının üç konumda da bulunduğu girdiler: (12, 7, 5), (7, 12, 5), (7, 5, 12). Bir karşılaştırma satırındaki yazım hatası ancak en büyük o konumdayken ortaya çıkar.
- Hepsi negatif: (−5, −2, −9). Başlangıç değeri hatasını yakalar.
- Eşit değerler: (3, 3, 1) ve (4, 4, 4).
>yerine>=yazılması sonucu değiştirmez ama eşitlik durumunun da denendiğini gösterir. - Sıfır içeren girdi: (0, −1, −3). Beklenen 0'dır; hatalı sürüm de 0 verir. Bu test tek başına hatalı sürümü yakalayamaz, yanlış başlangıç değerinin şans eseri doğru sonuç verdiği bir durumdur.
4.7.3Sık yapılan hatalar
Bazı hatalar o kadar sık yapılır ki adları vardır. Tablo 4.21 bunları örnekleri ve sonuçlarıyla listeler.
| Hata | Örnek | Ne olur? |
|---|---|---|
| Başlangıç değeri yok ya da yanlış | toplam başlatılmadan toplam ← toplam + x | Java derlemez; bazı dillerde rastgele bir değerle hesaplar |
| Sınır hatası (bir eksik / bir fazla) | her i ← 1 … n − 1 için | Son öğe atlanır ya da bir fazla tur döner |
| Sonsuz döngü | Döngü değişkeni gövdede güncellenmez | Program bitmez |
| Sıfıra bölme | ort ← toplam / n, n = 0 | Çalışma zamanı hatası |
| Dizinin dışına çıkmak | 4 öğeli dizide notlar[4] | Çalışma zamanı hatası |
| Parantez unutmak | ort ← a + b / 2 | Yanlış sonuç (mantık hatası) |
| Girdiyi doğrulamamak | Yaş olarak −3 kabul etmek | Anlamsız sonuç, sonraki adımlarda hata |
Başlangıç değeri hatasında diller farklı davranır. Java, değer verilmemiş bir yerel değişkenin okunmasına izin vermez:
public class Baslatilmamis {
public static void main(String[] args) {
int toplam;
toplam = toplam + 5;
System.out.println(toplam);
}
}Baslatilmamis.java:4: error: variable toplam might not have been initialized
toplam = toplam + 5;
^
1 errorC'de ise aynı kod derlenir. Değer verilmemiş yerel değişken, bellekte o anda ne varsa onu tutar ve program her çalıştırmada farklı bir sonuç verebilir. Java'nın katılığı burada bir korumadır: hatayı çalışma zamanından derleme zamanına taşır.
Sınır hatası (off-by-one error) en sinsi hatalardandır. 1'den n'ye kadar toplam isterken her i ← 1 … n − 1 için yazılırsa n atlanır. n = 5 için toplam 15 yerine 1 + 2 + 3 + 4 = 10 çıkar. Program çalışır, sonuç “makul” görünür ve hata kolayca gözden kaçar.
Bu hataların çoğu sınır durumlarıyla (edge cases) ortaya çıkar: 0, boş liste, en küçük ve en büyük değer, negatif sayı. Normal girdilerle çalışan bir program sınırlarda çoğu zaman bozulur. Bu yüzden her test kümesinde birkaç sınır durumu bulunmalıdır.
Bir program, girilen notların ortalamasını ort ← toplam / n ile hesaplıyor. Hangi sınır durumları denenmelidir, her biri hangi hatayı ortaya çıkarabilir?
- Hiç not girilmemesi (n = 0): sıfıra bölme. Program, n = 0 iken “not girilmedi” yazacak biçimde düzeltilir.
- Tek not (n = 1): ortalama notun kendisi olmalıdır. Döngü sınırında bir eksik varsa sonuç 0 çıkar.
- En küçük ve en büyük notlar (0 ve 100): girdi denetimi bu değerleri yanlışlıkla reddetmemelidir; örneğin
not > 0yerinenot ≥ 0yazılmalıdır. - Geçersiz not (−5 ya da 120): program bu değeri kabul etmemelidir.
- Kesirli sonuç (70, 85, 90): ortalama 81,67 civarında olmalıdır. Tam sayı bölmesi varsa 81 çıkar.
4.7.4Yazılım hatalarının bedeli
Bir ödevdeki hata düşük bir notla sonuçlanır. Gerçek sistemlerdeki hataların bedeli ise milyonlarca dolar, hatta insan hayatı olabilir. Üç ünlü örnek, bu notta anlatılan hata türlerinin gerçek dünyadaki karşılığını gösterir.
Ariane 5 (1996). Avrupa Uzay Ajansı'nın yeni roketi Ariane 5, ilk uçuşunda kalkıştan yaklaşık 37 saniye sonra rotasından saptı ve birkaç saniye içinde parçalandı. Soruşturma kurulunun raporuna göre neden bir sayı dönüşümüydü. Seyrüsefer yazılımı, 64 bitlik kayan noktalı bir yatay hız değerini 16 bitlik işaretli bir tam sayıya dönüştürüyordu. 16 bitlik işaretli bir tam sayı en fazla 32.767 değerini tutabilir. Yazılım daha yavaş tırmanan Ariane 4'ten alınmıştı ve orada bu sınır hiç aşılmamıştı. Ariane 5'in yatay hızı çok daha büyüktü, dönüşüm taşma hatası verdi ve seyrüsefer sistemi durdu.
Java'da 40000.0 değerindeki bir double, 16 bitlik short tipine zorla dönüştürülürse ne olur?
public class Tasma {
public static void main(String[] args) {
double hiz = 40000.0;
short kisa = (short) hiz;
System.out.println(kisa);
System.out.println(Short.MAX_VALUE);
}
}-25536
32767short tipinin en büyük değeri 32.767'dir. 40.000 bu sınırı aşar. Java hata vermez; sayının ikili gösteriminin yalnızca son 16 bitini tutar. 16 bit 216 = 65.536 farklı değer tutabildiği için sonuç 40.000 − 65.536 = −25.536 olur. Pozitif bir hız sessizce negatif bir sayıya dönüşmüştür. Ariane 5'in yazılımı Ada diliyle yazılmıştı ve taşmada sessiz kalmak yerine bir hata verdi. Ancak o hatayı yakalayıp işleyen kod yoktu ve sistem kapandı. İki durumda da kök neden aynıdır: değerin, tipin alabileceği aralığın dışına çıkabileceği hiç denenmemiştir.
Mars Climate Orbiter (1999). NASA'nın Mars'a gönderdiği sonda, yörüngeye girerken gezegene fazla yaklaştı ve kayboldu. Yer istasyonundaki bir yazılım iticilerin etkisini İngiliz birimiyle (pound-kuvvet·saniye) üretiyordu. Bu veriyi kullanan öbür yazılım ise metrik birim (newton·saniye) bekliyordu. 1 pound-kuvvet·saniye yaklaşık 4,45 newton·saniyedir. Bu yüzden itmenin etkisi yaklaşık 4,45 kat küçük hesaplandı ve yörünge hesabı dokuz aydan uzun süren yolculuk boyunca giderek kaydı. İki programın her biri tek başına doğru çalışıyordu; hata aralarındaki arayüzdeydi.
Therac-25 (1985–1987). Kanser tedavisinde kullanılan bir radyoterapi cihazının yazılımında bir zamanlama hatası vardı. Operatör ayarları belirli bir sırayla ve çok hızlı girdiğinde güvenlik denetimi atlanıyordu. Önceki modellerde bu denetimi donanım kilitleri de yapıyordu; Therac-25'te bu iş yalnızca yazılıma bırakılmıştı. 1985–1987 arasındaki altı kazada hastalar normalin çok üstünde radyasyon dozu aldı; en az üç hasta hayatını kaybetti.
Üç hata da derleyicinin yakalayamayacağı türdendir. Biri bir sınır durumu, biri birim tutarsızlığı, biri zamanlamaya bağlı bir mantık hatasıdır. Ortak ders birkaç maddede toplanır. Sınır durumları denenmeli, birimler değişken adlarında ve belgelerde açıkça yazılmalıdır. Bir sistemden alınan yazılım, yeni sistemin koşullarıyla yeniden sınanmalı ve kod bir başkasına incelettirilmelidir.
Birimi değişken adında belirtmemek. hiz adlı bir değişkenin km/sa mı m/s mi olduğu koddan anlaşılmaz. hizKmSa ya da hizMs gibi adlar, iki programcının aynı sayıyı farklı birimlerde yorumlamasını önler. Mars Climate Orbiter'ın kaybı, bu küçük alışkanlığın değerini gösterir.
4.7.5Okunabilir kod
Kod bir kez yazılır, defalarca okunur. Onu haftalar sonra yazanın kendisi, ekip arkadaşları ve bakımı devralan başkaları okur. Okunabilir kod hataları hem önler hem de bulunmalarını kolaylaştırır. Aşağıdaki iki sözde kod aynı işi yapar.
Çalışır ama okunmaz:
a ← oku()
b ← oku()
c ← a*b/2
yaz cAynı program, okunur:
// üçgenin alanı = taban × yükseklik / 2
taban ← oku()
yukseklik ← oku()
alan ← taban × yukseklik / 2
yaz "Alan: ", alanİlk programı okuyan, c'nin ne olduğunu anlamak için formülü çözmek zorundadır; a*b/2 bir üçgen alanı olabilir, bir ortalama hesabının yanlış yazılmış hâli de olabilir. İkinci program kendini anlatır. Aradaki farklar şunlardır:
- Anlamlı adlar. c yerine alan, a yerine taban yazılır. Okuyan kişi değişkenin rolünü adından anlar.
- Tutarlı girinti ve boşluk. İşlemlerin çevresindeki boşluklar ve iç içe blokların düzenli girintisi, yapının sınırını bir bakışta gösterir.
- Sihirli sayı yerine adlandırılmış sabit. Kodun içine gömülü, anlamı açıklanmamış sayıya sihirli sayı (magic number) denir.
fiyat * 0.20yerineKDV_ORANI ← 0.20tanımlanır vefiyat * KDV_ORANIyazılır. Oran değiştiğinde tek bir satır düzeltilir. - Tekrarı fonksiyona taşımak. Aynı formül üç yerde yazılıysa bir düzeltmenin üç kez yapılması gerekir ve biri mutlaka unutulur.
- Açıklama satırı. Açıklama kodun ne yaptığını değil, neden yaptığını anlatır.
// x'e 1 ekleaçıklaması kodu tekrar eder ve bir şey katmaz.// dizi 0'dan başlıyor, sıra numarası için 1 ekleaçıklaması ise bir niyeti açıklar.
Okunabilir üçgen alanı programının Java karşılığı nedir? Taban 6, yükseklik 5 ve taban 7, yükseklik 3 için çıktılar nelerdir?
import java.util.Scanner;
public class UcgenAlani {
public static void main(String[] args) {
Scanner giris = new Scanner(System.in);
// üçgenin alanı = taban × yükseklik / 2
double taban = giris.nextDouble();
double yukseklik = giris.nextDouble();
double alan = taban * yukseklik / 2;
System.out.println("Alan: " + alan);
}
}Alan: 15.0Alan: 10.56 × 5 / 2 = 15 ve 7 × 3 / 2 = 10,5. Değişkenler double olduğu için ikinci sonuç kesirli kısmıyla doğru çıkar. int kullanılsaydı 21 / 2 tam sayı bölmesi 10 verirdi. Adlandırılmış sabit kullanımı da benzer biçimde yazılır:
public class KDV {
static final double KDV_ORANI = 0.20;
public static void main(String[] args) {
double fiyat = 250.0;
double kdv = fiyat * KDV_ORANI;
System.out.println("KDV: " + kdv);
System.out.println("Toplam: " + (fiyat + kdv));
}
}KDV: 50.0
Toplam: 300.0250 TL'lik bir ürün için KDV 250 × 0,20 = 50 TL, toplam 300 TL'dir. Oran değişirse yalnızca KDV_ORANI satırı değiştirilir.
Kısa adların kodu hızlandırdığını ya da küçülttüğünü düşünmek. Derleyici adları makine koduna taşımaz; c ile ucgeninAlani aynı hızda çalışır. Kısa adların tek etkisi, kodu okuyanın işini zorlaştırmaktır. Döngü sayacı i gibi kısa ve yerleşik kullanımlar bunun istisnasıdır.
4.8Özet
- Programlama dili, algoritmayı bilgisayarın yürütebileceği biçimde ifade eden biçimsel bir dildir. Söz dizimi neyin yazılabileceğini, anlam yazılanın ne yaptığını belirler. Söz dizimi doğru bir program yanlış iş yapabilir.
- Kaynak kod insanın okuduğu metindir; makine dili işlemcinin çalıştırdığı ikili komutlardır; assembly makine komutlarının simgesel yazılışıdır. Yüksek seviyeli bir satır çoğu zaman birkaç makine komutuna karşılık gelir.
- Kaynak kod üç yolla çalıştırılır. Derlenen dillerde (C) program önceden makine koduna çevrilir. Yorumlanan dillerde (Python) komutlar çalışma sırasında değerlendirilir. Hibrit dillerde (Java) kaynak kod bytecode'a derlenir ve bytecode bir sanal makinede (JVM) yürütülür. JIT derleyici sık çalışan kodu çalışma sırasında makine koduna çevirir. Derlenen/yorumlanan ayrımı dilin değil, uygulamanın özelliğidir.
- Hatalar üç türdür. Derleme hatası çeviri sırasında bulunur, program hiç çalışmaz. Çalışma zamanı hatası yürütme sırasında programı durdurur. Mantık hatası hata iletisi vermez, yanlış sonuç üretir ve ancak test ile bulunur.
- Derleyici beş aşamada çalışır: sözcük çözümleme (belirteçler), söz dizimi çözümleme (söz dizimi ağacı), anlam çözümleme (adlar ve tipler), ara kod ve iyileştirme (ör. sabit katlama), kod üretimi. İlk üç aşama hata arar. Bağlayıcı nesne dosyalarını ve kütüphaneleri birleştirir, yükleyici programı belleğe alıp başlatır.
- Diller seviyeye (düşük: makine dili, assembly; yüksek: C, Java, Python), türe (genel amaçlı, alana özgü, betik, işaretleme) ve paradigmaya (buyurgan, nesne yönelimli, fonksiyonel) göre sınıflandırılır. Seviye bir kalite ölçüsü değil, soyutlama düzeyidir. Dil seçimini problem alanı, platform, kütüphaneler, performans, güvenlik ve ekip deneyimi belirler.
- Statik tiplemede (Java) tip hataları derleme sırasında, dinamik tiplemede (Python) ilgili satır çalıştığında bulunur.
- Problemden programa giden yol: problemi tanımla (girdi, çıktı, kısıt), elle örnek çöz, algoritmayı kur, koda dönüştür, test et. Test bir eksik bulursa önceki aşamaya dönülür.
- Yazılım geliştirme süreci gereksinim, tasarım, kodlama, test, dağıtım ve bakımdan oluşan yinelemeli bir süreçtir. Şelale modeli aşamaları bir kez ve sırayla geçer. Çevik yaklaşım kısa döngülerle ilerler. Prototipleme belirsiz gereksinimi görünür kılar. IDE, derleyici, hata ayıklayıcı, sürüm kontrolü, test araçları ve paket yöneticisi bu sürecin araçlarıdır.
- Değişken, karar, döngü, fonksiyon ve dizi temel programlama yapılarıdır. İşlem önceliği matematikteki gibidir. Döngünün bitmesi için gövdenin koşulu etkilemesi gerekir. Her fonksiyon çağrısı çağrı yığınında kendi çerçevesini açar. Dizi indisleri 0'dan başlar, son indis uzunluk − 1'dir.
- Hatalar sınır durumlarıyla (0, boş liste, negatif sayı, en küçük ve en büyük değer) test edilerek ve program izleme tablosuyla adım adım izlenerek bulunur. Okunabilir kod (anlamlı adlar, adlandırılmış sabitler, fonksiyonlar, “neden” anlatan açıklamalar) hataları hem önler hem de bulunmalarını kolaylaştırır.
4.9Kavram listesi
Terimlerin İngilizce karşılıkları ve öteki sunumlardaki kullanımları terim sözlüğünde bulunur.
- programlama dili programming language
- söz dizimi syntax
- anlam semantics
- kaynak kod source code
- makine dili machine language
- makine kodu machine code
- assembly assembly language
- çeviri aracı translator
- derleyici compiler
- derleme compilation
- yorumlayıcı interpreter
- yorumlama interpretation
- derlenen / yorumlanan compiled / interpreted
- bytecode (ara kod) bytecode
- ara kod intermediate code
- sanal makine virtual machine
- Java Sanal Makinesi (JVM) Java Virtual Machine
- JIT (tam zamanında) derleyici just-in-time compiler
- derleme hatası compile error
- söz dizimi hatası syntax error
- çalışma zamanı hatası runtime error
- mantık hatası logic error
- sözcük çözümleme lexical analysis
- belirteç token
- söz dizimi çözümleme parsing
- söz dizimi ağacı syntax tree
- anlam çözümleme semantic analysis
- kod iyileştirme code optimization
- sabit katlama constant folding
- nesne dosyası object file
- bağlayıcı linker
- bağlama hatası linking error
- yükleyici loader
- çalıştırılabilir dosya executable file
- sınıf yükleyici class loader
- yüksek / düşük seviyeli dil high-level / low-level language
- alana özgü dil domain-specific language
- betik dili scripting language
- işaretleme dili markup language
- programlama paradigması programming paradigm
- buyurgan yaklaşım imperative programming
- nesne yönelimli object-oriented
- fonksiyonel functional programming
- statik tipleme static typing
- dinamik tipleme dynamic typing
- şelale modeli waterfall model
- çevik agile
- prototipleme prototyping
- IDE integrated development environment
- hata ayıklayıcı debugger
- kesme noktası breakpoint
- paket yöneticisi package manager
- sürüm kontrolü version control
- kayıt commit
- dal branch
- birleştirme merge
- çakışma merge conflict
- veri tipi data type
- işlem önceliği operator precedence
- sonsuz döngü infinite loop
- parametre parameter
- argüman argument
- çağrı yığını call stack
- dizi array
- indis index
- kütüphane library
- API (uygulama programlama arayüzü) application programming interface
- sınır hatası off-by-one error
- sihirli sayı magic number
4.10Kendini Dene
Aşağıdaki Java hatalarının her biri hangi türdendir? Derleme hatasıysa derleyicinin hangi aşamasında yakalanır? (a)
System.out.println("Merhaba);(b) Bildirilmemiştoplamdeğişkenini kullanmak. (c) n öğeli bir dizidei <= nkoşuluyla dönen döngü. (d) Ortalamayı hesaplarken toplamı n − 1'e bölmek.Cevap
(a) Derleme hatasıdır; kapanmamış dizgi bir belirtece dönüştürülemediği için sözcük çözümleme aşamasında yakalanır (javac: “unclosed string literal”). (b) Derleme hatasıdır; adın bildirilip bildirilmediğine anlam çözümleme aşamasında bakılır (“cannot find symbol”). (c) Çalışma zamanı hatasıdır; son turda i = n olur ve
dizi[n]dizinin dışındadır, JavaArrayIndexOutOfBoundsExceptionverir. (d) Mantık hatasıdır; program her girdiyle çalışır ama ortalamayı olduğundan büyük bulur.Üç satırlık bir Python dosyası şöyle:
print("A"),print(1 / 0),print("B"(son satırda parantez kapanmamış). Dosya çalıştırılınca ekranda ne görünür? Satır satır çalışan basit bir yorumlayıcı ne gösterirdi?Cevap
Python çalıştırmaya başlamadan önce dosyanın tamamını ayrıştırır. Üçüncü satırdaki kapanmamış parantezi bulur ve yalnızca
SyntaxError: '(' was never closediletisini verir. “A” yazılmaz, sıfıra bölmeye hiç ulaşılmaz. Satır satır çalışan basit bir yorumlayıcı ise önce “A” yazar, ikinci satırda sıfıra bölme hatasıyla durur; üçüncü satırdaki söz dizimi hatasını hiç görmez.toplam = toplam + 1;satırı sözcük çözümlemede kaç belirtece ayrılır? Belirteçlerin türleri nelerdir?Cevap
Altı belirteç:
toplam(tanımlayıcı),=(operatör),toplam(tanımlayıcı),+(operatör),1(sabit değer),;(ayırıcı). Boşluklar atılır. Aynı ad iki kez geçtiği için iki ayrı belirteç olarak sayılır.Aşağıdaki sözde kod ne yazar? Gövde kaç kez çalışır, koşul kaç kez denetlenir?
s ← 0;i ← 1;i ≤ 4 iken tekrarla: s ← s + i × i; i ← i + 2;yaz s.Cevap
1. tur: 1 ≤ 4 doğru; s = 0 + 1 × 1 = 1, i = 3. 2. tur: 3 ≤ 4 doğru; s = 1 + 3 × 3 = 10, i = 5. Denetim: 5 ≤ 4 yanlış, döngü biter. Ekrana 10 yazılır. Gövde 2 kez çalışır, koşul 3 kez denetlenir. Çarpma toplamadan önce yapıldığı için
s + i × iifadesinde parantez gerekmez.fonksiyon f(x): döndür 2 × x + 1tanımlı.yaz f(f(2))ne yazar? Kaç çerçeve açılır, ikisi aynı anda yığında bulunur mu?Cevap
Önce içteki çağrı çalışır: f(2) için bir çerçeve açılır, x = 2, 2 × 2 + 1 = 5 döndürülür ve çerçeve kapanır. Sonra dıştaki çağrı f(5) için yeni bir çerçeve açılır: 2 × 5 + 1 = 11. Ekrana 11 yazılır. İki çerçeve açılır ama aynı anda yığında bulunmaz: dıştaki çağrı, argümanı hesaplanıp içteki çerçeve kapandıktan sonra başlar.
Java'da
int s = 17 % 5 * 2 + 9 / 2;satırından sonra s kaçtır? Hesaplama sırasını yazın.Cevap
%,*ve/aynı öncelik düzeyindedir ve toplamadan önce, soldan sağa yapılır. 17 % 5 = 2, 2 × 2 = 4. Ardından 9 / 2 iki tam sayının bölümü olduğu için 4'tür (4,5 değil). Son olarak 4 + 4 = 8. s = 8 olur.Bir Python fonksiyonu x 10'dan büyükse
"a" + 5döndürüyor, değilse"küçük". Programmesaj(3)ve ardındanmesaj(20)çağrılarını yapıyor. Hata ne zaman ortaya çıkar? Aynı mantık Java'da yazılsaydı ne olurdu?Cevap
Python dosyayı sorunsuz ayrıştırır, çünkü söz dizimi doğrudur.
mesaj(3)“küçük” döndürür ve yazılır.mesaj(20)çağrısında hatalı satır ilk kez çalışır veTypeErrorverir. Dinamik tiplemede tip hatası ancak satır çalışınca görülür; x hiç 10'u geçmeseydi hata hiç görülmezdi. Java'da dönüş tipiStringolan bir metotta"a" + 5dizgi birleştirmedir ve geçerlidir: “a5” döndürür. Dönüş tipiintolsaydı derleyici tip uyuşmazlığını program hiç çalışmadan bildirirdi.Bir dizinin en küçük öğesini bulmak için
ek ← 0ile başlayan ve her öğeyieğer d[i] < ek ise ek ← d[i]ile karşılaştıran bir program yazılmış. d = [3, 8, 1, 6] için çıktı nedir? Hata nedir, nasıl düzeltilir?Cevap
Dört karşılaştırmanın hiçbiri doğru değildir: 3, 8, 1 ve 6'nın hiçbiri 0'dan küçük değildir. ek hiç değişmez ve çıktı 0 olur; doğru sonuç 1'dir. Hata başlangıç değerindedir: 0, girdilerin dışından gelen bir değerdir ve pozitif sayılar için en küçük değer gibi davranır. Düzeltme
ek ← d[0]'dır. İzleme: ek = 3; 8 < 3 yanlış; 1 < 3 doğru, ek = 1; 6 < 1 yanlış. Çıktı 1 olur.
4.11İleri okuma
- A. B. Downey, C. Mayfield, Think Java (2. baskı, O'Reilly, 2019): Java ile programlamaya giriş; ilk bölümleri derleme, yorumlama, hata türleri ve hata ayıklamayı bu nottaki düzeyde anlatır. Green Tea Press sitesinde ücretsiz okunabilir.
- R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach (Addison-Wesley, 2016): Java ile programlamayı bilim ve mühendislik örnekleri üzerinden öğretir; değişken, döngü, fonksiyon ve diziler için bol örnek içerir.
- R. W. Sebesta, Concepts of Programming Languages (12. baskı, Pearson, 2019): dillerin tarihçesini, söz dizimi ve anlamın tanımlanmasını, tip sistemlerini ve paradigmaları ayrıntılı karşılaştırır.
- A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers: Principles, Techniques, and Tools (2. baskı, Addison-Wesley, 2006): derleyicinin aşamalarını anlatan temel kaynak; sözcük çözümleme, söz dizimi çözümleme ve kod iyileştirme konularını derinleştirmek isteyenler için.
4.12Kaynaklar
- R. W. Sebesta, Concepts of Programming Languages, 12. baskı, Pearson, 2019.
- A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers: Principles, Techniques, and Tools, 2. baskı, Addison-Wesley.
- R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach, Addison-Wesley, 2016.
- A. B. Downey, C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (ücretsiz çevrim içi sürüm).
- Oracle, The Java Tutorials: Getting Started.
İlgili sayfalar: Programlamaya Giriş sunumu · Çalışma soruları · Java derleme ve çalıştırma simülatörü · Terim sözlüğü · © Sercan Külcü. Tüm hakları saklıdır.