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.

Tanım 4.1

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.

Algoritma: a ile b'yi topla, yazJavaSystem.out.println(a + b);Pythonprint(a + b)Cprintf("%d", a + b);Çıktı: 8a = 3, b = 5 için
Şekil 4.1: Aynı algoritma (a ile b'yi topla, yaz) Java, Python ve C ile yazılır. Yazım kuralları farklıdır; a = 3, b = 5 için üçü de 8 yazar.

Ş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.

Örnek 4.1

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:

8

Java'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.

Sık yapılan hata

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ığı.

Tanım 4.2

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.

Örnek 4.2

Aşağıdaki üç Java satırının her biri hangi açıdan sorunludur: söz dizimi mi, anlam mı?

  1. int b = 5 (satır sonunda noktalı virgül yok)
  2. double ort = (n1 + n2 + n3) / 4; (üç notun ortalaması isteniyor)
  3. 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.

Tanım 4.3

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.

Kaynak kodinsan okur, yazartoplam = a + b;derleyici çevirirAssemblykomutların simgesel adıLOAD R1, a · ADD R1, b · STORE R1, toplamassembler çevirirMakine diliişlemci çalıştırır (ikili)0001 0001 1000 · 0011 0001 1001 · 0010 0001 1010Makine kodu örnektir (gerçek kodlama işlemciye göre değişir).
Şekil 4.2: Kaynak koddan makine diline. Derleyici tek bir satırı birkaç assembly komutuna, assembler da bu komutları ikili makine koduna çevirir.

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.

Sık yapılan hata

İş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.

Kaynak kodTopla.javaÇeviriderleyiciMakine / ara kodTopla.classÇalıştırmaişlemci / JVMÇıktı8
Şekil 4.3: Kaynak koddan çıktıya giden hat. Kaynak kod çevrilir, çeviriden makine kodu ya da ara kod çıkar, bu kod işlemcide ya da JVM'de çalışır ve çıktı üretilir.

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.

Sık yapılan hata

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.

Tanım 4.4

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.

Kaynak kodTopla.javaÇeviriderleyiciMakine / ara kodTopla.classÇalıştırmaişlemci / JVMÇıktı8✗ derleme hatası Kaynak kodTopla.javaÇeviriderleyiciMakine / ara kodTopla.classÇalıştırmaişlemci / JVMÇıktı8✗ çalışma zamanı hatası
Şekil 4.4: Hataların hat üzerindeki yeri. Üstte: söz dizimi hatası çeviri durağında yakalanır, program hiç çalışmaz. Altta: sıfıra bölme çalıştırma durağında ortaya çıkar; o ana kadarki satırlar çoktan çalışmıştır.

Üçü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.66666666666667

Program 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.

Tablo 4.1: Üç hata türünün karşılaştırması
Hata türüNe zaman ortaya çıkar?Kim bildirir?Örnek
Derleme (söz dizimi, tip)Çeviri sırasında; program hiç çalışmazDerleyici, satır numarasıylaUnutulan ;, metni int'e atamak
Çalışma zamanıYürütme sırasında, hatalı satıra gelinceJVM ya da işletim sistemi; program dururSıfıra bölme, dizinin dışına çıkmak
MantıkHiçbir zaman kendiliğinden görünmezKimse; ancak test ile bulunurEksik parantez, yanlış bölen
Örnek 4.3

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.

  1. (a) Derleme hatasıdır. Noktalı virgül bir söz dizimi kuralıdır; derleyici satır numarasıyla bildirir ve program üretilmez.
  2. (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.
  3. (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.
Sık yapılan hata

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.

Tanım 4.5

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.

Tablo 4.2: Çalıştırma yaklaşımları
YaklaşımİşleyişÖrnek diller
DerlenenProgram önceden hedef makine koduna çevrilirC, C++, Rust
YorumlananKomutlar çalışma sırasında değerlendirilirPython, JavaScript
HibritAra kod sanal makinede yürütülürJava, 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.

Derlenenkaynak.cderleyiciprogram.exeişlemciYorumlanankaynak.pyyorumlayıcıişlemciHibrit (Java)Topla.javajavacTopla.classJVMişlemcituruncu: çeviri aracı
Şekil 4.5: Derlenen, yorumlanan ve hibrit çalıştırma hatları. Turuncu kutular çeviri araçlarıdır: derleyici, yorumlayıcı, javac ve JVM.

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.

Sık yapılan hata

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ırSözde kod
1yaz "Merhaba"
2x ← 10
3yaz 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.

Tablo 4.3: Aynı programın derleyici ve yorumlayıcıyla işlenmesi
AdımDerleyiciBasit yorumlayıcı
1Satı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
2Bölme kurala uygundur; çeviri başarılı, çalıştırılabilir dosya üretilirSatır 2 çevrilir ve çalışır: x = 10; satır 3 henüz okunmadı
3Çalıştırma başlar; satır 1: MerhabaSatır 3 çevrilir, çalışırken hata: sıfıra bölme
4Satır 2: x = 10; satır 3: çalışma zamanı hatası—
EkranMerhaba, ardından hataMerhaba, 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.

Derleyici: önce tümü çevrilir, sonra çalışır1yaz "Merhaba"✓2x ← 10✓3yaz x / 0✗ hatakonsolMerhabaHATA Yorumlayıcı: satır satır çevir ve çalıştır1yaz "Merhaba"✓2x ← 10✓3yaz x / 0✗ hatakonsolMerhabaHATA
Şekil 4.6: Üç satırlık programın son durumu. Üstte derleyici: üç satırın tamamı önce çevrildi (✓), çalıştırmada satır 3 hata verdi. Altta yorumlayıcı: her satır çevrilip hemen çalıştı; satır 3'e gelince hata.

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 closed

Ekranda “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 zero

Bu 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.

Örnek 4.4

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.

  1. (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.
  2. (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.
  3. (c) Python dosyanın tamamını önce ayrıştırır ve satır 2'deki hatayı bulur. Ekranda yalnızca SyntaxError iletisi 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.

Tanım 4.6

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.

Tanım 4.7

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.

JavaScript motoru (ör. V8): önce yorumlar, sık çalışan kısmı JIT ile makine koduna çevirirKaynak kod.jsYorumlayıcıkomut komut yürütürsık çalışan kısımJIT derleyiciçalışırken çevirirMakine kodudoğrudan CPU'daSonraki çağrılarda yorumlama atlanır; program çalıştıkça hızlanır.
Şekil 4.7: JIT derleme. JavaScript motoru (ör. V8) önce kodu yorumlar; sık çalışan kısmı çalışma sırasında makine koduna çevirir. Sonraki çağrılarda yorumlama atlanır ve program çalıştıkça hızlanı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.

Sık yapılan hata

“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).

SözcükçözümlemeSöz dizimiçözümlemeAnlamçözümlemeAra kod veiyileştirmeKodüretimiKaynak satır:alan = en * boy + 2;alantanımlayıcı=operatörentanımlayıcı*operatörboytanımlayıcı+operatör2sabit değer;ayırıcıboşluklar atıldı · 8 belirteç · her belirtecin türü belli
Şekil 4.8: Sözcük çözümleme. 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 errors
KapanmamisDizgi.java:3: error: unclosed string literal
        System.out.println("Merhaba);
                           ^
1 error

2. 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.

SözcükçözümlemeSöz dizimiçözümlemeAnlamçözümlemeAra kod veiyileştirmeKodüretimi=alan+*2enboy* daha derinde:önce hesaplanırağacın kökü: atama
Şekil 4.9: Söz dizimi çözümleme. Belirteçler bir ağaca dizilir; kökte atama vardır, daha derindeki çarpma önce hesaplanır.

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 errors

3. 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).

SözcükçözümlemeSöz dizimiçözümlemeAnlamçözümlemeAra kod veiyileştirmeKodüretimien→ tanımlı · tip int ✓boy→ tanımlı · tip int ✓en * boy + 2→ int × int + int → int ✓alan ← int→ alan int: tipler uyumlu ✓boy hiç bildirilmeseydi → error: cannot find symbol
Şekil 4.10: Anlam çözümleme. Her adın tanımlı olduğu ve tiplerin uyumlu olduğu denetlenir; boy hiç bildirilmeseydi “cannot find symbol” hatası verilirdi.

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 = t2

Burada 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).

SözcükçözümlemeSöz dizimiçözümlemeAnlamçözümlemeAra kod veiyileştirmeKodüretimiüç adresli ara kodt1 = en * boyt2 = t1 + 2alan = t2iyileştirme örneğien = 3, boy = 4 sabitse:alan = 14çalışmadan önce hesaplandısabit katlama: sonucu değişmeyen işlemler derleme sırasında yapılır
Şekil 4.11: Ara kod ve iyileştirme. İfade, her biri tek işlem yapan komutlara bölünür; değerleri sabit olan işlemler derleme sırasında hesaplanır.

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.

SözcükçözümlemeSöz dizimiçözümlemeAnlamçözümlemeAra kod veiyileştirmeKodüretimihedef işlemcinin komutlarıLOAD R1, enMUL R1, boyADD R1, #2STORE R1, alanikiliyemakine kodu0001 0001 1000 …javac ise bytecode üretir:iload iload imul iconst_2 iadd istore
Şekil 4.12: Kod üretimi. Hedef işlemcinin komutları seçilir ve ikiliye çevrilir; javac ise JVM için bytecode üretir.
Örnek 4.5

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.

Tablo 4.4: Derleyicinin aşamaları ve yakaladığı hatalar
AşamaGirdi → çıktıYakalanan hata (javac iletisi)
Sözcük çözümlemekarakterler → belirteçlerGeçersiz karakter (illegal character), kapanmamış dizgi (unclosed string literal)
Söz dizimi çözümlemebelirteçler → söz dizimi ağacıEksik ; (';' expected), kapanmamış parantez
Anlam çözümlemeağaç → denetlenmiş ağaçTanımsız ad (cannot find symbol), tip uyuşmazlığı (incompatible types)
Ara kod ve iyileştirmeağaç → basit komutlar—
Kod üretimibasit komutlar → makine kodu ya da bytecode—
Sık yapılan hata

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.

Tanım 4.8

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.

main.chesap.cderleyiciher dosya ayrımain.ohesap.okütüphanehazır derlenmiş kodbağlayıcıprogram.exeyükleyici (işletim sist.)bellek → işlemcinesne dosyaları (.o / .obj) tek başına çalışmaz
Şekil 4.13: İki kaynak dosya ayrı derlenir; bağlayıcı nesne dosyalarını ve kütüphaneyi birleştirir, yükleyici programı belleğe alı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.

Sık yapılan hata

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.

Tablo 4.5: Derlenen, yorumlanan ve hibrit çalışmanın karşılaştırması
ÖlçütDerlenen (C)Yorumlanan (Python)Hibrit (Java)
Çalışma hızıGenellikle en hızlı: doğrudan makine koduDaha yavaş: komutlar çalışırken çözülürJIT sayesinde derlenene yakın
Hata bildirimiSöz dizimi ve tip hataları çalıştırmadan önceTip hataları çoğunlukla ancak o satır çalışıncaÇoğu derlemede, kalanı çalışırken
DağıtımHer işletim sistemi ve işlemci için ayrı dosyaKaynak kod + hedefte kurulu yorumlayıcıTek .class / .jar + her platformda JVM
Geliştirme döngüsüHer değişiklikten sonra yeniden derlemeKaydet ve hemen çalıştırDerle, 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.

Örnek 4.6

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?

  1. C: Her platform için ayrı ayrı derlenmiş üç çalıştırılabilir dosya. Windows'ta üretilen .exe Linux'ta çalışmaz. Kaynak kod da dağıtılabilir; o zaman her bilgisayarda bir C derleyicisi bulunmalıdır.
  2. Python: Kaynak kod dosyası (.py). Üç bilgisayarda da Python yorumlayıcısının kurulu olması gerekir.
  3. Java: Tek bir .class ya da .jar dosyası. Üç 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

Tanım 4.9

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.

Makine dili10110000 01100001AssemblyMOV AL, 61hCx = x + 1;Java, C#System.out.println(x);Pythonprint(x)soyutlama artar, insan diline yaklaşır →← donanıma yakınlık, ayrıntılı denetim
Şekil 4.14: Dil seviyeleri merdiveni. Yukarı çıkıldıkça soyutlama artar ve dil insan diline yaklaşır; aşağı inildikçe donanıma yakınlık ve ayrıntılı denetim artar.

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.

Örnek 4.7

10110000 01100001 makine komutundaki ikinci bayt hangi sayıdır? Bu sayı assembly'de neden 61h diye yazılır?

  1. İ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.
  2. Bayt dört bitlik iki gruba ayrılır: 0110 = 6 ve 0001 = 1. Her dört bit tek bir onaltılı basamaktır; sayı onaltılı tabanda 61'dir. Denetim: 6 · 16 + 1 = 97.
  3. 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.

19501960197019801990200020102020Assembly1949FORTRAN1957COBOL1959BASIC1964C1972C++1983Python1991Java, JavaScript1995C#2000Go2009Rust 1.02015
Şekil 4.15: Programlama dillerinin zaman çizelgesi, 1949–2015. Her dilin altındaki yıl, ilk sürümünün ya da adının duyurulduğu yıldır.

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.

Problem alanı / platformSık seçilen dillerWeb arayüzü (tarayıcı)JavaScript, TypeScriptAndroid uygulamasıKotlin, JavaiOS uygulamasıSwiftVeri analizi, yapay zekâPython, RGömülü sistem, sürücüC, C++, RustKurumsal sunucuJava, C#, GoSon kararı kütüphane ekosistemi, performans, güvenlik, ekip deneyimi ve bakım maliyeti verir.
Şekil 4.16: Problem alanına ve platforma göre sık seçilen diller. Son kararı kütüphane ekosistemi, performans, güvenlik, ekip deneyimi ve bakım maliyeti verir.

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?

Örnek 4.8

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?

  1. (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.
  2. (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.
  3. İki projede farklı dillerin seçilmesi bir çelişki değildir. Seçim, problemin ve platformun ihtiyaçlarından çıkar.
Sık yapılan hata

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.

Tablo 4.6: Dil türleri ve örnekleri
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 sunarJava, C, Pythonfor (int i = 0; i < 3; i++)
Alana özgüTek bir işe odaklanırSQL, Excel formülleriSELECT ad FROM ogrenci WHERE ort >= 60;
BetikTekrarlanan işleri otomatikleştirir, genellikle yorumlanırPython, JavaScript, Bash, PowerShellCopy-Item *.txt yedek\
İşaretlemeİçeriğin yapısını tanımlar; karar ve döngü yokturHTML, 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.

Sık yapılan hata

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.

Tanım 4.10

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.

Aynı görev, üç yaklaşım: notlar = {70, 90, 80} dizisinin toplamı (240)BuyurganNesne yönelimliFonksiyonelint toplam = 0;for (int n : notlar) toplam += n;toplam: 0 → 70 → 160 → 240durum adım adım değişirogrenci : Ogrencinotlar = {70, 90, 80}toplam() → 240ogrenci.toplam()veri ve davranış bir arada{70, 90, 80}topla240Arrays.stream(notlar).sum()dönüşüm; dizi değişmez
Şekil 4.17: Aynı görev, üç yaklaşım. Buyurgan döngü durumu adım adım değiştirir; nesne yönelimli çözümde toplamı nesnenin kendi metodu hesaplar; fonksiyonel çözüm diziyi değiştirmeden bir dönüşümle toplar.
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
240

Python'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.

Örnek 4.9

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.

  1. Buyurgan: eb adlı bir değişken dizinin ilk öğesiyle (12) başlatılır. Her öğe eb ile karşılaştırılır; daha büyükse eb güncellenir. Durum adım adım değişir: 12 → 45 → 45 → 45. Sonuç 45'tir.
  2. Fonksiyonel: Java'da Arrays.stream(dizi).max().getAsInt(), Python'da max(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.
  3. İ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.

Tanım 4.11

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.

Tablo 4.7: Statik ve dinamik tiplemenin karşılaştırması
ÖzellikStatik (Java)Dinamik (Python)
Tip nerede durur?Değişkende, bildirimde yazılırDeğerde, bildirim yok
Tip hatası ne zaman bulunur?Derleme sırasında, program çalışmadanO satır çalıştığında
Aynı ad farklı tipte değer tutabilir mi?HayırEvet
Güçlü yönHatalar erken bulunur, IDE daha çok yardım ederKısa kod, hızlı deneme
Sık yapılan hata

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.

Tablo 4.8: Sözde kod yapılarının Java ve Python karşılıkları
Sözde kodJavaPython
oku xx = giris.nextInt();x = int(input())
yaz "Toplam: ", tSystem.out.println("Toplam: " + t);print("Toplam:", t)
x ← x + 1x = 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çinfor (int i = 1; i <= n; i++)for i in range(1, n + 1):
koşul doğru iken tekrarlawhile (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")
bitti

x = −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.

Örnek 4.10

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: ", toplam

Java 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: 15
Toplam: 15
1
2
3
bitti

Toplam 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ımlaGirdi: en, boyÇıktı: alanKısıt: en, boy > 0Örnekleri dene3, 4 → 125, 5 → 25en 0 → geçersizAlgoritmayı kuroku en, boyalan ← en × boyyaz alanKoda dönüştüralan = en * boy;println(alan);(Java)Test et3, 4 → 12 ✓-2, 4 → -8 ✗→ kısıt kontrolü eklegeri dön
Şekil 4.18: Problemden programa beş aşama. Testte negatif girdi yanlış sonuç verince (turuncu ok) algoritmaya dönülür ve bir kısıt denetimi eklenir.
  1. 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.
  2. Ö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.
  3. Algoritmayı kur. Dilden bağımsız adımlar sözde kodla yazılır: en'i oku, boy'u oku, alanı hesapla, alanı yaz.
  4. 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.
  5. 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: 12
Geçersiz ölçü
Geçersiz ölçü
Alan: 10
Sık yapılan hata

Problemi 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.

Tablo 4.9: Alan algoritmasının izlenmesi, 1. çalıştırma (en = 3, boy = 4)
AdımSatırenboyalanAçıklama / çıktı
1oku en3——3 okunur ve en kutusuna yazılır
2oku boy34—4 okunur
3eğer en > 0 ve boy > 034—3 > 0 ve 4 > 0 → doğru; “ise” dalına girilir
4alan ← en × boy34123 × 4 = 12
5yaz "Alan: ", alan3412Çı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.

Tablo 4.10: Alan algoritmasının izlenmesi, 2. çalıştırma (en = −2, boy = 4)
AdımSatırenboyalanAçıklama / çıktı
1oku en−2——−2 okunur
2oku boy−24—4 okunur
3eğer en > 0 ve boy > 0−24—−2 > 0 yanlış; “ve” ile bağlı koşulun tamamı yanlış → “değilse” dalı
4yaz "Geçersiz ölçü"−24—Çı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.

Örnek 4.11

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?

  1. “ise” dalına iki satır eklenir: cevre ← 2 × (en + boy) ve yaz "Çevre: ", cevre. Kısıt denetimi aynı kalır; geçersiz ölçüde çevre de hesaplanmaz.
  2. Koşul: 6 > 0 ve 1,5 > 0 → doğru.
  3. alan = 6 × 1,5 = 9.
  4. cevre: önce parantez, 6 + 1,5 = 7,5; sonra 2 × 7,5 = 15.
  5. Çı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.

1. Gereksinim2. Tasarım3. Kodlama4. Test5. Dağıtım6. Bakım
Şekil 4.19: Yazılım geliştirme süreci. Gereksinimden bakıma altı aşama; testte bulunan hata kodlamaya, bakımda gelen yeni istekler gereksinime geri götürür.
  1. 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.
  2. 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.
  3. Kodlama. Tasarım bir programlama diliyle yazılır.
  4. 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.
  5. 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.
  6. 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.

Sık yapılan hata

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).

ŞelaleGereksinimTasarımKodlamaTestDağıtımÇevik1. döngüplan·kod·test2. döngüplan·kod·test3. döngüplan·kod·testher döngü sonunda çalışan sürüm + geri bildirim
Şekil 4.20: Şelale modeli aşamaları basamak basamak, bir kez geçer. Çevik yaklaşım kısa döngülerle ilerler; her döngünün sonunda çalışan bir sürüm ve kullanıcı geri bildirimi vardır.

Ş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.

Tablo 4.11: Geliştirme yaklaşımlarının karşılaştırması
YaklaşımGüçlü yönDikkat
Şelale modeliAşamalar ve belgeler nettirDeğişikliğe dönüş maliyetlidir
ÇevikKısa döngülerle geri bildirim alırSürekli iletişim gerektirir
PrototiplemeBelirsiz gereksinimi görünür kılarPrototip ürün sanılmamalıdır
Örnek 4.12

Üç 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.

  1. (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.
  2. (b) Prototipleme. Kullanıcı ne istediğini ancak somut ekranları görünce söyleyebilir. Hızlı bir taslak bu belirsizliği giderir.
  3. (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).

Bir geliştirme turu: her araç döngünün farklı adımında devreye girerIDE / düzenleyiciyazDerleyiciçevir, çalıştırTest araçlarıdoğrulaHata ayıklayıcıhatayı buldüzelt ve yeniden deneSürüm kontrolü: her değişikliği kaydederPaket yöneticisi: kütüphaneleri getirir
Şekil 4.21: Bir geliştirme turu. Her araç döngünün farklı bir adımında devreye girer; sürüm kontrolü ve paket yöneticisi bütün döngüyü destekler.
  • 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.
Sık yapılan hata

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.

Tanım 4.12

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.

K1ilk sürümK2alan eklendiK3çevre eklendiK4birleştirmeD1deneme dalı: yeni hesapana dalher kayıt: kim, ne zaman, ne değişti
Şekil 4.22: Ana dalda dört kayıt. İkinci kayıttan ayrılan deneme dalı (D1) yeni bir hesabı dener ve dördüncü kayıtta ana dala birleştirilir.

Ş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.

Örnek 4.13

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?

  1. Dosyanın başına eklenen sabit satırı yalnızca Mehmet'in değişikliğidir; Git bunu sorunsuz alır.
  2. 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.
  3. İ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ık yapılan hata

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

Tanım 4.13

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.

yas19tip: tam sayıboy1.75tip: kayan noktalı sayıad"Ali"tip: dizgigectidoğrutip: mantıksal
Şekil 4.23: Bellekte adlandırılmış kutular. Her kutunun bir adı, bir değeri ve bir tipi vardır.
Tablo 4.12: Temel veri tipleri ve Java karşılıkları
TipNe tutar?Örnek değerJavaYapılabilen işlemler
Tam sayıKesirsiz sayı19, −3, 0intDört işlem, kalan (mod)
Kayan noktalı sayıKesirli sayı1.75, 81.67doubleDört işlem
DizgiKarakter dizisi, metin"Ali"StringBirleştirme, uzunluk, karakter alma
MantıksalDoğru ya da yanlıştrue, falsebooleanve, 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.

Sık yapılan hata

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:

  1. Parantez içi önce hesaplanır.
  2. Çarpma, bölme ve mod (kalan) toplamadan ve çıkarmadan önce yapılır.
  3. Toplama ve çıkarma en son yapılır.
  4. 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.

Tablo 4.13: İşlem önceliği örnekleri
İfadeHesaplama sırasıSonuç
2 + 3 × 4Önce 3 × 4 = 12, sonra 2 + 1214
(2 + 3) × 4Önce parantez 2 + 3 = 5, sonra 5 × 420
20 / 4 × 2Aynı düzey, soldan sağa: 20 / 4 = 5, sonra 5 × 210
10 − 4 − 3Soldan sağa: 10 − 4 = 6, sonra 6 − 33
7 mod 3 + 1Önce 7 mod 3 = 1, sonra 1 + 12

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.

+2×34önce: 12sonra: 2 + 12 = 14
Şekil 4.24: 2 + 3 × 4 ifadesinin ağacı. Çarpma daha derinde olduğu için önce yapılır (12), sonra kökteki toplama (14).

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
9

Son 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.

Örnek 4.14

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ı?

  1. İ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.
  2. İ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.
Sık yapılan hata

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).

EvetHayırkoşul ?işlem Aişlem B EvettekrarHayırkoşul ?gövdeçık
Şekil 4.25: Solda karar yapısı: koşula göre iki yoldan yalnızca biri izlenir. Sağda döngü: koşul doğru oldukça gövde tekrarlanır; geri dönen ok döngünün işaretidir.
Tanım 4.14

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"
Tanım 4.15

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.

Tablo 4.14: İlk sürümün izlenmesi (4. satır yok)
AdımSatıriKoşulGövde çalışma sayısıÇıktı
1i ← 11—0
2i ≤ 3 iken11 ≤ 3 → doğru0
3yaz i1—11
4i ≤ 3 iken11 ≤ 3 → doğru1
5yaz i1—21
……1hep doğrusınırsız artar1, 1, 1, …
İlk sürüm (4. satır yok)i1koşul1 ≤ 3 → doğrugövde çalışma sayısı…↻ ∞
Şekil 4.26: Sonsuz döngü. i hep 1'dir, koşul hep doğrudur ve gövde sınırsız sayıda çalışır.

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.

Tablo 4.15: Düzeltilmiş sürümün izlenmesi
AdımKoşul denetimiGövdei (turdan sonra)Gövde çalışma sayısı
Başlangıç—i ← 110
1. tur1 ≤ 3 → doğru1 yazılır, i ← 221
2. tur2 ≤ 3 → doğru2 yazılır, i ← 332
3. tur3 ≤ 3 → doğru (eşitlik)3 yazılır, i ← 443
Çıkış4 ≤ 3 → yanlışDöngüden çıkılır; “bitti” yazılır43

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
bitti
Örnek 4.15

Aş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. 1. tur: 1 < 20 doğru; 1 yazılır, k = 3.
  2. 2. tur: 3 < 20 doğru; 3 yazılır, k = 9.
  3. 3. tur: 9 < 20 doğru; 9 yazılır, k = 27.
  4. 27 < 20 yanlış; döngü biter. Üç tur döner ve ekrana 1, 3, 9 yazılır; koşul 4 kez denetlenir.
  5. 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ı.
Sık yapılan hata

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).

4girdikare(x)x × x döndürsonuç16notlar dizisi85[0]72[1]90[2]64[3]indis 0'dan başlar
Şekil 4.27: Solda kare fonksiyonu: 4 girdisini alır, 4 × 4 = 16 döndürür. Sağda notlar dizisi: dört hücre, indisler 0'dan başlar.
Tanım 4.16

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 + b

Fonksiyon ç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.

Tablo 4.16: Fonksiyon çağrısının izlenmesi
AdımOlayAna programalan çerçevesi
1Tanım (satır 1–4) kaydedilir, çalışmaz; program 5. satırdan başlara = —, b = —yok
2alan(3, 4) çağrılır; yeni çerçeve, 3 → en, 4 → boya = —, b = —en = 3, boy = 4
3Gövde: sonuc ← 3 × 4 = 12a = —, b = —en = 3, boy = 4, sonuc = 12
4döndür sonuc: 12 çağıran yere gönderilira = —, b = —kaldırılacak
5Çerçeve kalkar; dönen değer a'ya atanıra = 12, b = —yok
6alan(2, 5) çağrılır; yepyeni bir çerçevea = 12, b = —en = 2, boy = 5
7Gövde: sonuc ← 2 × 5 = 10a = 12, b = —en = 2, boy = 5, sonuc = 10
810 döndürülür, çerçeve kapanıra = 12, b = —kaldırılacak
9Dönen değer b'ye atanıra = 12, b = 10yok
10yaz a + b → 22a = 12, b = 10yok
ana programa = — b = —alanen = 3 boy = 4 sonuc = 12sonuc ← 3 × 4 = 12çağrı yığını (üstteki çerçeve çalışıyor) ana programa = 12 b = —alanen = 2 boy = 5 sonuc = 10sonuc ← 2 × 5 = 10çağrı yığını (üstteki çerçeve çalışıyor)
Şekil 4.28: Çağrı yığınının iki anı. Üstte birinci çağrı çalışıyor: çerçevede en = 3, boy = 4, sonuc = 12. Altta ikinci çağrı: aynı fonksiyon için yeni bir çerçeve, en = 2, boy = 5, sonuc = 10.

İ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);
    }
}
22
Örnek 4.16

fonksiyon 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?

  1. Ö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.
  2. 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.
  3. İ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.
Sık yapılan hata

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: ", ort

Toplamı 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.

Tablo 4.17: Notların ortalamasının izlenmesi (notlar = [85, 72, 90, 64])
Adıminotlar[i]İşlemtoplam
Başlangıç——toplam ← 00
1. tur0850 + 8585
2. tur17285 + 72157
3. tur290157 + 90247
4. tur364247 + 64311
Döngüden sonra——ort ← 311 / 4311 (ort = 77,75)
notlar85[0]72[1]90[2]64[3]toplam311ort = 77.75
Şekil 4.29: Döngü bittikten sonra dizi ve biriktirici: toplam 311, ortalama 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.75

Python'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.75

Dö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.

Örnek 4.17

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?

  1. sayac ← 0. i = 0: sıcaklık[0] = 18; 18 > 20 yanlış, sayaç 0.
  2. i = 1: 21 > 20 doğru, sayaç 1.
  3. i = 2: 25 > 20 doğru, sayaç 2.
  4. i = 3: 19 > 20 yanlış, sayaç 2.
  5. 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.

Tanım 4.17

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.

Senin programınkok ← karekök(16)yaz kok → 4.0API: ad, girdi, çıktıkarekök(16) çağrısı4.0 dönerKütüphaneNewton yöntemi, kayannokta ayrıntıları …içi görünmez
Şekil 4.30: Program kütüphane fonksiyonunu API üzerinden çağırır: karekök(16) çağrısı 4.0 döndürür. Kütüphanenin içi görünmez.

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.

Tablo 4.18: Kütüphane çağrısı örnekleri
ÇağrıDilSonuç
Math.sqrt(16)Java4.0
Math.max(3, 8)Java8
len("Giresun")Python7

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
7
import math
print(math.sqrt(16))
print(max(3, 8))
print(len("Giresun"))
4.0
8
7

API 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.

Sık yapılan hata

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.

  1. 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.
  2. 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.
  3. 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.
  4. 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.
  5. 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.
  6. 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.
Hata ayıklayıcıAdımDevam1n1 ← 702n2 ← 853n3 ← 90▶ort ← n1 + n2 + n3 / 35yaz "Ortalama: ", ortDeğişkenler: n1 = 70 n2 = 85 n3 = 90kesme noktası: 4. satırda durdu
Şekil 4.31: Hata ayıklayıcı penceresi. Program 4. satırdaki kesme noktasında durmuştur; değişkenler panelinde n1 = 70, n2 = 85, n3 = 90 görünür.

Ş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.

Sık yapılan hata

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.

Tablo 4.19: Test 1 (a = 7, b = 12, c = 5; beklenen 12)
SatırKoşuleb
eb ← 0—0
eğer a > eb7 > 0 → doğru7
eğer b > eb12 > 7 → doğru12
eğer c > eb5 > 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).

Tablo 4.20: Test 2 (a = −5, b = −2, c = −9; beklenen −2)
SatırKoşuleb
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 ✗
Test 2: −5, −2, −9a-5b-2c-9eb0çıktı 0, beklenen -2✗ mantık hatası
Şekil 4.32: Test 2'nin sonu. eb hiç değişmemiştir; çıktı 0, beklenen −2: mantık hatası.

Üç 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: 12
En büyük: 0
import 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: 12
En büyük: -2
En büyük: 3
Örnek 4.18

Düzeltilmiş en büyük programı için hangi test girdileri seçilmelidir? Her biri neyi sınar?

  1. 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.
  2. Hepsi negatif: (−5, −2, −9). Başlangıç değeri hatasını yakalar.
  3. 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.
  4. 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.

Tablo 4.21: Sık yapılan hatalar
HataÖrnekNe olur?
Başlangıç değeri yok ya da yanlıştoplam başlatılmadan toplam ← toplam + xJava derlemez; bazı dillerde rastgele bir değerle hesaplar
Sınır hatası (bir eksik / bir fazla)her i ← 1 … n − 1 içinSon öğe atlanır ya da bir fazla tur döner
Sonsuz döngüDöngü değişkeni gövdede güncellenmezProgram bitmez
Sıfıra bölmeort ← toplam / n, n = 0Çalışma zamanı hatası
Dizinin dışına çıkmak4 öğeli dizide notlar[4]Çalışma zamanı hatası
Parantez unutmakort ← a + b / 2Yanlış sonuç (mantık hatası)
Girdiyi doğrulamamakYaş olarak −3 kabul etmekAnlamsı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 error

C'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.

Örnek 4.19

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?

  1. Hiç not girilmemesi (n = 0): sıfıra bölme. Program, n = 0 iken “not girilmedi” yazacak biçimde düzeltilir.
  2. Tek not (n = 1): ortalama notun kendisi olmalıdır. Döngü sınırında bir eksik varsa sonuç 0 çıkar.
  3. En küçük ve en büyük notlar (0 ve 100): girdi denetimi bu değerleri yanlışlıkla reddetmemelidir; örneğin not > 0 yerine not ≥ 0 yazılmalıdır.
  4. Geçersiz not (−5 ya da 120): program bu değeri kabul etmemelidir.
  5. 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.

Örnek 4.20

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
32767

short 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.

Sık yapılan hata

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 c

Aynı 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.20 yerine KDV_ORANI ← 0.20 tanımlanır ve fiyat * KDV_ORANI yazı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 ekle açıklaması kodu tekrar eder ve bir şey katmaz. // dizi 0'dan başlıyor, sıra numarası için 1 ekle açıklaması ise bir niyeti açıklar.
Örnek 4.21

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.0
Alan: 10.5

6 × 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.0

250 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.

Sık yapılan hata

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

ProblemgereksinimAlgoritmasonlu, açık adımlarKaynak kodprogramlama diliderlemeyorumlamaÇalışan programCPU / sanal makinedeğişken · karar · döngü · fonksiyon · dizibakım ve yeni gereksinimler: süreç yinelemelidir
Şekil 4.33: Problemden çalışan programa: problem algoritmaya, algoritma kaynak koda dönüşür; kaynak kod derleme ya da yorumlama ile çalışır. Bakım ve yeni gereksinimler süreci başa döndürür.
  • 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.

4.10Kendini Dene

  1. 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ş toplam değişkenini kullanmak. (c) n öğeli bir dizide i <= n koş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, Java ArrayIndexOutOfBoundsException verir. (d) Mantık hatasıdır; program her girdiyle çalışır ama ortalamayı olduğundan büyük bulur.

  2. Üç 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 closed iletisini 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.

  3. 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.

  4. 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 × i ifadesinde parantez gerekmez.

  5. fonksiyon f(x): döndür 2 × x + 1 tanı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.

  6. 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.

  7. Bir Python fonksiyonu x 10'dan büyükse "a" + 5 döndürüyor, değilse "küçük". Program mesaj(3) ve ardından mesaj(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 ve TypeError verir. 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üş tipi String olan bir metotta "a" + 5 dizgi birleştirmedir ve geçerlidir: “a5” döndürür. Dönüş tipi int olsaydı derleyici tip uyuşmazlığını program hiç çalışmadan bildirirdi.

  8. Bir dizinin en küçük öğesini bulmak için ek ← 0 ile başlayan ve her öğeyi eğ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

  1. R. W. Sebesta, Concepts of Programming Languages, 12. baskı, Pearson, 2019.
  2. A. V. Aho, M. S. Lam, R. Sethi, J. D. Ullman, Compilers: Principles, Techniques, and Tools, 2. baskı, Addison-Wesley.
  3. R. Sedgewick, K. Wayne, Computer Science: An Interdisciplinary Approach, Addison-Wesley, 2016.
  4. A. B. Downey, C. Mayfield, Think Java, 2. baskı, O'Reilly, 2019 (ücretsiz çevrim içi sürüm).
  5. 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.