C Scope ve Lifetime: Değişkenler Neden Bazen Kaybolur?

C Scope ve Lifetime

C Scope ve Lifetime kavramları ne diye düşünüyorsunuz değil mi ? Aslında Bu kavramı şöyle açıklayabiliriz : C yazarken “Az önce vardı, şimdi yok!” dediğiniz değişkenler aslında kaybolmaz; scope (görünürlük alanı) ve lifetime (yaşam süresi) kuralları bittiği için erişilemez hâle gelir. Bu yazıda, C scope ve lifetime ilişkisini netleştirip, en sık görülen hataları (dangling pointer, use-after-free, stack adresini döndürmek vb.) örneklerle göstereceğim.

Sen de şu durumları yaşadın mı?

  • Fonksiyon içinde tanımladığın değişken fonksiyondan çıkınca “bozuldu” mu?
  • Bir pointer bir süre sonra rastgele değerler mi göstermeye başladı?
  • Global değişken her yerden erişiliyor ama “neden bazen tehlikeli?” diye düşündün mü?

Yorumlara kendi örneğini bırak; birlikte teşhis edelim.

1) Scope Nedir? Değişkeni Nereden Görebilirsin?

C Scope Lifetime
C Scope Lifetime

C Scop ve Lifetime kavramlarını incelemeye başlarken ilk kavramdan yani Scop’dan başlamamız olayın tam mantığını anlamamız için en değerli şeydir.

Scope, bir ismin (değişkenin) kodun hangi bölümünden erişilebilir olduğunu söyler. C’de temel olarak:

  • Block scope: Süslü parantez { } içi (if/for/while/fonksiyon gövdesi).
  • Function scope: Özellikle goto etiketleri gibi özel durumlar.
  • File scope: Dosya seviyesinde (global) tanımlananlar.

1.1 Block scope (en yaygını)

Bir blok içinde tanımlanan değişken, blok bittiğinde artık isim olarak görünmez.

// Block scope örneği
#include <stdio.h>

int main(void) {
    if (1) {
        int x = 42;               // x sadece bu blokta görünür
        printf("%d\n", x);
    }
    // printf("%d\n", x);         // HATA: x burada scope dışında
    return 0;
}

Burada değişken “kaybolmadı”; sadece scope dışında kaldı. Derleyici zaten erişmene izin vermez.

1.2 Shadowing: Aynı isimle değişken “üstünü örter”

Dışarıdaki değişkenin adı, içeride tekrar kullanılırsa içteki değişken dıştakini shadow eder. Bu da “ben bunu değiştirdim sanıyordum” hatalarının kaynağıdır.

// Shadowing örneği
#include <stdio.h>

int main(void) {
    int count = 10;

    for (int i = 0; i < 1; i++) {
        int count = 99; // dıştaki count'u shadow eder
        printf("icerde: %d\n", count);
    }

    printf("disarda: %d\n", count);
    return 0;
}

Çıktı: içeride 99, dışarıda 10. Burada lifetime değil, tamamen scope kaynaklı bir sürpriz var.

2) Lifetime Nedir? Değişken Ne Zaman “Yaşar” ve Ne Zaman Ölür?

C Scope ve Lifetime
C Scope ve Lifetime

C Scope ve Lifetime kavramlarının ikincisi olan Lifetime; bir nesnenin bellekte geçerli olduğu zaman aralığıdır. C’de bunu belirleyen şey genelde storage duration (saklama süresi) olur:

  • Automatic (stack): Blok boyunca yaşar, blok bitince ölür.
  • Static: Program çalıştığı sürece yaşar.
  • Dynamic (heap): malloc ile başlar, free ile biter.

2.1 Automatic (stack) lifetime: “Fonksiyondan çıkınca bozuldu”

C’de Fonksiyon içindeki normal yerel değişkenler genelde stack’te tutulur ve blok bitince yaşamları biter.

// Stack değişkeninin lifetime'ı
#include <stdio.h>

void foo(void) {
    int a = 5;               // automatic storage
    printf("foo: %d\n", a);
} // burada a'nin lifetime'ı biter

int main(void) {
    foo();
    return 0;
}

Bu örnekte sorun yok. Sorun, stack’teki nesnenin adresini dışarı taşıyınca başlar.

2.2 KLASİK HATA: Stack adresini döndürmek (dangling pointer)

Aşağıdaki kod, “değişken kayboldu” hissinin en saf hâlidir. Aslında pointer “var”, ama işaret ettiği nesne öldü.

