Bu yazıda C++’da önemli bir yere sahip olan bellek yönetimi konuşundan bahsedeceğim. Yazı içerisinde stack hafız ve heap hafıza hakkında bilgiler verilmiş olup, heap hafızada bellek tahsisi yapılısı ve ilgili belleğin serbest bırakılışı anlatılmıştır. Yazının devamında bellek yönetimi sırasında karşılaşabileceğiniz bazı hataların çözümleri anlatılmış olup, shared_ptr konusundan bahsedilmiştir. Ayrıca konunun anlatımını kuvvetlendirmek için pek çok kod örneğine de yazı içerisinde yer verilmiştir.
Bellek Yönetimi
Stack Hafıza ve Heap Hafıza
Bu yazı serisi içerisinde şimdiye kadar ele almış olduğumuz lokal değişkenlerimizin vs. çoğunu stack hafızada oluşturduk. Stack kendi bellek yönetimini kendisi yapabilmektedir. Bu sayede stack hafızada oluşturulan değişkenler işlem bittiğinde stack’den kaldırılır. stack’de değerler ilk giren son çıkar yada son giren ilk çıkar mantığına göre tutulmaktadır. stack derleyici tarafından yönetilen bir bellek alanıdır.
heap hafızada ise durum farklıdır. heap hafızada veriler hafızada rastgele tutulmaktadır. Heap hafıza üzerinde bir bellek alanı kullanmak istiyorsak new anahtar kelimesinden faydalanabiliriz. Bu yöntemle tayin ettiğimiz bir bellek alanı artık derleyici tarafından değil, doğrudan programcı tarafından yönetilmeye başlar, bu sebepten dolayı da oldukça dikkatli olunması gerekir. Heap’de kullanılacak bir bellek alanı oluşturma işlemi, dinamik bellek tahsisi olarak adlandırılır. C++’da çöp toplayıcı olmadığı için ilgili veriyi kaldırırken delete komutunu kullanabilirsiniz, bu sayede ilgili bellek alanını serbest bırakmış olursunuz.
stack hafıza heap hafızaya göre daha hızlıdır. İşlemcilerin register bilgileri stack’de tutulmaktadır. Stack hafıza genelde bir kaç megabyte büyüklüğünde bir depolama alanıdır. Stack hafıza sınırlı bir alanı temsil etmekte olduğu için heap hafızayı kullanmanız gerekebilir.
Heap hafız vasıtasıyla, önceden stack hafızada gerçekleştiremediğimiz, dizi boyutunu bir değişken yardımıyla temsil etme işlemini gerçekleştirebiliriz. Bu işlemi gerçekleştirmek için veri_tipi* dizi_ismi = new veri_tipi [ değişken_adı ]; şeklinde bir ifade yapısından faydalanabiliriz. Heap hafızada oluşturulacak bu diziyi delete[] dizi_ismi; şeklinde bir ifade vasıtası ile hafızadan kaldırabilirsiniz.
Heap’de bir bellek alanı kullanmak istiyorsak işaretçilerden ve new anahtar ifadesinden faydalanmamız gerekir. Aşağıdaki örnekte heap hafızada int, float ve char veri boyutunda yer kaplayacak olan x ,y ve z değişkenlerini ve araç nesnesi boyutunda yer kaplayacak olan araç1, araç2 ve araç3 nesnelerini oluşturmuş olduk. x değişkenine 18 değerini, y değişkenine 10 değerini ve z değişkenine işe u harfini atadık. İlgili depolama alanlarıyla işimiz bittikten sonra, ilgili depolama alanlarını delete ifadesi vasıtasıyla serbest bıraktık. Eğer bu kaldırma işlemini ilgili depolama alanlarıyla ilgili verilerin tanımlı olduğu süslü parantezler içerisinde gerçekleştirmeseydik, ilgili depolama alanlarını serbest bırakamazdık. Ayrıca örnek içerisinde, veriler isminde a değişkeni vasıtası ile sonradan tekrar boyutlandırabileceğimiz, bir adet de dizi tanımladık.
#include<iostream>
class araç {
public:
int hız;
};
void main() {
int* x = new int;
*x = 18;
delete x;
// nullptr ile işaretçinin rastgele bir bellek
// alanını işaret etmesi engellendi
float* y = nullptr;
{
y = new float;
*y = 10;
}
delete y;
{
char* z = new char;
// 117 u harfinin ASCII kodudur.
*z = 117;
delete z;
}
araç* araç1 = new araç;
delete araç1;
{
araç* araç2 = new araç;
delete araç2;
}
araç* araç3;
{
araç3 = new araç;
}
delete araç3;
int a;
a = 14;
int* veriler = new int[a];
delete[] veriler;
}
Hafıza Kullanımıyla Alakalı Hatalar
Birkaç megabyte boyutundaki stack hafızanın dolmasına sebebiyet verecek işlemler gerçekleştirirseniz, yığın taşması hatası alırsınız. heap hafızada oluşturmuş olduğunuz bir değişkeni, işiniz bittikten sonra kaldırılmazsanız, bu durum bellek sızıntısı olarak adlandırılır.
Aynı bellek adresini gösteren farklı işaretçiler oluşturmuşsanız, ilgili bellek alanını serbest bırakmanıza rağmen, oluşturmuş olduğunuz işaretçi vasıtasıyla ilgili bellek alanına hala müdahale ediyor olabilirsiniz. Bu durum programın işleyişi sırasında size hata olarak dönmese de, ileride size ait olmayan bir bellek alanını kullandığınız için hataya sebebiyet verebilir. Bu durum sarkan işaretçi olarak adlandırılır. Aşağıdaki örnekte oluşturulan x değişkeni kaldırılmış olsa da, aynı bellek alanını gösteren y işaretçisi hala kullanılabilir durumdadır. İlgili y işaretçisinin gösterdiği hafıza alanı, programımız için ayrılan bir bellek alanı olmaktan çıktığı için hataya sebebiyet verebilir.
#include<iostream>
void main() {
int* x = new int;
int* y = x;
delete x;
*y = 5;
std::cout << *y;
}
C dilindeki malloc ve free komutları, new ve delete komutlarıyla benzer işlevdedir. Ancak C++’da malloc ve free komutlarını sınıflarla beraber kullanmanız, C dilinde sınıf yapısı olmadığı için hataya sebebiyet verebilir. Aşağıda malloc ve free komutlarının kullanımına bir örnek verilmiştir. Örnekteki sizeof(…) ifadesi bize içindeki yapının hafıza boyutunu döndürmektedir. Üzerinde çalıştığınız sisteme göre, sizeof (int) ifadesi yerine, int veri tipi 4 byte bir bellek alanı kapladığı için dört de yazabilirsiniz.
#include<iostream>
void main() {
int* x = (int*) malloc (sizeof (int));
*x = 10;
std::cout << *x;
free(x);
int* y = (int*) malloc(4);
*y = 18;
std::cout << *y;
free(y);
}
Bir tür bellek sızıntısı durumu işe bir sınıf içerisinde heap hafızada tutulan değişkenlerde bulunmaktaysa oluşabilmektedir. Çünkü bir nesneyi kaldırmamız demek, her zaman için içerisinde tanımlı olan değişkenleri de kaldırdığımız anlamına gelmemektedir. Heap hafıza da tanımlamış olduğumuz değişkenler, bizim yönetimimizde olduğu için nesne kaldırılırken ilgili değişkenlerin kaldırılmasını da sağlamamız gerekmektedir. Bu hatayı yıkıcı metot içerisinde ilgili değişkenleri kaldırma yöntemi ile engelleyebiliriz. Aşağıdaki örnekte bellek sızıntısının oluşması engellenmiştir.
#include<iostream>
class arac {
public:
int* speed = new int;
double* lenght = new double;
~arac() {
delete speed;
delete lenght;
}
};
void main() {
arac arac1;
}
shared_ptr
shared_ptr kullandığımızda bir heap bellek alanındaki verileri ve ilgili verilerin bellek adresini göstermekte olan işaretçileri kaldırma yükünden kurtuluruz. Bu sayede hem heap hafızada kullanmış olduğumuz alanları işimiz bittikten sonra serbest bırakıp bırakmadığımızı kontrol etme yükünden, hem de sarkan işaretçi durumuyla karşılaşmaktan kurtuluruz.
Aşağıdaki örnekte ptrarac1 ve ptrx1 isimlerinde iki shared_ptr ve ilgili işaretçileri kullanarak aynı bellek adreslerini işaret edecek ptrarac2 ve ptrx2 isimlerinde iki shared_ptr daha oluşturmuş olduk.
#include<iostream>
class arac {
public:
int hız;
};
void main(){
std::shared_ptr <arac> ptrarac1 = std::make_shared <arac>();
std::shared_ptr <arac> ptrarac2 = ptrarac1;
std::shared_ptr <int> ptrx1 = std::make_shared <int>();
std::shared_ptr <int> ptrx2 = ptrx1;
}