C fonksiyonları, çalışma yığını (runtime stack) ve özyineleme
Fonksiyon tanımı, değerle çağırma (call by value), static yerel değişken, LC-3'te aktivasyon kaydı (R5 frame pointer, R6 stack pointer) ve özyinelemeli fonksiyonların izlenmesi.
Şimdiye kadar her şey main içindeydi. Gerçek programlar fonksiyonlara bölünür: her biri tek bir işi yapar, adı vardır, parametre alır, bir değer döndürür. Bu sayfa fonksiyonların C’deki kurallarını, LC-3’te nasıl çalıştıklarını (çalışma yığını, runtime stack) ve kendini çağıran fonksiyonları (özyineleme, recursion) anlatıyor. Sınavda iki tür soru gelir: “What is printed?” ve “yığını çiz: R5 nerede, parametre hangi offset’te?”.
Fonksiyon tanımı
int square(int x) { /* dönüş tipi, ad, parametre listesi */
return x * x;
}
int main(void) {
int r = square(4); /* argüman 4 */
printf("%d\n", r);
return 0;
}
- Parametre (parameter): fonksiyon tanımındaki değişken (
x). - Argüman (argument): çağrıda verilen değer (
4). - Prototip (prototype): fonksiyon main’den sonra tanımlanacaksa önceden bildirilir:
int square(int x);. voiddönüş tipi: değer döndürmez.
Değerle çağırma (call by value)
C’de argümanın değeri kopyalanır. Fonksiyon parametresini değiştirirse sadece kendi kopyasını değiştirir.
void swap(int a, int b) { int t = a; a = b; b = t; }
int main(void) {
int x = 1, y = 2;
swap(x, y);
printf("%d %d\n", x, y);
return 0;
}
1 2
LC-3 düzeyinde açıklaması basit: argümanlar yığına kopyalanır, swap o kopyaları değiştirir, fonksiyon dönünce o bölge yığından atılır. Takas için x ve y’nin adreslerini geçmek gerekir (pointer, 13. hafta).
Kapsam ve ömür
| Değişken türü | Nerede yaşar (LC-3)? | Ömrü |
|---|---|---|
| Local | yığında, aktivasyon kaydında | fonksiyon çalıştığı sürece |
| Global | global veri bölgesinde (R4) | program boyunca |
| static local | global veri bölgesinde | program boyunca, ama sadece fonksiyon içinden görülür |
int counter(void) { static int c = 0; c++; return c; }
İlk çağrı 1, ikinci 2, üçüncü 3 döndürür. static olmasa her seferinde 1.
Çalışma yığını (runtime stack)
Her fonksiyon çağrısı için bellekte bir aktivasyon kaydı (activation record ya da stack frame) açılır. Kayıtlar yığın (stack) düzeninde tutulur: son giren ilk çıkar (LIFO). main bir fonksiyon çağırınca onun kaydı main’inkinin üstüne konur; fonksiyon dönünce kaldırılır. Özyinelemenin çalışmasının sebebi budur: her çağrının kendi kaydı, yani kendi n’i vardır.
LC-3 C derleyicisinde:
| Register | Görevi |
|---|---|
| R6 | stack pointer: yığının tepesi |
| R5 | frame pointer: çalışan fonksiyonun kaydı (ilk yerel değişken) |
| R4 | global pointer: global değişkenlerin bölgesi |
| R7 | JSR’ın yazdığı dönüş adresi |
Yığın düşük adreslere doğru büyür: push = R6’yı bir azalt, sonra yaz.
Aktivasyon kaydının yapısı
düşük adres
+--------------------+
R5 - 1 → | 2. yerel değişken |
R5 → | 1. yerel değişken |
R5 + 1 | dynamic link | çağıranın R5'i
R5 + 2 | return address | çağıranın R7'si
R5 + 3 | return value |
R5 + 4 | 1. argüman | çağıran itti
R5 + 5 | 2. argüman |
+--------------------+
yüksek adres
Çağırma adımları
Çağıran (caller):
- Argümanları sağdan sola yığına it (böylece 1. argüman tepeye en yakın olur).
JSR fonksiyon.- Dönüşte tepedeki dönüş değerini oku, dönüş değerini ve argümanları yığından at.
Çağrılan (callee):
- Dönüş değeri için yer aç (R6 - 1).
- R7’yi it (dönüş adresi).
- R5’i it (dynamic link).
- R5 ← R6 - 1 (yeni çerçeve), yerel değişkenler için yer aç.
- Gövdeyi çalıştır; yerellere
LDR/STR Rx, R5, #-k, argümanlaraLDR Rx, R5, #4+kile eriş. - Dönüş değerini R5 + 3’e yaz, yerelleri at, R5 ve R7’yi geri yükle,
RET.
Çözümlü örnek: add2(3, 9)
int add2(int a, int b) { int s; s = a + b; return s; }
/* main: r = add2(3, 9); */
Simülatörde, başlangıçta R6 = R5 = x4000. add2’nin içinde s = a + b satırından hemen sonra yığın:
adres içerik R5'e göre anlamı
x3FFA x000C R5 + 0 s = 12
x3FFB x4000 R5 + 1 dynamic link (main'in R5'i)
x3FFC x300B R5 + 2 return address (JSR'dan sonraki komut)
x3FFD x0000 R5 + 3 return value (henüz yazılmadı)
x3FFE x0003 R5 + 4 a = 3
x3FFF x0009 R5 + 5 b = 9
R5 = x3FFA, R6 = x3FFA
Gövdenin LC-3 karşılığı:
LDR R0, R5, #4 ; a
LDR R1, R5, #5 ; b
ADD R0, R0, R1
STR R0, R5, #0 ; s = a + b
Dönüşten sonra main R0 = 12’yi tepeden okur, R6 tekrar x4000 olur.
Özyineleme (recursion)
Özyinelemeli bir fonksiyonun iki parçası var:
- Temel durum (base case): özyineleme olmadan cevaplanan küçük durum.
- Özyinelemeli adım: problemi küçültüp kendini çağırma.
Temel durum yoksa ya da problem küçülmüyorsa yığın taşar (stack overflow).
int fact(int n) {
if (n <= 1) return 1; /* temel durum */
return n * fact(n - 1); /* küçültülmüş problem */
}
fact(4)
= 4 * fact(3)
= 4 * (3 * fact(2))
= 4 * (3 * (2 * fact(1)))
= 4 * (3 * (2 * 1))
= 24
En derin anda yığında dört fact kaydı ve main’in kaydı vardır.
Basma sırası: önce mi sonra mı?
void p(int n) {
if (n == 0) return;
printf("%d ", n); /* inerken */
p(n - 1);
printf("%d ", n); /* çıkarken */
}
p(3):
3 2 1 1 2 3
Kural: özyinelemeli çağrıdan önceki iş inerken sırayla, sonraki iş yığın açılırken ters sırayla yapılır.
Çağrı ağacı
İki kez kendini çağıran fonksiyonlarda bir ağaç çiz:
fib(4)
/ \
fib(3) fib(2)
/ \ / \
fib(2) fib(1) fib(1) fib(0)
/ \
fib(1) fib(0)
düğüm sayısı = 9 çağrı, fib(4) = 3
fib(5) için 15 çağrı. Aynı alt problemler (fib(2) gibi) tekrar tekrar hesaplanır.
Sık yapılan hatalar
| Yanlış düşünce | Doğrusu |
|---|---|
| swap(x, y) x ile y’yi değiştirir | Kopyalar değişir; pointer gerekir |
| static local her çağrıda 0’lanır | Bir kez başlar, değerini korur |
| Yığın yukarı (büyük adrese) büyür | LC-3’te düşük adrese doğru |
| R6 frame pointer | R6 stack pointer, R5 frame pointer |
| İlk argüman R5 + 3’te | R5 + 3 dönüş değeri; ilk argüman R5 + 4 |
| Argümanlar soldan sağa itilir | Sağdan sola; 1. argüman tepeye yakın |
| Özyinelemeli çağrıdan sonraki printf aynı sırada | Ters sırada (yığın açılırken) |
| Temel durum olmadan da biter | Sonsuz özyineleme, stack overflow |
Kendini test et
Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.
What is printed?
void swap(int a, int b) { int t = a; a = b; b = t; }
int main(void) {
int x = 1, y = 2;
swap(x, y);
printf("%d %d\n", x, y);
return 0;
}Cevabı göster
C'de argümanlar değerle geçer (call by value): a ve b, x ve y'nin kopyalarıdır. swap kendi kopyalarını değiştirir, x ve y'ye dokunmaz. Gerçek takas için pointer gerekir (13. hafta).
What is printed?
int f(int x) { x = x * 2; return x + 1; }
int main(void) {
int a = 5;
int b = f(a);
printf("%d %d\n", a, b);
return 0;
}Cevabı göster
f, a'nın kopyasıyla çalışır: x = 10, dönüş 11. a hâlâ 5.
What is printed?
int counter(void) { static int c = 0; c++; return c; }
int plain(void) { int c = 0; c++; return c; }
int main(void) {
counter();
counter();
printf("%d %d\n", counter(), plain() + plain());
return 0;
}Cevabı göster
static yerel değişken bir kez başlatılır ve çağrılar arasında değerini korur (yığında değil, global veri bölgesinde durur). Üçüncü çağrı 3 döndürür. plain'deki c her çağrıda yeniden 0 olur, her çağrı 1 döner: 1 + 1 = 2.
What is printed?
int fact(int n) {
if (n <= 1) return 1;
return n * fact(n - 1);
}
/* in main: */ printf("%d\n", fact(5));Cevabı göster
fact(5) = 5 · fact(4) = 5 · 4 · fact(3) = … = 5 · 4 · 3 · 2 · 1 = 120. Temel durum (base case) n ≤ 1.
What is printed by p(3)?
void p(int n) {
if (n == 0) return;
printf("%d ", n);
p(n - 1);
printf("%d ", n);
}Cevabı göster
Her çağrı önce kendi n'ini basar, sonra bir alt çağrı yapar, alt çağrı bitince n'ini tekrar basar. İnerken 3 2 1, p(0) hemen döner, çıkarken 1 2 3. İkinci printf'ler yığın açılırken (LIFO) ters sırada çalışır.
With the recursive fib below, what is fib(5), and how many times is fib called in total (including the first call)?
int fib(int n) {
if (n < 2) return n;
return fib(n - 1) + fib(n - 2);
}Cevabı göster
Çağrı sayısı C(n) = 1 + C(n-1) + C(n-2), C(0) = C(1) = 1. C(2) = 3, C(3) = 5, C(4) = 9, C(5) = 15. Değer: fib(5) = 5. Aynı alt problemler tekrar tekrar hesaplanır; bu yüzden bu yöntem yavaştır.
main calls fact(4) (the function above). What is the maximum number of activation records on the runtime stack at the same time, counting main?
Cevabı göster
fact(4) → fact(3) → fact(2) → fact(1). fact(1) çalışırken dört fact kaydı ve main'in kaydı yığındadır: 5. fact(1) temel durumda döner, kayıtlar ters sırada kaldırılır.
In the LC-3 C calling convention, which registers are the stack pointer, the frame pointer, and the global data pointer?
Cevabı göster
R6 her zaman yığının tepesini gösterir. R5 o an çalışan fonksiyonun aktivasyon kaydını (ilk yerel değişkenini) gösterir; yerel değişkenler R5'e göre offset'le okunur. R4 global değişkenlerin bölgesini gösterir. R7 dönüş adresi için kullanılır.
List what an LC-3 activation record contains.
Cevabı göster
Yerel değişkenler (R5+0, R5-1, …), dynamic link (çağıranın R5'i, R5+1), return address (çağıranın R7'si, R5+2), return value için yer (R5+3) ve argümanlar (R5+4'ten itibaren). Argümanları çağıran, gerisini çağrılan fonksiyon yığına koyar.
add2(3, 9) is called with the LC-3 convention. Inside add2, R5 = x3FFA. At which addresses are the parameters a and b, and what does x3FFB contain?
int add2(int a, int b) { int s; s = a + b; return s; }Cevabı göster
İlk parametre R5 + 4 = x3FFE (3), ikinci R5 + 5 = x3FFF (9). Argümanlar sağdan sola itildiği için a tepeye daha yakındır.
x3FFB = R5 + 1: dynamic link (simülasyonda x4000). x3FFC = R5 + 2: dönüş adresi, x3FFD = R5 + 3: dönüş değeri, x3FFA = R5 + 0: s = 12.
What is printed?
int dsum(int n) { if (n < 10) return n; return n % 10 + dsum(n / 10); }
int power(int b, int e) { if (e == 0) return 1; return b * power(b, e - 1); }
/* in main: */ printf("%d %d\n", dsum(4705), power(3, 4));Cevabı göster
dsum(4705) = 5 + dsum(470) = 5 + 0 + dsum(47) = 5 + 0 + 7 + 4 = 16. power(3, 4) = 3 · 3 · 3 · 3 · 1 = 81.
What is printed?
void rev(int n) { if (n == 0) return; printf("%d", n % 10); rev(n / 10); }
void up(int n) { if (n == 0) return; up(n / 10); printf("%d", n % 10); }
/* in main: */ rev(123); printf(" "); up(123);Cevabı göster
rev, basamağı ÖNCE basar, sonra özyineler: son basamaktan başlar → 321. up, önce özyineler, dönüşte basar: en derindeki çağrı ilk basamağı (1) basar → 123. printf'in özyinelemeli çağrıdan önce mi sonra mı olduğu sırayı belirler.
Doğrulama: C çıktıları Zig cc (clang, C99) ile derlenip çalıştırıldı (sorular verify/SEN2201/c/w12.c, gövde örnekleri verify/SEN2201/c/w13.c başında). LC-3 aktivasyon kaydı, kitabın çağırma kuralına göre elle yazılmış assembly ile kendi simülatörümüzde çalıştırıldı ve yığının bellek dökümü alındı (verify/SEN2201/12-stack.mjs).