← Kursa Dön
📄 Text · 12 min

inline, Scope ve Lifetime

Bir evin odalarını düşün. Her odanın kendi eşyaları var — yatak odasındaki yastık salon da mevcut değil. Kapıdan çıkınca odadaki eşyalara erişemezsin. Programlamada scope (kapsam) tam olarak bu: bir değişkenin görülebilir ve erişilebilir olduğu kod bölgesi. Lifetime (ömür) ise o değişkenin bellekte ne kadar süre yaşadığı. Bu derste ayrıca inline fonksiyonları ve static local değişkenleri de öğreneceksin.


inline Fonksiyonlar

Normal bir fonksiyon çağrısında şunlar olur: argümanlar stack'e yerleştirilir, program akışı fonksiyona atlar, fonksiyon çalışır, dönüş değeri geri aktarılır ve akış çağrılan yere döner. Bu adımlar küçük ama her çağrıda tekrarlanır.

inline fonksiyonlar derleyiciye bir öneridir: "Bu fonksiyonu çağırmak yerine kodunu doğrudan çağrı noktasına yerleştir." Bu, fonksiyon çağrı maliyetini ortadan kaldırır.

#include <iostream>
using namespace std;

inline int kare(int x) {
    return x * x;
}

inline int maksimum(int a, int b) {
    return (a > b) ? a : b;
}

int main() {
    // Derleyici bunu şöyle dönüştürebilir:
    // int sonuc = 5 * 5;  (fonksiyon çağrısı yok)
    int sonuc = kare(5);
    cout << "5² = " << sonuc << endl;

    cout << "Max: " << maksimum(10, 20) << endl;

    return 0;
}

inline Hakkında Önemli Noktalar

  • inline bir öneridir, zorunluluk değil. Derleyici inline yapmayı uygun görmezse (mesela fonksiyon çok büyükse) normal çağrı yapar.

  • Modern derleyiciler -O2 ve üstü optimizasyon seviyelerinde inline yazmasan bile küçük fonksiyonları otomatik inline eder.

  • inline anahtar kelimesinin asıl pratik anlamı: aynı fonksiyon tanımını birden fazla çeviri biriminde (translation unit / .cpp dosyası) kullanabilmeyi sağlar. Bu header dosyalarında fonksiyon tanımlamak için kritik.

// utils.h — inline olmadan birden fazla .cpp'den include edilirse
// "multiple definition" linker hatası alırsın
inline double daireAlani(double r) {
    return 3.14159265 * r * r;
}

Ne Zaman inline Kullan?

  • Küçük, basit fonksiyonlar: 1-5 satır, sık çağrılan

  • Header dosyasındaki tanımlar: Birden fazla .cpp'den include edilecekse

  • constexpr fonksiyonlar: Zaten örtük olarak inline

// constexpr — derleme zamanında hesaplanabilir, örtük inline
constexpr int kup(int x) {
    return x * x * x;
}

💡 İpucu: Modern C++'ta inline anahtar kelimesini performans için değil, header'da tanım yazmak için kullan. Performans optimizasyonunu derleyiciye bırak — derleyici bu konuda insandan çok daha iyi karar verir.


Local Scope — Yerel Kapsam

Bir değişken süslü parantez {} içinde tanımlandığında, sadece o blokta erişilebilir. Blok bittiğinde değişken yok olur.

#include <iostream>
using namespace std;

int main() {
    int x = 10;  // main'in scope'u

    if (x > 5) {
        int y = 20;  // if bloğunun scope'u
        cout << x << endl;  // OK — x üst scope'ta
        cout << y << endl;  // OK — y bu scope'ta
    }

    cout << x << endl;  // OK
    // cout << y << endl;  // DERLEME HATASI — y burada erişilemez!

    return 0;
}

Fonksiyon parametreleri ve içinde tanımlanan değişkenler local scope'a sahiptir:

void hesapla(int n) {        // n: local scope
    int sonuc = n * 2;       // sonuc: local scope
    cout << sonuc << endl;
}
// n ve sonuc burada mevcut değil

Döngü Değişkenleri

for döngüsünde tanımlanan değişken sadece döngü içinde yaşar:

for (int i = 0; i < 5; i++) {
    cout << i << endl;  // OK
}
// cout << i << endl;  // HATA — i burada yok

// while ve do-while'da değişken dışarıda tanımlanır
int j = 0;
while (j < 5) {
    cout << j << endl;
    j++;
}
cout << j << endl;  // OK — j hâlâ erişilebilir (değeri 5)

Global Scope — Evrensel Kapsam

Tüm fonksiyonların dışında tanımlanan değişkenler global scope'a sahiptir. Programın her yerinden erişilebilir.

#include <iostream>
using namespace std;

int sayac = 0;  // Global değişken

void arttir() {
    sayac++;  // Global değişkene erişim
}

