İçeriğe geç
academia.sh

Ders 09 / 13

Denetim Akışı

Bir dalın hangi kod yolunu izleyeceğinin koşumda üretilen veriye bakılarak belirlenmesi; switch düşmesi, döngü biçimleri, goto'nun sınırlı yeri ve hangi dalların var olduğu ile hangisinin seçildiği arasındaki ayrım.

Yayın Güncelleme

Bu derste

  • Bir dalın hangi kod yolunu izleyeceğinin koşumda üretilen veriye bağlı olduğunu ölçer
  • switch düşmesini, döngü biçimlerini ve goto'nun sınırlı kullanımını örnekle ayırt eder
  • Hangi dalların var olduğu ile hangisinin koştuğu arasındaki farkı ön işlemci ve koşum üzerinden gösterir
İçindekiler

İşleçler dersi, bir ifadenin sonucunu neyin belirlediğini ölçtü ve yanıt hep aynıydı: kaynakta yazılan tipler ve yapı, koşum başlamadan önce zaten biliniyordu. Denetim akışı bu düzeni tersine çeviriyor. Bir program hangi dalı izleyecek, hangi döngü kaç kez dönecek — bunlar artık kaynağı okuyarak önceden söylenemez; ancak program çalışıp veriyi ürettikten sonra bilinir.

Programlama Temelleri kursu (M01/K02) if, while ve benzeri denetim yapılarını Python’la zaten kurmuştu; oradaki sözdizimi ile buradaki arasında yüzeysel bir benzerlik var, ama bu ders sözdizimini değil, kararın hangi aşamada verildiğini ölçüyor: bir dalın seçilip seçilmediği veriye, hangi dalların kaynakta var olduğu ise ayrı bir aşamaya bağlı olabiliyor.

Bu ders M11’in ölçü ekseni bakımından bir dönüm noktası. Veri Tipleri, Değişkenler ve İşleçler dersleri hep aynı yöne işaret etti: kaynakta yazılan tipler ve yapı, koşum hiç başlamadan kararı veriyordu. Denetim akışında bu yön tersine döner — bir dalın seçilip seçilmeyeceği artık kaynağı okuyarak değil, programı çalıştırıp veriyi üreterek bilinir. Ama bu tersine dönüş mutlak değil: aşağıda görüleceği gibi, hangi dalların kaynakta bile mevcut olduğu hâlâ derleyiciden önceki bir aşamada, ön işlemcide karara bağlanabiliyor. Denetim akışı, bu yüzden tek bir sınıfa değil, iki sınıfın kesişimine oturan bir konu.

Veriye Bağlı Dallanma

Aşağıdaki kesit, Derleme Aşamaları dersinde tanıtılan çekirdeğin bir parçasıdır: bir dizinin toplamı hesaplanıyor ve bu toplama bakılarak bir dal seçiliyor.

// asama.c — veriye bagli dallanma (kesit)
#include <stdio.h>
#include <stddef.h>

int main(void) {
    int veri[] = {4, 8, 15, 16};
    int toplam = 0;
    for (size_t i = 0; i < sizeof veri / sizeof veri[0]; i++) {
        toplam += veri[i];
    }
    const char *karar = (toplam > 40) ? "buyuk" : "kucuk";
    printf("%s %s\n", "veriye bagli dallanma", karar);
    return 0;
}
veriye bagli dallanma buyuk

TS7. karar değişkeninin "buyuk" mu "kucuk" mu olacağı, kaynakta hiçbir yerde yazılı değil — veri dizisindeki dört sayı toplanana kadar bilinemez. toplam 43 çıktığı için "buyuk" dalı seçiliyor, ama diziye tek bir eleman eklense ya da bir eleman değişse sonuç değişebilirdi; kaynak kodun kendisi hiç değişmeden. Bu, önceki derste ölçülen işleç seçimiyle taban tabana zıt bir karar: orada sonucu tipler belirliyordu ve koşum başlamadan biliniyordu, burada sonucu veri belirliyor ve ancak döngü tamamlandıktan sonra bilinebiliyor.

Bu fark ?: koşullu işlecinin kendisinde de görünür. (toplam > 40) ? "buyuk" : "kucuk" ifadesinin hangi dalının değer üreteceği koşuma bağlı, ama ifadenin kendisinin bir değer ürettiği derlemede zaten bellidir — derleyici iki daldan ortak bir sonuç tipi türetir (burada ikisi de aynı olduğu için const char *) ve ikisinden birinin seçileceğini bilir, yalnız hangisinin seçileceğini bilmez. Bu, denetim akışının genel bir özelliğini gösteriyor: kaynak, hangi dalların var olduğunu ve her dalın hangi tipte bir sonuç üreteceğini derlemede sabitler; yalnız veriye bakılarak hangisinin çalışacağı koşuma bırakılır. if gövdeleri ve switch case’leri için de aynı ayrım geçerli: kaynaktaki her dal derlenir ve ikili dosyada yerini alır, ama bir koşumda hangisinin gerçekten yürütüldüğü ancak o koşum bittikten sonra söylenebilir.

