Mart 12, 2023
Merhaba, bugün C++ dilinde "new" kullanımının arkasında neler olduğuna dair bir bakış atacağız. C++ ve C donanım tabanlı birçok amaç için kullanılabilen dillerdir ve bellek yönetimi konusunda biraz daha açık yazım yapısına sahiptirler. (pointer vb.) Bu nedenle, geçmişten gelen düşük donanımları yönetmek için performansa dayalı bir teknoloji diline ihtiyaç duyulduğunda C++ ve C tercih edilir. Bu diller donanımın gücünü verimli bir şekilde kullanabilen kod parçacıkları oluşturma hedefi taşırlar. Bu nedenle, savunma sanayisi, oyun endüstrisi, otonom araç teknolojileri, işletim sistemleri, arama motorları, haberleşme gibi kilit alanlarda yaygın olarak kullanılmaktadırlar. Örnek vermek gerekirse aktif olarak C, C++ teknolojisi kullanan yazılımlar için Linux(Core), Windows, Firefox, Adobe gibi örnekler verilebilir.
Ancak bu özellikleri nedeniyle C++ diğer dillere göre daha karmaşık bir syntax yapısına sahip ve daha fazla düşük seviyeli donanım bilgisi gerektiren bir dildir. Performans ve verimlilik söz konusu olduğunda, doğru şekilde yazılmış ve mimari olarak implemente edilmiş bir C, C++ kodu oldukça yararlı olabilir. Ancak bu konuda birçok noktaya dikkat edilmesi gerektiği için hataya daha fazla açık bir dil haline gelebilir. Şimdi genel bir bakış açısı sunduktan sonra konuyu daha da özelleştirebiliriz.
"Neden daha az kullanmalıyız?" sorusuna doğru bir cevap verebilmek için C++ dilinin genel yapısı hakkında bilgi sahibi olmak ve "new" kullanımı sonrası bellek olaylarının sonuçlarını bilmek önemlidir.
Üç tip bellek yönetim tipi vardır:
- Static Memory Allocation | Statik Bellek Ayırma
Programın başlatılmasından önce "static" olarak tanımlanmış değerler için bellek alanından yer tahsisi yapılır. Bu tahsis işlemi sadece bir kez gerçekleştirilir ve değerler sabit boyutludur. Program sonlanana kadar bellek içerisinde yer alırlar, programın sonlanmasıyla birlikte beraber belleğe iade edilirler.
- Automatic Memory Allocation | Otomatik Bellek Ayırma
Lokal değişkenler ve fonksiyon/method tanımları genellikle bir alanda başlatılır ve aynı alandan çıktıktan sonra bellekte iade edilir. Bu veriler "Stack | Yığın" olarak adlandırılan bir yapıda saklanır ve iadeleri de bu şekilde gerçekleştirilir. Bu bellek yönetim biçimi için daha güvenli olduğu düşüncesi hakimdir.
- Dynamic Memory Allocation | Dinamik Bellek Ayırma
"new, malloc, calloc" gibi çağrılar ile bellek üzerinden anlık olarak alan tahsisi yapılması söz konusudur. Belleğin tahsisi ve iadesi programcıya bağlıdır. Bu sebeple hataya açık ve dikkatli kullanılması gerekir. Tanımlanan değişkenler ve objeler bellek üzerinde "heap" adını verdiğimiz alanda tutulur. Bu kısım program çalıştığı süre zarfında bellek alanından tahsis edilir ve kullanılır. Bu sebeple işletim adresi ve uzunluğu sabit değildir. Program işleyişinde istenen yer ve oluşan verinin büyüklüğü ile değişicidir.
Bu yazının temel konusu ise tanımlama ve kullanım biçimi doğrultusunda gerçekleşen "Automatic Memory Allocation | Otomatik Bellek Ayırma" ve "Dynamic Memory Allocation | Dinamik Bellek Ayırma" biçimlerinin farklılıklarından oluşmaktadır.
Kaynak: digiKey
Şimdi bellek alanlarının tahsisinde gerçekleşen farklılıklarla birlikte performans ve güvenilirlik için oluşan durumlara göz atalım. Herhangi bir kod parçacağınız içerisinde yalnızca "new" deyimiyle çağrı yaptığınızda bellek üzerinden bir alan tahsisi gerçekleştirirsiniz, ayrıca bu alanın yönetimi ve idaresi (C, C++ gibi programlama dilleri için) sizlerin yönetimindedir. Belleğe iadesi de öyle! Asla unutmamak gerekir. C ve C++ kaynak yönetimi için bir Garbage Collector'a sahip değildir. Bu sebeple kullanılmayan değişken ve değerleri otomatik olarak iade işlemi gerçekleştirmez, süreci yönetmez. Bunun sebebi ise yukarıda bahsetmiş olduğum eskiye dönük mimarisi, performans isteği gibi çeşitli sebeplerden kaynaklanmaktadır. Hatalı kullanım ile birlikte bellek üzerinden tahsis ettiğiniz alanı iade etmememiz sonucunda "Memory Leak" adını verdiğimiz durum ile karşılaşabilir veya programınızı kullanılamayan bir hale getirebilirsiniz.
Şimdi bu durum için basit bir örnek üzerinden inceleyelim.
Örnek üzerinden gidecek olursak yukarıdaki kod parçacağında görüldüğü üzere bir "MyClass" adıyla bir sınıf tanımlı, bu sınıftan bir nesne oluşturuyoruz ve "delete" işlemi gerçekleştirilmeden if şartı içerisinde yer alan return ile programı sonlandırıyoruz. Görüldüğü üzere "MyClass" sınıfına ait "Destructor | Yıkıcı" herhangi bir şekilde çağrılmıyor. Son durumda tanımlamış olduğumuz class nesnesi halen bellekte yer almaya devam ediyor. Bu durum şu an için korkutucu gözükmeyebilir. Fakat eğer bu parçacığı çok fazla çalıştırırsam? Bir süre zarfı sonrasında belleğimde alan kalmayacak ve işletim sistemi üzerinden diğer programlar, süreçler hatalı davranmaya, belki de sonlanmaya başlayacaktır. Ayrıca kritik bir noktada bırakılan bu tip bir veri güvenlik ihlali de oluşturabilir. Örneğin bir kullanıcıya ait tanımlı bir veriyi bir class yapısından oluşturduğunuz obje yardımıyla tutuyorsunuz. Bu objeyi eğer doğru biçimde belleğe iade etmez veya içeriğini karartmazsanız bellek üzerinde okunabilir olarak kalacaktır.
! C ve C++ bellek yönetimi için sizlere farklı yollar sunuyor fakat "Garbage Collector" bulunmayışıyla birlikte yönetimin tüm sorumluluğunu da size devrediyor.
C++ bizlere bu durum için "Smart Pointers | Akıllı İşaretçiler" gibi bir yol sunuyor. Yaptığımız "new" tanımlarının "Dynamic Memory Allocation | Dinamik Bellek Ayırma" yerine "Automatic Memory Allocation | Otomatik Bellek Ayırma" ile gerçekleşmesini sağlayabilirsiniz. Programınızın performansı ve kaynak yönetimi için bu yol sizlere daha iyi sonuçlar verecektir. Detaylandırmadan önce aynı örneği bir smart pointer yardımıyla gerçekleştirelim;
Bu örnek üzerinden baktığımızda ise if şartı içerisinde program return ettiğinde var olan değişken değeri için bellek alanı iade ediliyor. Çünkü "Destructor | Yıkıcı" çağrısının gerçekleştiğini görüyoruz. Ayrıca nesnenin bellek adresi boşaltılarak yeniden kullanılmak üzere tahsis ediliyor.
Dynamic Memory Allocation | Dinamik Bellek Ayırma VS Automatic Memory Allocation | Otomatik Bellek Ayırma
Otomatik Bellek Ayırma işlemi, Dinamik Bellek Ayırma işlemine kıyasla daha hızlı ve güvenlidir. "heap" yapısı için adresler değişicidir ve bellek alanında sıralı değildirler. Adresci kayıtlarının tutulması ve değerlere erişim maliyetlidir. Bu maliyetler beraberinde performans kaybına yol açar. Otomatik Bellek Ayırma işleminde yer alan "stack" yapısı için ise değerler "FILO (First In Last Out)" olarak tutulduğundan erişim hızlıdır ve kayıtlı adresci defteri boyutu azdır. Bellek üzerinden veriye erişme maliyeti düşüktür ve değişkenlerin adres tanımları bir yığın halinde tutulduğundan değişkenlerin, verilerin iadesinde "Memory Leak" hatalarının önüne geçer.
"New" deyimini kullanmaktan her zaman kaçınmak zorunda değilsiniz. Özellikle yığın boyutuna oranla daha fazla veri almanız gerektiğinde veya new ifadesinin iadesini iyi yönetebileceğiniz durumlarda "new" kullanabilirsiniz. Ancak mümkün olduğunca "Smart Pointers | Akıllı İşaretçileri" kullanmanız bellek kaynaklarını daha iyi yönetmenize yardımcı olarak programınızın erken "return" etmesi veya bir exception durumu oluşması durumunda size yardımcı olacaktır. Sınıf yapıları oluştururken akıllı işaretçiler ayrıca çok kullanışlı olabilirler. Çünkü birden fazla akıllı işaretçi türü vardır. Bu işaretçileri işe ve duruma özgü kullanabilirsiniz. TheCherno kanalına ait "SMART POINTERS in C++ (std::unique_ptr, std::shared_ptr, std::weak_ptr)" ve CoffeeBeforeArch kanalına ait C++ Best Practices: RAII içeriklerini izleyerek daha fazla bilgi edinebilirsiniz.