// YANLIŞ: Yerel değişken adresi döndürmek
#include <stdio.h>

int* bad(void) {
    int x = 123;     // stack'te
    return &x;        // x'in lifetime'ı fonksiyon sonunda biter
}

int main(void) {
    int *p = bad();
    printf("%d\n", *p); // Tanımsız davranış (undefined behavior)
    return 0;
}

Bu undefined behavior (tanımsız davranış) üretir: bazen “çalışır gibi” görünür, bazen saçmalar, bazen çöker.

Doğru yaklaşım 1: Heap kullan

// DOĞRU: Heap üstünde değer döndürmek
#include <stdio.h>
#include <stdlib.h>

int* good_heap(void) {
    int *p = malloc(sizeof *p);
    if (!p) return NULL;
    *p = 123;
    return p; // lifetime free edilene kadar sürer
}

int main(void) {
    int *p = good_heap();
    if (p) {
        printf("%d\n", *p);
        free(p); // lifetime burada biter
    }
    return 0;
}

Doğru yaklaşım 2: Static değişken kullan

// DOĞRU: static ile program boyunca yaşayan nesne
#include <stdio.h>

int* good_static(void) {
    static int x = 123; // static storage: program sonuna kadar yaşar
    return &x;
}

int main(void) {
    int *p = good_static();
    printf("%d\n", *p);
    return 0;
}

Not: Static çözüm “tek bir değişken” döndürür; çoklu çağrıda aynı adres kullanılır. Thread-safe olmayabilir; dikkat.

2.3 Dynamic lifetime: use-after-free ve double free

Heap’te nesne yaşar ama sen free edince ölür. Sonra o pointer’ı kullanmak, “değişken bozuldu” gibi görünür ama gerçek sebep bellidir: nesne artık yok.

// use-after-free örneği
#include <stdio.h>
#include <stdlib.h>

int main(void) {
    int *p = malloc(sizeof *p);
    if (!p) return 1;

    *p = 77;
    free(p);          // lifetime bitti

    // printf("%d\n", *p); // YANLIŞ: use-after-free (undefined behavior)

    p = NULL;         // iyi pratik: dangling pointer riskini azaltır
    return 0;
}

3) Scope ≠ Lifetime: Biri Görünürlük, Biri Ömür

C Scope ve Lifetime
C Scope ve Lifetime

C Scope ve Lifetime kavramlarını ayırmak bazen kafa karıştırıcı olsada aslında çok kolay ve basit bir şekilde ayrılabiliyor.

En kritik ayrım: Bir değişkenin scope’u bitse bile lifetime’ı devam edebilir (static/global), ya da scope devam ediyor gibi görünse bile lifetime bitmiş olabilir (free edilmiş heap).

3.1 Scope biter, lifetime devam eder: static ve global

// Scope ile lifetime ayrımı (static)
#include <stdio.h>

void counter(void) {
    static int c = 0; // yalnızca bu fonksiyon scope'unda görünür
    c++;              // ama lifetime program boyunca sürer
    printf("c=%d\n", c);
}

int main(void) {
    counter();
    counter();
    counter();
    return 0;
}

Burada c sadece counter içinde görünür (scope dar), ama her çağrıda kaldığı yerden devam eder (lifetime uzun).

3.2 Scope var, lifetime yok: free sonrası hâlâ pointer elde

Pointer değişkenin scope’u sürüyor olabilir; ama işaret ettiği heap nesnesinin lifetime’ı bitmiş olabilir. Bu ayrımı zihnine oturtursan hataların %70’i çözülür.

4) “Değişken Kayboluyor” Hissinin 6 Yaygın Sebebi

Gemini Generated Image a5vi3ba5vi3ba5vi

  • Dangling pointer: Stack adresini dışarı taşımak veya free sonrası kullanmak.
  • Shadowing: İç blokta aynı isimle yeni değişken tanımlamak.
  • Uninitialized variable: Değer atanmadan okumak (rastgele gibi görünür).
  • Buffer overflow: Dizi taşması komşu değişkenleri bozar.
  • Use-after-free / double free: Heap ömrünü yanlış yönetmek.
  • Data race: Thread’ler aynı veriye kilitsiz erişince “bazen” bozulur.

4.1 Buffer overflow ile “yanındaki değişken bozuldu” örneği