Switch Düşmesi

Bir switch ifadesinde art arda gelen case etiketleri, aralarında break olmadıkça aynı gövdeye düşer (fall-through) — bir etiketten bir sonrakine akış kesilmeden geçilir. düşme burada fall-through karşılığıdır ve M06/K05’te kapalıdır — bu kullanım o dersteki drop duyusundan tamamen ayrı, C’de drop kavramı zaten yok.

// gecisli.c — switch dusmesi (fall-through) art arda etiketleri ayni govdeye baglar
#include <stdio.h>

static void seviyeyi_yazdir(int puan) {
    switch (puan / 10) {
        case 10:
        case 9:
            printf("%s\n", "seviye: A");
            break;
        case 8:
        case 7:
            /* kasitli dusme: 8 ve 7, ortak govdeye dusuyor */
            printf("%s\n", "seviye: B");
            break;
        default:
            printf("%s\n", "seviye: C");
            break;
    }
}

int main(void) {
    seviyeyi_yazdir(95);
    seviyeyi_yazdir(88);
    seviyeyi_yazdir(72);
    seviyeyi_yazdir(40);
    return 0;
}
seviye: A
seviye: B
seviye: B
seviye: C

puan / 10 ifadesi 10 verdiğinde akış gövdesiz case 10 etiketine giriyor, hiçbir gövde bulmadan case 9a düşüyor ve oradaki gövdeyi çalıştırıyor; 9 verdiğinde ise doğrudan case 9a atlıyor. İki etiket böylece tek bir gövdeyi paylaşıyor. Aynı düzen case 8 ile case 7 için de geçerli. break yazılmadığında akışın devam etmesi C’nin varsayılan davranışı; bir gövdeden çıkmak isteyen her case bunu açıkça break ile belirtmek zorunda. Yukarıdaki dört çağrı bunu doğruluyor: 95 gövdeli case 9a eşleşiyor ve düşmeye hiç uğramıyor; 88 ise gövdesiz case 8e eşleşip case 7nin gövdesine düşüyor.

Düşmenin kendisi, hangi casein eşleştiğine bakılmaksızın kaynakta her zaman mevcuttur — derleyici, break yazılmayan her etiketten bir sonrakine akan bir yol üretir ve bu yol ikili dosyada durur. Ama bu yolun izlenip izlenmeyeceği, yalnız switch ifadesinin başındaki değere bakılarak koşumda belirlenir: 95 girildiğinde akış zaten case 10dan başlamıyor, doğrudan case 9a atlıyor, çünkü switch bir eşleşen etiketi bulduğu anda oraya sıçrar ve yalnız oradan sonrasını sırayla yürütür. Düşme, bu yüzden “her etiket sırayla denenir” anlamına gelmiyor — switch önce eşleşen etiketi buluyor, düşme yalnız o noktadan sonraki etiketleri, aralarında break yoksa, gövdesiz geçiyor.

Döngü Biçimleri: Koşulun Ne Zaman Sınandığı

C üç temel döngü biçimi taşır ve aralarındaki fark, koşulun döngü gövdesinden önce mi sonra mı sınandığıdır — bu da veriye bağlı bir davranış farkına yol açar.

// donguler.c — while ve do-while ayni kosulu farkli calisma sayisiyla yorumlar
#include <stdio.h>

int main(void) {
    int sayac = 5;
    int calisma_sayisi_while = 0;
    while (sayac < 5) {
        calisma_sayisi_while++;
        sayac++;
    }
    printf("%s %d\n", "while calisma sayisi", calisma_sayisi_while);

    int calisma_sayisi_dowhile = 0;
    do {
        calisma_sayisi_dowhile++;
    } while (sayac < 5);
    printf("%s %d\n", "do-while calisma sayisi", calisma_sayisi_dowhile);
    return 0;
}
while calisma sayisi 0
do-while calisma sayisi 1

sayac baştan 5 ve while (sayac < 5) koşulu gövdeden önce sınandığı için, koşul en baştan yanlış ve gövde hiç çalışmıyor: calisma_sayisi_while sıfırda kalıyor. do { ... } while (sayac < 5) ise gövdeyi önce çalıştırıp sonra koşulu sınıyor; koşul yine yanlış olsa da gövde en az bir kez çalışmış oluyor ve calisma_sayisi_dowhile 1 çıkıyor. İki döngü de aynı koşulu taşıyor, aynı veriyle çalışıyor, ama sınama sırası farklı olduğu için sonuçları ayrışıyor — bu fark, koşulun kaynakta nerede yazıldığına değil, döngünün hangi biçimde bildirildiğine bağlı ve for döngüsü de while gibi koşulunu gövdeden önce sınar.

