SEN2201 · 12. hafta · Programming in C

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.

functionprototypeargumentparametercall by valuereturn valuestatic local variableruntime stackactivation record (stack frame)frame pointer (R5)stack pointer (R6)global pointer (R4)dynamic linkreturn addressrecursionbase case

Ş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);.
  • void dö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ü
Localyığında, aktivasyon kaydındafonksiyon çalıştığı sürece
Globalglobal veri bölgesinde (R4)program boyunca
static localglobal veri bölgesindeprogram 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:

RegisterGörevi
R6stack pointer: yığının tepesi
R5frame pointer: çalışan fonksiyonun kaydı (ilk yerel değişken)
R4global pointer: global değişkenlerin bölgesi
R7JSR’ı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):

  1. Argümanları sağdan sola yığına it (böylece 1. argüman tepeye en yakın olur).
  2. JSR fonksiyon.
  3. Dönüşte tepedeki dönüş değerini oku, dönüş değerini ve argümanları yığından at.

Çağrılan (callee):

  1. Dönüş değeri için yer aç (R6 - 1).
  2. R7’yi it (dönüş adresi).
  3. R5’i it (dynamic link).
  4. R5 ← R6 - 1 (yeni çerçeve), yerel değişkenler için yer aç.
  5. Gövdeyi çalıştır; yerellere LDR/STR Rx, R5, #-k, argümanlara LDR Rx, R5, #4+k ile eriş.
  6. 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:

  1. Temel durum (base case): özyineleme olmadan cevaplanan küçük durum.
  2. Ö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üşünceDoğrusu
swap(x, y) x ile y’yi değiştirirKopyalar değişir; pointer gerekir
static local her çağrıda 0’lanırBir kez başlar, değerini korur
Yığın yukarı (büyük adrese) büyürLC-3’te düşük adrese doğru
R6 frame pointerR6 stack pointer, R5 frame pointer
İlk argüman R5 + 3’teR5 + 3 dönüş değeri; ilk argüman R5 + 4
Argümanlar soldan sağa itilirSağdan sola; 1. argüman tepeye yakın
Özyinelemeli çağrıdan sonraki printf aynı sıradaTers sırada (yığın açılırken)
Temel durum olmadan da biterSonsuz özyineleme, stack overflow

Kendini test et

Sınavdaki gibi İngilizce sorular. Önce kâğıt kalemle çöz, sonra cevabı aç.

Soru 1kolay

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

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

Soru 2kolay

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
5 11

f, a'nın kopyasıyla çalışır: x = 10, dönüş 11. a hâlâ 5.

Soru 3orta

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

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.

Soru 4kolay

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
120

fact(5) = 5 · fact(4) = 5 · 4 · fact(3) = … = 5 · 4 · 3 · 2 · 1 = 120. Temel durum (base case) n ≤ 1.

Soru 5orta

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
3 2 1 1 2 3

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.

Soru 6zor

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
5, 15 calls

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

Soru 7orta

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
5

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.

Soru 8kolay

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 stack pointer, R5 frame pointer, R4 global pointer

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.

Soru 9orta

List what an LC-3 activation record contains.

Cevabı göster
Local variables, dynamic link, return address, return value, arguments

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.

Soru 10zor

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
a at x3FFE, b at x3FFF; x3FFB holds the caller's R5 (dynamic link)

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

Soru 11orta

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
16 81

dsum(4705) = 5 + dsum(470) = 5 + 0 + dsum(47) = 5 + 0 + 7 + 4 = 16. power(3, 4) = 3 · 3 · 3 · 3 · 1 = 81.

Soru 12zor

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
321 123

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

Deftere not kartı

Call by value: parametre = KOPYA. swap(int,int) çalışmaz -> pointer lazım return tek değer döndürür | void fonksiyon değer döndürmez static local: bir kez başlar, çağrılar arası değerini korur (yığında değil) Runtime stack: LIFO, LC-3'te düşük adrese doğru büyür R6 = stack pointer (tepe) | R5 = frame pointer (ilk local) | R4 = global pointer | R7 = dönüş adresi Aktivasyon kaydı (R5'e göre): local R5+0,-1.. | dynamic link R5+1 | return addr R5+2 | return value R5+3 | arg1 R5+4, arg2 R5+5 Çağıran: argümanları SAĞDAN SOLA it, JSR, dönüşte değeri al, pop Çağrılan: dönüş değeri yeri, R7 it, R5 it, R5=R6-1, local ayır ... geri al, RET Özyineleme: temel durum + kendini küçültülmüş problemle çağır print önce/sonra: önce -> inerken (ters değil), sonra -> çıkarken (ters sıra) fib(n) çağrı sayısı: 1,1,3,5,9,15 (n=0..5) | fact(n) derinlik n (+ main)