← Kursa Dön
📄 Text · 12 min

Debugging ve Profiling Teknikleri

"Kod yazmak işin yarısı, hata bulmak diğer yarısı." Bu söz abartı değil. Deneyimli programcılar zamanlarının büyük bölümünü debugging'e harcar. Ama doğru araçları ve teknikleri bilirsen, bu süre dramatik şekilde azalır.

Debugging'i bir dedektiflik gibi düşün. Olay yeri: çöken program. İpuçları: hata mesajları, log'lar, bellek durumu. Araçların: derleyici uyarıları, debugger, sanitizer'lar. İyi bir dedektif rastgele aramaz — sistematik çalışır. Debugging de öyle.


Compiler Uyarıları — İlk Savunma Hattı

Debugging'in en ucuz ve en etkili yolu: derleyicinin sana söylediklerini dinlemek. Varsayılan olarak derleyiciler birçok uyarıyı göstermez. Bunları açıkça etkinleştirmen gerekir.

Temel Uyarı Bayrakları

# Minimum tavsiye edilen bayraklar
g++ -Wall -Wextra -Wpedantic -std=c++17 main.cpp -o main

# Daha katı: uyarilari hata olarak kabul et
g++ -Wall -Wextra -Wpedantic -Werror -std=c++17 main.cpp -o main
BayrakNe Yapar
-WallYaygın uyarıları etkinleştirir (aslında "hepsini" değil)
-WextraEkstra uyarılar (kullanılmayan parametre, vs.)
-WpedanticStandart dışı uzantıları uyarır
-WerrorTüm uyarıları hata olarak kabul eder (derleme durur)

Uyarıların Yakaladığı Hatalar

#include <iostream>

int topla(int a, int b) {
    int sonuc = a + b;
    // return unutuldu! -Wall bunu yakalar
}

int main() {
    int x;          // baslatilmamis! -Wextra uyarir
    if (x == 5) {   // tanimsiz davranis
        std::cout << "Bes!\n";
    }

    int y = 3;
    if (y = 5) {    // atama mi karsilastirma mi? -Wall uyarir
        std::cout << "Bu her zaman calisir\n";
    }

    return 0;
}

Bu kodda üç hata var ve derleyici uyarıları hepsini yakalar:

  1. topla fonksiyonunda return yok

  2. x başlatılmamış

  3. if (y = 5) muhtemelen if (y == 5) olmalıydı

💡 Her zaman `-Wall -Wextra` ile derle. Yeni proje mi başlıyorsun? -Werror de ekle — uyarı birikmeden hepsini çözmek zorunda kalırsın. Bu disiplin uzun vadede çok zaman kazandırır.


assert() ile Kontrol

assert() makrosu, bir koşulun doğru olmasını beklediğin yerlere kontrol noktaları koyar. Koşul yanlışsa program durur ve sana tam olarak nerede durduğunu söyler.

#include <iostream>
#include <cassert>

double karekokHesapla(double x) {
    assert(x >= 0.0 && "Negatif sayinin karekoku alinamaz!");

    double tahmin = x / 2.0;
    for (int i = 0; i < 100; i++) {
        tahmin = (tahmin + x / tahmin) / 2.0;
    }
    return tahmin;
}

int main() {
    std::cout << "sqrt(25) = " << karekokHesapla(25.0) << "\n";
    std::cout << "sqrt(-4) = " << karekokHesapla(-4.0) << "\n";  // ASSERT FAIL!
    return 0;
}

Çıktı:

sqrt(25) = 5
program: main.cpp:5: double karekokHesapla(double): Assertion `x >= 0.0 && "Negatif sayinin karekoku alinamaz!"' failed.
Aborted

Dosya adı, satır numarası ve koşul — hata bulmak için yeterli bilgi.

assert vs exception

Özellikassertexception
AmaçProgramcı hatası (bug) yakalamaÇalışma zamanı hatası yönetimi
Release'deNDEBUG ile devre dışıHer zaman aktif
TepkiProgram durur (abort)Yakalanabilir, kurtarılabilir
// assert — bu asla olmamali (programci hatasi)
assert(index < boyut && "Index sinir disi!");

// exception — bu olabilir (kullanici girdisi)
if (yas < 0) {
    throw std::invalid_argument("Yas negatif olamaz");
}

⚠️ assert içinde yan etkili ifadeler yazma! assert(dosya.open("test.txt")) gibi bir ifade Release modda devre dışı kalır ve dosya hiç açılmaz. Yan etkili işlemleri assert dışında yap.


GDB — GNU Debugger Temelleri

GDB, C++ için standart komut satırı debugger'ıdır. Programı adım adım çalıştırmanı, değişkenleri incelemeni ve çökme noktasını bulmanı sağlar.

Hazırlık

Debug bilgisiyle derle:

g++ -g -O0 main.cpp -o program
# -g: debug bilgisi ekle
# -O0: optimizasyon kapali (degiskenler optimize edilmesin)

Temel GDB Komutları

gdb ./program          # GDB'yi baslat

GDB içinde:

KomutKısaltmaAçıklama
break mainb mainmain fonksiyonunda durma noktası koy
break main.cpp:15b main.cpp:1515. satırda durma noktası
runrProgramı başlat
nextnBir sonraki satıra geç (fonksiyona girme)
stepsBir sonraki satıra geç (fonksiyona gir)
continuecSonraki breakpoint'e kadar devam et
print xp xx değişkeninin değerini göster
backtracebtÇağrı yığınını (call stack) göster
info localsYerel değişkenleri listele
quitqGDB'den çık

Pratik Örnek

Diyelim ki şu program çöküyor:

#include <iostream>
#include <vector>

int elemanBul(const std::vector<int>& v, int hedef) {
    for (size_t i = 0; i <= v.size(); i++) {  // BUG: <= yerine < olmali
        if (v[i] == hedef) {
            return i;
        }
    }
    return -1;
}

int main() {
    std::vector<int> sayilar = {10, 20, 30, 40, 50};
    int sonuc = elemanBul(sayilar, 99);
    std::cout << "Sonuc: " << sonuc << "\n";
    return 0;
}

GDB oturumu:

$ gdb ./program
(gdb) break elemanBul
(gdb) run
Breakpoint 1, elemanBul at main.cpp:4
(gdb) print v.size()
$1 = 5
(gdb) next
(gdb) next
(gdb) print i
$2 = 0
(gdb) continue
... (program coker)
(gdb) backtrace
#0 elemanBul at main.cpp:5
#1 main at main.cpp:13
(gdb) print i
$3 = 5     <-- i == 5, ama v.size() == 5, yani v[5] sinir disi!

backtrace komutu çökmenin nerede olduğunu gösterir. i = 5 iken v[5]'e erişim — sınır dışı.

💡 IDE'lerin debugger'ları GDB'yi grafiksel arayüzle sarar. VS Code, CLion, Qt Creator — hepsi arka planda GDB veya LLDB kullanır. Ama terminal GDB bilmek sorun çözme gücünü artırır.


AddressSanitizer (ASan)

AddressSanitizer, bellek hatalarını çalışma zamanında tespit eden bir derleyici aracıdır. Programın 2x kadar yavaşlar ama bellek hatalarını kesin olarak bulur.

Etkinleştirme

g++ -g -fsanitize=address -fno-omit-frame-pointer main.cpp -o program
./program

ASan'ın Yakaladığı Hatalar

1. Buffer Overflow:

int main() {
    int dizi[5] = {1, 2, 3, 4, 5};
    dizi[5] = 6;  // STACK BUFFER OVERFLOW — ASan yakalar
    return 0;
}

ASan çıktısı:

ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7fff...
WRITE of size 4 at 0x7fff... thread T0
    #0 main main.cpp:3

2. Use After Free:

int main() {
    int* p = new int(42);
    delete p;
    *p = 10;  // USE AFTER FREE — ASan yakalar
    return 0;
}

3. Memory Leak:

int main() {
    int* p = new int(42);
    // delete p; — unutuldu
    return 0;
}
// ASan: detected memory leaks

ASan hatanın tam yerini (dosya, satır), erişim türünü (read/write) ve bellek durumunu gösterir.


UndefinedBehaviorSanitizer (UBSan)

UBSan, tanımsız davranış (undefined behavior) olan kodları tespit eder. ASan ile birlikte kullanılabilir.

g++ -g -fsanitize=undefined main.cpp -o program
./program

# ASan + UBSan birlikte
g++ -g -fsanitize=address,undefined main.cpp -o program

UBSan'ın Yakaladığı Hatalar

#include <iostream>
#include <climits>

int main() {
    // Signed integer overflow — tanimsiz davranis!
    int x = INT_MAX;
    x = x + 1;  // UBSan yakalar
    std::cout << x << "\n";

    // Sifira bolme
    int a = 10, b = 0;
    int c = a / b;  // UBSan yakalar

    // Null pointer dereference
    int* p = nullptr;
    *p = 5;  // UBSan yakalar

    return 0;
}

UBSan çıktısı:

main.cpp:7:9: runtime error: signed integer overflow: 2147483647 + 1
cannot be represented in type 'int'

⚠️ CI/CD pipeline'ında her iki sanitizer'ı da çalıştır. Debug build'lerde ASan + UBSan aktif olsun. Release build'lerde kapalı olsun (performans maliyeti var).


Logging Stratejileri

Debugger her zaman kullanılabilir olmayabilir (üretim ortamı, uzak sunucu, aralıklı hata). Logging bu durumlar için vazgeçilmezdir.

Basit Log Sistemi

#include <iostream>
#include <fstream>
#include <string>
#include <chrono>
#include <ctime>

enum class LogSeviye {
    DEBUG, INFO, UYARI, HATA
};

class Logger {
public:
    Logger(const std::string& dosyaAdi)
        : dosya_(dosyaAdi, std::ios::app) {}

    void log(LogSeviye seviye, const std::string& mesaj) {
        auto simdi = std::chrono::system_clock::now();
        auto zaman = std::chrono::system_clock::to_time_t(simdi);

        dosya_ << zamanDamgasi(zaman) << " ["
               << seviyeStr(seviye) << "] "
               << mesaj << "\n";
        dosya_.flush();
    }

private:
    std::ofstream dosya_;

    std::string seviyeStr(LogSeviye s) {
        switch (s) {
            case LogSeviye::DEBUG: return "DEBUG";
            case LogSeviye::INFO:  return "INFO ";
            case LogSeviye::UYARI: return "UYARI";
            case LogSeviye::HATA:  return "HATA ";
        }
        return "?????";
    }

    std::string zamanDamgasi(std::time_t t) {
        char buf[20];
        std::strftime(buf, sizeof(buf), "%Y-%m-%d %H:%M:%S", std::localtime(&t));
        return buf;
    }
};

int main() {
    Logger logger("uygulama.log");

    logger.log(LogSeviye::INFO, "Uygulama basladi");
    logger.log(LogSeviye::DEBUG, "Veritabanina baglaniliyor...");
    logger.log(LogSeviye::UYARI, "Baglanti yavas");
    logger.log(LogSeviye::HATA, "Sorgu basarisiz: timeout");
    logger.log(LogSeviye::INFO, "Uygulama kapandi");

    return 0;
}

Log dosyası çıktısı:

2026-02-19 23:50:00 [INFO ] Uygulama basladi
2026-02-19 23:50:00 [DEBUG] Veritabanina baglaniliyor...
2026-02-19 23:50:01 [UYARI] Baglanti yavas
2026-02-19 23:50:02 [HATA ] Sorgu basarisiz: timeout
2026-02-19 23:50:02 [INFO ] Uygulama kapandi

Log Seviyeleri Ne Zaman Kullanılır?

SeviyeKullanım
DEBUGGeliştirme sırasında detaylı bilgi
INFONormal çalışma bilgisi
UYARIPotansiyel sorun ama program çalışmaya devam ediyor
HATABir işlem başarısız oldu

💡 Üretim ortamında log seviyesini filtrele. DEBUG log'ları geliştirme ortamında açık, üretimde kapalı olsun. Çok fazla log da performans ve disk sorunlarına yol açar.


Common Bug Pattern'lar ve Çözümleri

1. Off-by-One Hatası

Döngü sınırlarında bir fazla veya bir eksik iterasyon.

// HATALI
for (int i = 0; i <= n; i++) { ... }  // n+1 iterasyon

// DOGRU
for (int i = 0; i < n; i++) { ... }   // n iterasyon

Çözüm: Range-based for loop kullan, mümkünse indeks kullanma.

2. Başlatılmamış Değişken

// HATALI
int toplam;
for (int i = 0; i < 10; i++) {
    toplam += i;  // toplam baslangicta ne?
}

// DOGRU
int toplam = 0;

Çözüm: Değişkenleri tanımlandığı yerde başlat. {} kullan.

3. Iterator Invalidation

Container'ı değiştirirken iterator kullanmak.

// HATALI
std::vector<int> v = {1, 2, 3, 4, 5};
for (auto it = v.begin(); it != v.end(); ++it) {
    if (*it % 2 == 0) {
        v.erase(it);  // iterator gecersiz oldu!
    }
}

// DOGRU — erase-remove idiom
v.erase(std::remove_if(v.begin(), v.end(),
    [](int x) { return x % 2 == 0; }), v.end());

4. Dangling Reference

Ömrü biten nesneye referans tutmak.

// HATALI
const std::string& tehlike() {
    std::string gecici = "merhaba";
    return gecici;  // gecici yok olacak!
}

// DOGRU — deger olarak don
std::string guvenli() {
    std::string gecici = "merhaba";
    return gecici;  // move semantics ile verimli
}

5. Integer Overflow

// HATALI
int faktoriyel(int n) {
    int sonuc = 1;
    for (int i = 2; i <= n; i++) {
        sonuc *= i;  // 13! icin int tasar!
    }
    return sonuc;
}

// DOGRU
long long faktoriyel(int n) {
    long long sonuc = 1;
    for (int i = 2; i <= n; i++) {
        sonuc *= i;
    }
    return sonuc;
}

Çözüm: Büyük sayılarla çalışırken long long kullan. UBSan ile signed overflow'u tespit et.


Debugging Kontrol Listesi

Bir hata bulduğunda sırayla dene:

  1. Derleyici uyarılarını aç (-Wall -Wextra) — çoğu hata burada yakalanır

  2. Sanitizer'larla çalıştır (-fsanitize=address,undefined) — bellek ve UB hataları

  3. Hatayı tekrarla — tutarlı tekrarlama yoksa debug edemezsin

  4. Minimal örnek oluştur — hatayı en az kodla tekrarla

  5. GDB ile adım adım ilerle — değişken değerlerini kontrol et

  6. Log ekle — aralıklı hatalar için log'lar hayat kurtarır

  7. Rubber duck debugging — sorunu birine (veya bir ördek figürüne) anlatırken çoğu zaman cevabı kendin bulursun


Özet

  • Derleyici uyarıları (-Wall -Wextra -Werror) en ucuz ve en etkili debugging aracıdır. Her zaman etkinleştir.

  • assert() ile programcı hatalarını (bug) yakala. Koşul yanlışsa program durur ve hatanın yerini söyler. Release'de devre dışı kalır.

  • GDB ile programı adım adım çalıştır: break, run, next, print, backtrace temel komutlardır. Çökme noktasını ve değişken değerlerini inceleme imkanı verir.

  • AddressSanitizer bellek hatalarını (buffer overflow, use-after-free, leak), UndefinedBehaviorSanitizer tanımsız davranışları (integer overflow, null deref) çalışma zamanında tespit eder.

  • Logging ile uzak sunucularda ve aralıklı hatalarda bilgi topla. Seviye bazlı (DEBUG/INFO/UYARI/HATA) log sistemi kur.

  • Yaygın bug pattern'lar: off-by-one, başlatılmamış değişken, iterator invalidation, dangling reference, integer overflow. Hepsinin çözümü modern C++ araçlarını (range-for, RAII, smart pointer) kullanmaktır.