Üç biçimin de gövdesi kaynakta yalnız bir kez yazılır, ama kaç kez çalışacağı koşumdaki veriye bağlıdır — sayacın başlangıç değeri farklı olsaydı, aynı üç döngü farklı sayıda dönerdi. while ile for arasındaki fark yalnız yazım biçimidir: for (baslangic; kosul; adim) bir while döngüsünün üç parçasını tek satırda toplar ve ikisi de aynı sınama sırasını (koşul önce) izler. do-while ise tek biçimde farklı: bir döngünün gövdesi koşula bakılmaksızın en az bir kez çalışmalıysa — bir menü göstermek, bir girdi istemek gibi — bu biçim, ayrı bir bayrak değişkeni ya da gövdeyi iki kez yazmak zorunda kalmadan bunu doğrudan ifade eder.

Goto: Sınırlı ve Kabul Edilen Bir Kullanım

goto, kaynağın herhangi bir yerinden bir etikete doğrudan atlar ve genel kullanımı akışı izlemeyi zorlaştırdığı için önerilmez. Ama ic ice geçmiş döngülerden tek adımda çıkmak gibi dar bir senaryoda, breakin yalnız en yakın döngüyü kırabilmesi yüzünden goto yaygın kabul gören bir çözüm sunar.

// arama.c — goto, ic ice donguden tek adimda cikmanin sinirli ve kabul edilen kullanimi
#include <stdio.h>

int main(void) {
    int izgara[3][3] = {{1, 2, 3}, {4, 5, 6}, {7, 8, 9}};
    int hedef = 6;
    int bulunan_i = -1, bulunan_j = -1;

    for (int i = 0; i < 3; i++) {
        for (int j = 0; j < 3; j++) {
            if (izgara[i][j] == hedef) {
                bulunan_i = i;
                bulunan_j = j;
                goto bulundu;
            }
        }
    }
bulundu:
    printf("%s %d\n", "bulunan satir", bulunan_i);
    printf("%s %d\n", "bulunan sutun", bulunan_j);
    return 0;
}
bulunan satir 1
bulunan sutun 2

hedef değeri izgara[1][2]de bulunuyor ve goto bulundu; iki döngüyü de tek satırda terk ediyor. Aynı sonucu break ile elde etmek isteyen bir yazım, iç döngüden çıktıktan sonra dış döngüyü de bir bayrak değişkenle kontrol edip ayrıca kırmak zorunda kalırdı — goto burada ek bir bayrak değişkeni gerektirmeden aynı işi görüyor. bulundu etiketinin döngülerin dışında, işlevin sonuna yakın durması da kabul gören kullanımın bir parçası: goto yalnız ileri doğru ve işlevin doğal akışının bittiği bir noktaya atlıyor, kaynağın ortasında karmaşık bir sıçrama ağı kurmuyor.

bulundu etiketine gerçekten atlanıp atlanmayacağı da veriye bağlı bir karar: hedef değeri izgara içinde hiç bulunmasaydı, iki döngü de tamamen bitmiş ve goto hiç çalışmamış olurdu, akış etikete yalnız döngülerin doğal sonunda, sırayla ulaşırdı. Kaynakta goto bulundu; satırı hep aynı yerde duruyor, ama o satırın çalışıp çalışmayacağı — ve çalışırsa kaçıncı döngü adımında — izgaranın içeriğine ve hedefin değerine bakılarak koşumda belirleniyor. goto bu yüzden denetim akışının genel kuralından ayrılmıyor: sıçramanın kendisi kaynakta sabit, sıçramanın tetiklenip tetiklenmeyeceği veriye bağlı.

Sınırlayıcı Ölçüm

Bir dalın hangi kod yolunu izleyeceği veriye bağlıydı, ama hangi dalların kaynakta var olduğu her zaman koşumla belirlenmez — bazen ön işlemci, derleyici kaynağı görmeden önce, hangi caselerin bile var olacağına karar vermiş olabilir.

// sinir.c — hangi dallarin var oldugu on islemcide, hangisinin secildigi kosumda belirlenir
#include <stdio.h>

#define EK_DURUM

static void kodu_degerlendir(int kod) {
    switch (kod) {
        case 1:
            printf("%s\n", "durum 1 calisti");
            break;
#ifdef EK_DURUM
        case 2:
            printf("%s\n", "durum 2 calisti (on islemci varligini onayladi)");
            break;
#endif
        default:
            printf("%s\n", "taninmayan durum");
            break;
    }
}

