C ++ 'da "manuel bellek yönetimi yapmanız gerektiğini" yaygın bir yanılgı olduğu belirtilmelidir. Aslında, kodunuzda genellikle herhangi bir bellek yönetimi yapmazsınız.
Sabit boyutlu nesneler (kapsam ömrü boyunca)
Bir nesneye ihtiyaç duyduğunuz durumlarda çoğu durumda, nesne programınızda tanımlanmış bir ömre sahip olacak ve yığında yaratılacaktır. Bu, tüm yerleşik ilkel veri türleri için değil, aynı zamanda sınıf ve yapı örnekleri için de geçerlidir:
class MyObject {
public: int x;
};
int objTest()
{
MyObject obj;
obj.x = 5;
return obj.x;
}
İşlev sona erdiğinde yığın nesneleri otomatik olarak kaldırılır. Java'da nesneler her zaman yığınta oluşturulur ve bu nedenle çöp toplama gibi bazı mekanizmalar tarafından kaldırılması gerekir. Bu, yığın nesneleri için sorun değildir.
Dinamik verileri yöneten nesneler (kapsam ömrü dahilinde)
Yığın üzerinde boşluk kullanmak, sabit boyutlu nesneler için işe yarar. Dizi gibi değişken bir alana ihtiyaç duyduğunuzda, başka bir yaklaşım kullanılır: Liste, dinamik belleği sizin için yöneten sabit boyutlu bir nesneye yerleştirilir. Bu işe yarar, çünkü nesneler özel bir temizleme fonksiyonuna, yıkıcıya sahip olabilir. Nesnenin kapsam dışına çıkması ve yapıcının tam tersi olması durumunda çağrılması garanti edilir:
class MyList {
public:
// a fixed-size pointer to the actual memory.
int* listOfInts;
// constructor: get memory
MyList(size_t numElements) { listOfInts = new int[numElements]; }
// destructor: free memory
~MyList() { delete[] listOfInts; }
};
int listTest()
{
MyList list(1024);
list.listOfInts[200] = 5;
return list.listOfInts[200];
// When MyList goes off stack here, its destructor is called and frees the memory.
}
Hafızanın kullanıldığı kodda hiçbir hafıza yönetimi yoktur. Emin olmamız gereken tek şey, yazdığımız nesnenin uygun bir yıkıcıya sahip olduğudur. Kapsamı nasıl terk edersek gidelim listTest
, istisna yoluyla ya da basitçe geri dönerek yıkıcı ~MyList()
çağrılacak ve herhangi bir hafızayı yönetmemize gerek kalmayacak.
( İkili NOT işlecini, yıkıcıyı~
belirtmek için kullanmanın komik bir tasarım kararı olduğunu düşünüyorum . Sayılarda kullanıldığında bitleri tersine çevirir; analojide, burada yapıcının ne yaptığını gösterir.)
Temel olarak, dinamik belleğe ihtiyaç duyan tüm C ++ nesneleri bu kapsüllemeyi kullanır. Nesnelerin kendi içerikleriyle ilgilendikleri basit fikrini ifade etmenin oldukça tuhaf bir yolu olan RAII ("kaynak edinme başlangıçtır") olarak adlandırılmıştır; Elde ettikleri şey onların temizliği.
Polimorfik nesneler ve kapsam dışı ömür
Şimdi, her iki durumda da açıkça tanımlanmış bir ömrü olan hafıza içindi: Yaşam süresi kapsamı ile aynı. Kapsamdan çıktığımızda bir nesnenin süresinin dolmasını istemiyorsak, bizim için belleği yönetebilecek üçüncü bir mekanizma vardır: akıllı bir işaretçi. Akıllı işaretçiler, çalışma zamanında türünde değişiklik gösteren, ancak ortak bir arabirime veya temel sınıfa sahip olan nesnelerinizin örnekleri olduğunda da kullanılır:
class MyDerivedObject : public MyObject {
public: int y;
};
std::unique_ptr<MyObject> createObject()
{
// actually creates an object of a derived class,
// but the user doesn't need to know this.
return std::make_unique<MyDerivedObject>();
}
int dynamicObjTest()
{
std::unique_ptr<MyObject> obj = createObject();
obj->x = 5;
return obj->x;
// At scope end, the unique_ptr automatically removes the object it contains,
// calling its destructor if it has one.
}
std::shared_ptr
Nesneleri çeşitli istemciler arasında paylaşmak için başka bir akıllı işaretçi var . İçerdikleri nesneyi yalnızca son istemci kapsam dışına çıktığında silerler; bu nedenle, ne kadar müşterinin olacağı ve nesneyi ne kadar süre kullanacakları tamamen bilinmediği durumlarda kullanılabilir.
Özet olarak, gerçekten herhangi bir manuel bellek yönetimi yapmadığınızı görüyoruz. Her şey kapsüllenir ve daha sonra tamamen otomatik, kapsam tabanlı bellek yönetimi ile halledilir. Bunun yeterli olmadığı durumlarda, ham belleği içine alan akıllı işaretçiler kullanılır.
Ham işaretçileri C ++ kodunun herhangi bir yerinde kaynak sahibi olarak, inşaatçıların dışındaki ham tahsisatların ve delete
imha dışında yapılan ham çağrıların, istisnalar ortaya çıktığında yönetmek neredeyse imkansız ve genellikle güvenli bir şekilde kullanılması neredeyse imkansızdır.
En iyisi: bu her tür kaynak için işe yarar
RAII'nin en büyük yararlarından biri hafızayla sınırlı olmamasıdır. Aslında dosyalar ve soketler (açma / kapama) ve muteksler (senkronizasyon / kilit açma) gibi senkronizasyon mekanizmaları gibi kaynakları yönetmek için çok doğal bir yol sağlar. Temel olarak, edinilebilecek ve serbest bırakılması gereken her kaynak C ++ ile aynı şekilde yönetilir ve bu yönetimin hiçbiri kullanıcıya bırakılmaz. Hepsi, kurucuda edinen ve yıkıcıya salıverilen sınıflarda kapsüllenir.
Örneğin, bir muteksi kilitleyen bir işlev genellikle C ++ dilinde şöyle yazılır:
void criticalSection() {
std::scoped_lock lock(myMutex); // scoped_lock locks the mutex
doSynchronizedStuff();
} // myMutex is released here automatically
Diğer diller, bunu manuel olarak yapmanızı (örneğin bir finally
cümlede) zorunlu kılmak suretiyle ya da bu problemi çözen uzmanlaşmış mekanizmalar ortaya koyar, ancak özellikle zarif bir şekilde değil (genellikle daha sonra, insanların yeterli olduğunda eksiklikten muzdarip). Bu mekanizmalar vardır denemek-ile-kaynaklar Java ve kullanma C ++ 'ın de ray değer yaklaşık her ikisi de C # açıklamada,.
Yani, özetlemek gerekirse, bunların hepsi C ++ 'da RAII'nin çok yüzeysel bir ifadesiydi, ancak okuyucunun C ++' da bellek ve hatta kaynak yönetiminin genellikle "manuel" değil aslında çoğunlukla otomatik olduğunu anlamalarına yardımcı olacağını umuyorum.