// DİKKAT: Buffer overflow örneği (tehlikeli kod)
#include <stdio.h>
#include <string.h>

int main(void) {
    char name[4];
    int pin = 1234;

    strcpy(name, "ismet"); // 4 byte'ı aşar, pin'i bozabilir (undefined behavior)
    printf("pin=%d\n", pin);
    return 0;
}

Bu tür hatalar “değişken kayboldu/bozuldu” diye rapor edilir ama kök sebep bellek taşmasıdır. strncpy, sınır kontrolü ve daha güvenli fonksiyonlar kullan.

5) Debug İpuçları: Scope ve Lifetime Hataları Nasıl Yakalanır?

undefined behavior C
undefined behavior C

5.1 Derleyici uyarılarını aç

Eğer C Scope ve Lifetime  kavramları ile çalışırken derleyici hatalarını okumak çok kıymetlidir. İşte bunu şöyle yapın;

Günlük C geliştirmede şu bayraklar hayat kurtarır:

gcc -std=c11 -Wall -Wextra -Wshadow -Wconversion -O0 -g main.c -o app
  • -Wshadow: Shadowing yakalar.
  • -g: Debug sembolleri.
  • -O0: Optimizasyonu kapatır; debug daha anlaşılır olur.

5.2 Sanitizer’lar ile undefined behavior avı

gcc -std=c11 -Wall -Wextra -O0 -g -fsanitize=address,undefined main.c -o app
./app

AddressSanitizer ve UBSan; use-after-free, stack-use-after-return, buffer overflow gibi “bazen olan” hataları anında raporlar.

5.3 “Benim değişkenim neden değişti?” kontrol listesi

  • Bu değişkenin scope’u hangi { } içinde?
  • Bu değişkenin lifetime’ı automatic mi, static mi, heap mi?
  • Adresini bir yere kaydettim mi? O adresin işaret ettiği nesne hâlâ yaşıyor mu?
  • Yanlışlıkla aynı isimle shadowing yaptım mı?
  • Yakında bir dizi/strcpy gibi taşma riski var mı?

Sonuç: C’de “Kaybolan Değişken” Diye Bir Şey Yok, Kural Var

C’de değişkenlerin kaybolması gibi görünen şeyler, çoğunlukla C scope ve lifetime kurallarını ihlal etmekten veya belleği yanlış kullanmaktan doğar. Eğer bir değişkene erişemiyorsan scope bitmiştir; erişebiliyor ama değeri saçmalıyorsa lifetime bitmiş (veya bellek bozulmuş) olabilir.

Sen en çok hangi tür hataya denk geliyorsun: stack adresi döndürmek mi, use-after-free mi, shadowing mi? Yorumlarda kısa bir kod parçası paylaşırsan birlikte analiz edebilirim.

Bu yazıyla birlikte C Scope ve Lifetime kavramlarını oldukça detaylı bir şekilde incelemiş olduk. Burada yaşayabileceğiniz hatalar , kafanızı karıştırabilecek soruları elimden geldikçe detaylandırarak anlatmaya çalıştım. Umarım verimli olmuştur. Bu sayde C Scope ve Lifetime kavramlarınıda bitirmiş olduk .

SSS (FAQ)

1)C Scope ve lifetime aynı şey mi?

Hayır. Scope “nereden erişirim”, lifetime “ne kadar yaşar” sorusunun cevabıdır. Pointer örneklerinde bu fark çok belirgindir.

2) Fonksiyondan yerel değişken adresi döndürmek neden yanlış?

Çünkü yerel değişken genelde stack’te yaşar ve fonksiyon bitince lifetime biter. Pointer kalır ama işaret ettiği nesne ölür (dangling pointer).

3) static değişken ne zaman tercih edilmeli?

Fonksiyon çağrıları arasında durumu korumak istediğinde iş görür. Ama tek bir paylaşılan örnek olduğu için yeniden giriş (reentrancy) ve çoklu thread senaryolarında risklidir.

4) Heap kullanıyorsam sorun bitti mi?

Hayır. Heap’te lifetime’ı sen yönetirsin: malloc ile başlar, free ile biter. Use-after-free ve memory leak en yaygın hatalardır.

5) “Bazen çalışıyor, bazen bozuluyor” hangi sınıfa girer?

Genelde undefined behavior. Stack adresini döndürme, buffer overflow, uninitialized okuma, use-after-free gibi hatalar “bazen” kendini böyle gösterir.

Kaynakça:

İlgili Yazılar