int main(void) {
    for (int kod = 1; kod <= 3; kod++) {
        kodu_degerlendir(kod);
    }
    return 0;
}
durum 1 calisti
durum 2 calisti (on islemci varligini onayladi)
taninmayan durum

EK_DURUM makrosu tanımlı olduğu için case 2 etiketi, derleyici kaynağı görmeden önce, ön işlemci tarafından metne dahil ediliyor. EK_DURUM tanımlı olmasaydı, o case bloğu kaynaktan tamamen silinmiş olurdu — derleyici onu hiç görmez, kod == 2 geldiğinde akış doğrudan defaulta düşerdi. Bu ölçümde EK_DURUM tanımlı olduğu için case 2 var ve kod döngüde 1den 3e kadar ilerlerken hangi durumun seçildiği her adımda veriye (döngü sayacına) bakılarak koşumda belirleniyor. İki karar birbirinden tamamen bağımsız: bir dalın var olup olmadığı ön işlemcide, o dalın seçilip seçilmediği koşumda karara bağlanıyor — ve bir dal hiç var olmazsa, hiçbir veri onu seçemez.

Bu iki aşamanın sırası tersine çevrilemez. Ön işlemci önce çalışır ve #ifdef bloklarını kaynaktan ya tamamen siler ya da tamamen bırakır; derleyici, kendisine ulaşan metinde zaten hangi caselerin bulunduğunu bilir, hiçbir zaman “bu case şu veriyle var olsun” diye bir karar vermez. Koşum ise tam tersi bir sınırlamayla çalışır: elindeki ikili dosyada zaten hangi dalların bulunduğunu değiştiremez, yalnız var olanlar arasından seçim yapabilir. Bir hatanın kaynağını ararken bu ayrım pratik bir sonuç doğurur: “bu dal hiç çalışmadı” ile “bu dal zaten kaynakta yok” iki ayrı olası neden ve ikisinin denetlendiği yer de farklı — biri koşum çıktısında, öbürü kaynağın ön işlemci yönergelerinde aranır.

Özet

  • Bir dalın hangi kod yolunu izleyeceği koşumda üretilen veriye bakılarak belirlenir; kaynak kodun kendisi bu kararı önceden vermez — sınıf koşum.
  • switch içinde break olmayan case etiketleri bir sonraki gövdeye düşer; bu düşme, break kaynakta açıkça yazılmadıkça C’nin varsayılan davranışıdır.
  • while koşulu gövdeden önce, do-while koşulu gövdeden sonra sınanır; aynı koşul ve aynı veriyle bile bu sıra farkı çalışma sayısını değiştirebilir.
  • goto, iç içe döngülerden tek adımda çıkmak gibi dar ve kabul gören bir senaryoda, ek bir bayrak değişkeni gerektirmeden aynı işi görür.
  • Hangi dalların kaynakta var olduğu ön işlemcide, var olan dallardan hangisinin seçildiği koşumda karara bağlanabilir — iki karar aynı switch içinde bir arada bulunabilir ve birbirinden bağımsızdır.
  • ?: koşullu işleci bir değer ürettiğini ve o değerin tipini derlemede sabitler; tip iki daldan türetilir, yalnız hangi dalın gerçekten değer üreteceği koşumda belirlenir.

Sonraki Adım

Bu derste ölçülen dallanmalar hep bilinen bir işlevin gövdesi içinde kaldı — hangi dalın seçileceği veriye bağlıydı, ama hangi işlevin çağrıldığı hep kaynakta sabitti. Fonksiyonlar dersi bu son varsayımı sınıyor: bir çağrının hangi tanıma bağlanacağı, iki birim ayrı ayrı derlendikten sonra, ancak bağlama sırasında çözülüyor. Yukarıdaki kodu_degerlendir gibi bir işlev hep aynı dosyada tanımlıydı; sıradaki derste bir işlev bir birimde çağrılacak, tanımı ise başka bir birimde duracak — ve aradaki bağı kim, hangi aşamada kurduğunu belirleyecek. toplam > 40 gibi bir sınamanın sonucu bu derste yalnız veri dizisine bağlıydı; sıradaki derste bir çağrının sonucu, ayrıca çağrılan işlevin hangi tanıma ulaştığına da bağlı olacak — kaynakta yazılan bir ad, iki farklı birimde iki farklı gövdeye karşılık gelemez, ama hangi gövdeye bağlandığını yalnız bağlayıcı bilir.

İlerlemeni kaydetmek ve not almak için Giriş yap

Notlarım

Not almak için giriş yapmalısın.

Aramak için yazmaya başlayın.

↑↓ ↵ Esc gezin · aç · kapat