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.
switchiçindebreakolmayancaseetiketleri bir sonraki gövdeye düşer; bu düşme,breakkaynakta açıkça yazılmadıkça C’nin varsayılan davranışıdır.whilekoşulu gövdeden önce,do-whilekoş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ı
switchiç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.