Programlamada gerçek dünya nesneleri nadiren tek bir değerden oluşur. Bir öğrencinin adı, numarası ve notu; bir oyun karakterinin koordinatları, canı ve seviyesi vardır. Bu bilgileri birbirinden kopuk değişkenlerde tutmak kısa örneklerde işe yarasa da uygulama büyüdükçe yönetilemez hale gelir. Yapılar (structs), farklı türlerdeki verileri mantıksal olarak tek bir varlık altında toplayan temel veri iskeletleridir.
Devamı...
Sistem programlama, bilgisayarın yalnızca ne yaptığını değil, bunu hangi maliyetle yaptığını da önemser. Bellek adresleri, işlemci önbelleği, kesmeler, dosya tanıtıcıları ve eşzamanlılık burada günlük araçlardır. C ile C++ arasındaki seçim de basitçe “eski mi yeni mi?” sorusu değildir: C doğrudanlık ve evrensel uyumluluk sunarken, C++ aynı donanım düzeyinde daha sağlam soyutlamalar kurmayı hedefler.
Devamı...
Bir değişkeni düşünün: çoğu zaman onun değerini kullanırız, fakat bilgisayar açısından asıl önemli soru şudur: Bu değer RAM’in neresinde duruyor? İşaretçiler (pointer), verinin kendisini taşımak yerine o verinin bellekteki adresini saklayan değişkenlerdir. Böylece büyük bir diziyi, karmaşık bir nesneyi veya bir yapıyı kopyalamak yerine ona doğrudan erişebiliriz. Bu yaklaşım sistem programlama, oyun motorları, gömülü yazılımlar ve yüksek performanslı uygulamaların görünmez turbo düğmesidir.
Devamı...
Yüksek seviyeli bir dilde yazılan if, switch veya for satırları zararsız görünür; ancak işlemci bunları doğrudan “karar” olarak algılamaz. Arka planda karşılaştırmalar, koşullu sıçramalar (branch), bellek erişimleri ve bazen tablo üzerinden dolaylı atlamalar çalışır. Bu nedenle kontrol yapısının maliyeti yalnızca kaynak koddaki satır sayısıyla değil, veri dağılımı, derleyici optimizasyonu ve CPU’nun dal tahmin başarısıyla belirlenir.
Devamı...
Programlar çalışırken tüm veri boyutları baştan bilinmez: Kullanıcının gireceği metnin uzunluğu, dosyadaki kayıt sayısı veya oluşturulacak nesne miktarı değişkendir. İşte bu noktada heap (öbek) alanı devreye girer. Heap, çalışma zamanında bellek talep etmemizi sağlayan bölgedir. Bu bellek çağıran fonksiyon bittiğinde kendiliğinden kaybolmaz; programcı onu bilinçli biçimde iade etmelidir. Güç büyük, sorumluluk daha da büyüktür.
Devamı...
Fonksiyonlar, büyük bir programı yönetilebilir görevlere ayırır; yığın (stack) ise bu görevlerin kim tarafından, hangi parametrelerle ve nereye dönmek üzere çağrıldığını düzenler. Bir fonksiyon çağrısı yalnızca kodun başka bir satıra atlaması değildir: İşletim sistemi, derleyici ve işlemci birlikte çalışarak geçici veriler için düzenli bir bellek kaydı oluşturur. Bu mekanizma sayesinde iç içe çağrılar, özyineleme ve yerel değişkenler güvenle yönetilir.
Yığın neden gereklidir?
Programın belleği kavramsal olarak kod, global veri, heap ve stack gibi bölümlere ayrılır. Heap dinamik olarak oluşturulan ve ömrü programcının ya da çöp toplayıcının kontrolündeki nesneler içindir. Stack ise fonksiyon çağrılarının doğal LIFO (Last In, First Out — son giren ilk çıkar) düzenini izler. main() içinden A(), onun içinden de B() çağrılırsa, önce B() bitmek zorundadır.
Her çağrıda stack üzerinde bir stack frame (çağrı çerçevesi) oluşturulur. Bu çerçeve tipik olarak dönüş adresini, parametreleri, yerel değişkenleri, kaydedilmiş kayıtçıları ve hizalama için ayrılan alanı içerir. Stack işaretçisi $SP, yığının güncel tepesini; frame pointer ise çoğu mimaride mevcut çerçevenin sabit bir referans noktasını gösterir.
| Bellek bölgesi |
Temel kullanım |
Ömür |
Yönetim |
| Kod bölgesi |
Derlenmiş komutlar |
Program boyunca |
İşletim sistemi / yükleyici |
| Global veri |
Global ve statik değişkenler |
Program boyunca |
Çalışma zamanı |
| Heap |
Dinamik nesneler |
İhtiyaca bağlı |
malloc/free veya GC |
| Stack |
Çağrılar ve yerel veriler |
Fonksiyon bitene kadar |
Otomatik |
Bir çağrının anatomisi
Örneğin aşağıdaki C kodunda topla, iki parametre alır ve sonucu çağırana döndürür:
Devamı...
Bir düşmanın oyuncuyu görünce kovalamaya başlaması, canı azalınca kaçması ve hedefini kaybedince devriye rotasına dönmesi basit görünür. Ancak bu kararlar doğrudan kodun içine serpiştirildiğinde kısa sürede yönetilemeyen bir koşul çorbası oluşur. Sonlu Durum Makinesi ve Davranış Ağacı, aynı düşman davranışını daha düzenli, test edilebilir ve genişletilebilir biçimde modelleyen iki klasik oyun yapay zekâsı tekniğidir.
Devamı...
Dosyalar, programların dış dünyayla kurduğu en kalıcı iletişim kanallarındandır. Bir günlük kaydı, CSV raporu, görsel ya da video; belleğe bir anda sığabilecek küçük bir metin de olabilir, gigabaytlarca süren ikili veri de. İşte stream (akış) yaklaşımı, verinin tamamını RAM’e yüklemek yerine onu küçük parçalar hâlinde sırayla işleyerek hem daha ölçeklenebilir hem de daha güvenli uygulamalar geliştirmeyi sağlar.
Devamı...
Bir C dizisi, sadece aynı türden değerlerin listesi değildir; bellekte art arda yerleştirilmiş sabit boyutlu fiziksel blokların ta kendisidir. İşaretçi aritmetiği ise bu blokların adresleri üzerinde güvenli ve anlamlı biçimde gezinmenin yoludur. İlk bakışta p + 1 ifadesi bir adresi yalnızca bir artırıyormuş gibi görünür. Oysa C burada tür bilgisini kullanır ve bir sonraki elemana sıçrar. Bu küçük ayrıntı, dizilerin performanslı çalışmasının temelidir.
Devamı...
Bir değişkeni yalnızca x = 42 diye düşünmek, buzdağının görünen kısmına bakmaktır. Statik tipli dillerde değişken; adı, tipi, yaşam süresi ve bellekte temsil edilme biçimi olan bir sözleşmedir. Derleyici bu sözleşmeyi program çalışmadan önce denetler: int bekleyen yere metin, belirli aralıkta sayı bekleyen yere taşacak bir değer koymaya çalışırsanız alarm çalar. Bu katılık ilk anda sınırlayıcı görünse de bellek düzenini öngörülebilir, hataları erken yakalanabilir ve performansı daha hesaplanabilir yapar.
Devamı...
C++ nesne yönelimli programlamanın en etkileyici fikirlerinden biri, aynı arayüzün farklı nesnelerde farklı sonuçlar üretmesidir. Buna çok biçimlilik (polymorphism) denir. Bir Animal* işaretçisiyle hem kediye hem köpeğe ses çıkarttırabilmek, kodun her somut sınıfı tek tek tanımasına gerek kalmadan genişlemesini sağlar. Bu esneklik, özellikle oyun motorları, ödeme sistemleri ve eklenti mimarilerinde hayat kurtarır.
Devamı...
C ve C++ kodu, derleyicinin eline doğrudan yazdığımız hâliyle ulaşmaz. Arada metin tabanlı ama etkisi büyük bir aşama vardır: ön işlemci (preprocessor). #include, #define ve #ifdef gibi satırlarla çalışan bu mekanizma, kaynak kodu derlenmeden hemen önce dönüştürür. Bunu bir tiyatro oyununun sahne arkası ekibi gibi düşünebilirsiniz: oyuncular (derleyici) sahneye çıkmadan dekorlar yerleşir, bazı replikler değiştirilir ve şartlara uymayan sahneler tamamen kaldırılır.
Devamı...
C ve C++, işlemciye ve belleğe yakın çalışabilmeleri sayesinde işletim sistemlerinden oyun motorlarına kadar pek çok alanda karşımıza çıkar. Ancak ilk programı yazmadan önce derleyici, hata ayıklayıcı ve editör üçlüsünü doğru biçimde hazırlamak gerekir. Bu rehberde GCC/Clang altyapısını kuracak, VS Code’u yapılandıracak ve kaynak kodun çalıştırılabilir programa nasıl dönüştüğünü öğreneceğiz.
Devamı...
C++ Standart Şablon Kütüphanesi, yani STL (Standard Template Library), tekerleği yeniden icat etmek yerine güvenilir parçalarla hızlı yazılım üretmenin yoludur. Dinamik diziler, bağlı listeler, yığınlar ve hazır algoritmalar sayesinde hem kod miktarı azalır hem de yıllardır test edilmiş uygulamalardan yararlanılır. Asıl güç, veri yapısını doğru probleme; algoritmayı da doğru veri düzenine eşleştirmekte yatar.
STL üç ana fikrin birleşimidir: container (veriyi tutar), iterator (veri üzerinde dolaşır) ve algorithm (veriye işlem uygular). Bu ayrım oldukça değerlidir: std::sort, hangi sınıftan geldiğini umursamadan rastgele erişim sunan bir aralık üzerinde çalışır. Böylece veri ile işlem birbirine sıkı sıkıya bağlanmaz.
En sık kullanılan container’lar aşağıdaki gibi karşılaştırılabilir:
| Yapı |
Güçlü yanı |
Zayıf yanı |
Tipik kullanım |
std::vector |
Sonda ekleme ve indeksle erişim çok hızlıdır |
Ortaya ekleme/kaldırma maliyetlidir |
Dinamik dizi, sonuç listesi |
std::list |
Bilinen konuma ekleme ve silme hızlıdır |
İndeksle erişim yoktur; önbellek dostu değildir |
Sık düğüm silinen zincirler |
std::deque |
Başta ve sonda ekleme hızlıdır |
Bellek düzeni vector kadar sade değildir |
Çift uçlu kuyruk |
std::stack |
LIFO işlemlerini sadeleştirir |
Sadece üst elemana erişilir |
Geri alma, parantez kontrolü |
Bir vector fiziksel olarak çoğunlukla bitişik bellekte yaşar. Bu nedenle v[i] erişimi $O(1)$ zamanlıdır ve işlemci önbelleğinden iyi yararlanır. Sona ekleme (push_back) normalde $O(1)$, kapasite dolduğunda ise yeni alan ayırma nedeniyle $O(n)$ maliyetli olabilir. Buna rağmen büyümenin seyrek gerçekleşmesi sebebiyle amortize maliyet şöyledir:
\[T_{amortize}(push\_back)=O(1)\]
Aşağıdaki örnek, sayıları sıralar, tekrarları temizler ve ikili arama yapar. Buradaki kritik nokta, binary_search fonksiyonunun doğru sonuç vermesi için aralığın önceden sıralı olmasıdır.
Devamı...
C dilinde bir program büyüdükçe, birbirleriyle ilişkili verileri ve bu veriler üzerinde çalışan fonksiyonları takip etmek zorlaşabilir. Örneğin bir banka hesabının bakiye, hesap numarası ve para yatırma fonksiyonu farklı yerlerde durabilir. C++ sınıfları ise bu dağınıklığı toplar: Veri ile o veriyi yöneten davranışlar aynı güvenli kutuda yaşar. Bu yaklaşım, nesne yönelimli programlamanın temel taşlarından olan kapsüllemedir.
Devamı...
C++’ta aynı algoritmayı int, double, std::string ya da kendi sınıflarımız için tekrar tekrar yazmak, kodun bakım maliyetini hızla artırır. Şablonlar (templates), algoritmayı türden ayırarak bu sorunu çözer: Bir kez kuralları tanımlar, tür seçimini derleyiciye bırakırsınız. Sonuç, çalışma anında tür kontrolü yapan yapılardan farklı olarak, derleme anında üretilen hızlı ve tür güvenli koddur.
Devamı...
C++’ta referanslar (references), bir değişkene ikinci bir ad vermenin güvenli ve okunaklı yoludur. C’den gelen işaretçiler güçlüdür; ancak *, &, -> ve nullptr kontrolleri özellikle fonksiyon imzalarını hızla zorlaştırabilir. Referans, adres fikrini tamamen gizlemez ama günlük kodda onu daha doğal bir sözdizimiyle sunar: Nesneyle uğraşıyor gibi yazarsınız, derleyici arka planda aynı belleğe erişimi sağlar.
Devamı...
Büyük bir C++ projesinde her nesneyi sıfırdan tanımlamak, kısa sürede kopyala-yapıştır bataklığına dönüşür. Bir aracın, çalışanın ya da oyun karakterinin ortak özellikleri varken bunları tekrar tekrar yazmak yerine kalıtım (inheritance) kullanırız. Kalıtım, bir temel sınıfın (base class) veri ve davranışlarını türetilmiş sınıflara (derived class) aktaran; böylece hem kod tekrarını azaltan hem de anlamlı nesne hiyerarşileri kuran nesne yönelimli programlama mekanizmasıdır.
Devamı...
Bellek yönetimi, programlamanın “ışıkları kapattım mı?” sorusudur: Bir nesneyi ayırdığımızı hatırlarız ama serbest bırakıp bırakmadığımızdan asla tam emin olamayız. Bu projede C ile küçük bir mark-and-sweep garbage collector geliştirerek manuel bellek yönetimini, nesne erişilebilirliğini ve sanal belleğin süreçteki rolünü birlikte inceleyeceğiz.
Devamı...
C++’ta bir nesne yalnızca değişken tanımlandığında ortaya çıkan basit bir veri paketi değildir; doğar, kullanılır ve zamanı gelince yok olur. Bu yaşam döngüsünün iki başrol oyuncusu yapıcı metotlar (constructor) ile yıkıcı metotlardır (destructor). Constructor, nesne bellekte oluşturulurken başlangıç durumunu güvenle kurar; destructor ise nesne kapsam dışına çıkarken sahne arkasında kalan kaynakları toplar. Özellikle dosya, ağ bağlantısı, dinamik bellek ve kilit gibi kaynaklarda bu otomasyon, C++’ın en güçlü fikirlerinden biri olan RAII yaklaşımının temelidir.
Devamı...