void yazdir() {
    cout << "Sayaç: " << sayac << endl;
}

int main() {
    arttir();
    arttir();
    arttir();
    yazdir();  // Sayaç: 3

    return 0;
}

Global Değişkenlerin Tehlikeleri

Global değişkenler kullanışlı görünür ama büyük projelerde ciddi sorunlara yol açar:

  • Kim değiştirdi? Herhangi bir fonksiyon her an değiştirebilir — hata ayıklama kabusu

  • İsim çakışması: Farklı dosyalarda aynı isimde global değişken tanımlanabilir

  • Test edilemezlik: Fonksiyonlar global duruma bağımlı olunca birim testi zorlaşır

  • Thread güvenliği: Çok iş parçacıklı programlarda yarış koşulları oluşur

⚠️ Dikkat: Global değişkenlerden mümkün olduğunca kaçın. Veriye ihtiyaç duyan fonksiyonlara parametre olarak geçir. Global değişken kullanman gereken ender durumlar: sabitler (const/constexpr) ve konfigürasyon ayarları.

// İYİ — global sabit
constexpr double PI = 3.14159265358979;
const int MAX_KULLANICI = 1000;

// KÖTÜ — global değişken (değiştirilebilir)
int aktifKullaniciSayisi = 0;  // Kim, ne zaman değiştirecek?

Block Scope ve Variable Shadowing

İç içe bloklarda aynı isimde değişken tanımlayabilirsin. İç bloktaki değişken dış bloktakini gölgeler (shadow). Bu demek ki iç blokta dış değişkene erişemezsin — aynı isim iç bloktaki değişkeni ifade eder.

#include <iostream>
using namespace std;

int x = 100;  // Global

int main() {
    int x = 50;  // Local — global x'i gölgeler
    cout << "main x: " << x << endl;  // 50

    {
        int x = 10;  // İç blok — üst scope'taki x'i gölgeler
        cout << "iç blok x: " << x << endl;  // 10
    }

    cout << "main x (tekrar): " << x << endl;  // 50 — iç blok bitti

    // Global x'e erişmek için scope resolution operatörü
    cout << "global x: " << ::x << endl;  // 100

    return 0;
}

::x söz dizimi scope resolution operatörü ile global scope'taki x'e doğrudan erişir.

Shadowing Tehlikeli mi?

Çoğu zaman evet. Farklı scope'larda aynı isimli değişkenler kafa karıştırır ve hataya yol açar. Derleyicilere -Wshadow uyarısını açarak bu durumları yakalayabilirsin.

// KÖTÜ — gölgeleme, kafa karıştırıcı
void hesapla(int deger) {
    for (int i = 0; i < 10; i++) {
        int deger = i * 2;  // Parametre gölgelendi!
        cout << deger << endl;  // Hangisi? İç bloktaki!
    }
    cout << deger << endl;  // Parametredeki orijinal
}

// İYİ — farklı isimler, açık ve net
void hesapla_temiz(int giris) {
    for (int i = 0; i < 10; i++) {
        int sonuc = i * 2;
        cout << sonuc << endl;
    }
    cout << giris << endl;
}

💡 İpucu: Derleyici uyarılarını aç: g++ -Wall -Wshadow. Shadow uyarıları çok değerli bug'ları erken yakalar.


static Local Değişkenler

Normal local değişkenler fonksiyon her çağrıldığında oluşturulur ve fonksiyon bitince yok edilir. static local değişkenler ise farklıdır: fonksiyon ilk çağrıldığında bir kez oluşturulur ve program bitene kadar yaşar. Sonraki çağrılarda aynı değişken kullanılır — değeri korunur.

#include <iostream>
using namespace std;

void sayacFonksiyon() {
    static int sayac = 0;  // Sadece ilk çağrıda 0'a ayarlanır
    sayac++;
    cout << "Çağrı sayısı: " << sayac << endl;
}

int main() {
    sayacFonksiyon();  // Çağrı sayısı: 1
    sayacFonksiyon();  // Çağrı sayısı: 2
    sayacFonksiyon();  // Çağrı sayısı: 3
    sayacFonksiyon();  // Çağrı sayısı: 4

    return 0;
}

static int sayac = 0; satırı sadece bir kez çalışır. Sonraki çağrılarda atlanır ve sayac'ın önceki değeri korunur.

static Local Ne Zaman Kullanılır?

  • Çağrı sayacı: Fonksiyonun kaç kez çağrıldığını takip etmek

  • Lazy initialization: Pahalı bir nesneyi ilk ihtiyaç anında oluşturmak

  • Singleton pattern: Tek bir nesne örneğini garanti etmek

#include <iostream>
#include <string>
using namespace std;

// Lazy initialization — sadece ilk çağrıda oluşturulur
const string& getConfig() {
    static string config = "Varsayılan ayarlar yüklendi";
    // Pahalı başlatma sadece ilk çağrıda yapılır
    return config;
}

// Benzersiz ID üreteci
int benzersizId() {
    static int sonId = 0;
    return ++sonId;
}

int main() {
    cout << getConfig() << endl;

    cout << "ID: " << benzersizId() << endl;  // 1
    cout << "ID: " << benzersizId() << endl;  // 2
    cout << "ID: " << benzersizId() << endl;  // 3

    return 0;
}

Lifetime — Değişkenlerin Ömrü

C++'ta üç tür ömür vardır:

1. Automatic Lifetime (Otomatik Ömür)

Local değişkenler — tanımlandığında oluşur, scope bitince yok olur.

void fonksiyon() {
    int x = 10;      // x oluşturuldu
    string s = "hi";  // s oluşturuldu
}                      // x ve s yok edildi

Bu en yaygın ve en güvenli ömür türüdür. Bellek yönetimi otomatiktir — sen bir şey yapmana gerek yok.

2. Static Lifetime (Statik Ömür)

Global değişkenler ve static local değişkenler — program başında oluşur (veya ilk kullanımda), program bitince yok olur.

int globalDegisken = 42;  // Program başında oluşur

void fonksiyon() {
    static int sayac = 0;  // İlk çağrıda oluşur
    sayac++;
}
// Her ikisi de program sonuna kadar yaşar

3. Dynamic Lifetime (Dinamik Ömür)

new ile oluşturulan nesneler — programcı oluşturur, programcı yok etmeli (delete). Bunu ilerideki derslerde detaylı öğreneceksin.

void fonksiyon() {
    int* p = new int(42);  // Dinamik ömür — programcı yönetir
    // ... kullan ...
    delete p;               // Programcı silmeli!
}

Lifetime Özet Tablosu

TürNe Zaman OluşurNe Zaman Yok OlurÖrnek
AutomaticTanımlandığındaScope bitinceLocal değişkenler
StaticProgram başında / ilk kullanımProgram sonundaGlobal, static local
Dynamicnew çağrıldığındadelete çağrıldığındaHeap nesneleri

⚠️ Dikkat: Dynamic lifetime en tehlikeli olandır — delete unutursan bellek sızıntısı (memory leak), yanlış zamanda delete yaparsan çöken program. Modern C++'ta unique_ptr ve shared_ptr kullanarak bu riski ortadan kaldırabilirsin (ilerideki ders konusu).


Her Şeyi Bir Arada — Pratik Örnek

#include <iostream>
#include <string>
using namespace std;

// Global sabit — OK, değişmiyor
constexpr int MAX_LOG = 100;

// inline fonksiyon — küçük, sık çağrılan
inline string seviyeEtiketi(int seviye) {
    switch (seviye) {
        case 0: return "DEBUG";
        case 1: return "INFO";
        case 2: return "WARNING";
        case 3: return "ERROR";
        default: return "UNKNOWN";
    }
}

void logYaz(const string& mesaj, int seviye = 1) {
    // static local — çağrılar arası durum koruma
    static int logSayaci = 0;

    if (logSayaci >= MAX_LOG) {
        cout << "[UYARI] Log limiti aşıldı!" << endl;
        return;
    }

    logSayaci++;

    // Block scope — etiket sadece burada yaşar
    {
        string etiket = seviyeEtiketi(seviye);
        cout << "[" << logSayaci << "] ["
             << etiket << "] " << mesaj << endl;
    }
    // etiket burada erişilemez — temiz scope
}

int main() {
    logYaz("Sistem başlatıldı");
    logYaz("Kullanıcı giriş yaptı");
    logYaz("Disk alanı azalıyor", 2);
    logYaz("Veritabanı hatası", 3);

    return 0;
}

Bu örnekte:

  • MAX_LOG: global constexpr sabit (static lifetime)

  • seviyeEtiketi: inline fonksiyon

  • logSayaci: static local değişken (çağrılar arası değer korur)

  • etiket: block scope değişken (scope bitince yok olur)


Özet

  • inline fonksiyonlar derleyiciye "çağrı yerine kodu yerleştir" önerisidir — asıl pratik faydası header'da tanım yapabilmektir

  • Local scope süslü parantez {} ile belirlenir — değişken blok bitince erişilemez ve yok olur

  • Global scope fonksiyonların dışındadır — her yerden erişilebilir ama tehlikelidir, mümkünse constexpr sabitler dışında kullanma

  • Variable shadowing iç bloktaki değişkenin dış bloktakini gölgelemesidir — kafa karıştırır, -Wshadow ile yakala

  • static local değişkenler fonksiyon çağrıları arasında değerini korur — sayaç, lazy init ve singleton için kullanışlı

  • Lifetime: automatic (scope ile), static (program ömrü), dynamic (new/delete ile) — modern C++'ta smart pointer'lar dynamic ömrü güvenli hale getirir