C ++ ile pratikte RAII nedir, akıllı işaretçiler nelerdir, bunlar bir programda nasıl uygulanır ve RAII'yi akıllı işaretçilerle kullanmanın faydaları nelerdir?
C ++ ile pratikte RAII nedir, akıllı işaretçiler nelerdir, bunlar bir programda nasıl uygulanır ve RAII'yi akıllı işaretçilerle kullanmanın faydaları nelerdir?
Yanıtlar:
RAII'nin basit (ve belki de aşırı) bir örneği bir File sınıfıdır. RAII olmadan, kod şöyle görünebilir:
File file("/path/to/file");
// Do stuff with file
file.close();
Başka bir deyişle, dosyayı bitirdikten sonra kapattığımızdan emin olmalıyız. Bunun iki dezavantajı vardır - ilk olarak, Dosya'yı her nerede kullanırsak kullanalım, File :: close () adını vermeliyiz. İkinci sorun, dosyayı kapatmadan önce bir istisna atılırsa ne olur?
Java, nihayet yan tümcesini kullanarak ikinci sorunu çözer:
try {
File file = new File("/path/to/file");
// Do stuff with file
} finally {
file.close();
}
veya Java 7'den beri, bir kaynak-denemesi bildirimi:
try (File file = new File("/path/to/file")) {
// Do stuff with file
}
C ++ RAII kullanarak her iki sorunu da çözer - yani, Dosya yıkıcısındaki dosyayı kapatır. File nesnesi doğru zamanda (yine de olması gerekir) yok edildiği sürece, dosyayı kapatmak bizim için halledilir. Yani, kodumuz şimdi şuna benziyor:
File file("/path/to/file");
// Do stuff with file
// No need to close it - destructor will do that for us
Bu, Java'da yapılamaz çünkü nesnenin ne zaman imha edileceğini garanti etmez, bu nedenle dosya gibi bir kaynağın ne zaman serbest bırakılacağını garanti edemeyiz.
Akıllı işaretçiler üzerine - çoğu zaman, sadece yığın üzerinde nesneler yaratırız. Örneğin (ve başka bir cevaptan bir örnek çalmak):
void foo() {
std::string str;
// Do cool things to or using str
}
Bu iyi çalışıyor - ama str dönmek istiyorsak? Bunu yazabiliriz:
std::string foo() {
std::string str;
// Do cool things to or using str
return str;
}
Peki, bunda yanlış olan ne? Dönüş türü std :: string - yani değere göre döndürdüğümüz anlamına gelir. Bu, str kopyaladığımız ve aslında kopyayı döndürdüğümüz anlamına gelir. Bu pahalı olabilir ve kopyalama maliyetinden kaçınmak isteyebiliriz. Bu nedenle, referans veya işaretçi ile geri dönme fikri ortaya çıkabilir.
std::string* foo() {
std::string str;
// Do cool things to or using str
return &str;
}
Ne yazık ki, bu kod çalışmıyor. Str'ye bir işaretçi döndürüyoruz - ancak yığın üzerinde str oluşturuldu, bu nedenle foo () 'dan çıktıktan sonra silineceğiz. Diğer bir deyişle, arayan işaretçiyi aldığında işe yaramaz (ve muhtemelen her türlü korkak hataya neden olabileceğinden işe yaramazdan daha kötüdür)
Peki, çözüm nedir? Yeni kullanarak yığın üzerinde str oluşturabiliriz - bu şekilde, foo () tamamlandığında str imha edilmez.
std::string* foo() {
std::string* str = new std::string();
// Do cool things to or using str
return str;
}
Tabii ki, bu çözüm de mükemmel değil. Nedeni str oluşturduk, ama asla silmiyoruz. Bu çok küçük bir programda sorun olmayabilir, ancak genel olarak, sildiğimizden emin olmak istiyoruz. Arayanın nesneyi bitirdiğinde silmesi gerektiğini söyleyebiliriz. Dezavantajı, arayanın ekstra karmaşıklık ekleyen ve yanlış anlayabilen hafızayı yönetmesi gerektiğidir.
Akıllı işaretçiler burada devreye giriyor. Aşağıdaki örnek shared_ptr kullanıyor - aslında ne kullanmak istediğinizi öğrenmek için farklı akıllı işaretçi türlerine bakmanızı öneririm.
shared_ptr<std::string> foo() {
shared_ptr<std::string> str = new std::string();
// Do cool things to or using str
return str;
}
Şimdi shared_ptr, str. Örneğin
shared_ptr<std::string> str = foo();
shared_ptr<std::string> str2 = str;
Şimdi aynı dizeye iki referans var. Str için kalan referans olmadığında, silinir. Bu nedenle, artık kendiniz silmek konusunda endişelenmenize gerek yok.
Hızlı düzenleme: yorumların bazılarının belirttiği gibi, bu örnek (en azından!) İki nedenden dolayı mükemmel değildir. İlk olarak, dizelerin uygulanması nedeniyle, bir dizenin kopyalanması ucuz olma eğilimindedir. İkincisi, adı verilen dönüş değeri optimizasyonu olarak bilinen şey nedeniyle, derleyici işleri hızlandırmak için biraz akıllılık yapabileceğinden değere göre döndürmek pahalı olmayabilir.
Şimdi File sınıfımızı kullanarak farklı bir örnek deneyelim.
Diyelim ki bir dosyayı günlük olarak kullanmak istiyoruz. Bu, dosyamızı yalnızca ekleme modunda açmak istediğimiz anlamına gelir:
File file("/path/to/file", File::append);
// The exact semantics of this aren't really important,
// just that we've got a file to be used as a log
Şimdi, dosyamızı birkaç başka nesnenin günlüğü olarak ayarlayalım:
void setLog(const Foo & foo, const Bar & bar) {
File file("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Ne yazık ki, bu örnek korkunç bir şekilde bitiyor - bu yöntem sona erdiğinde dosya kapatılacak, yani foo ve bar artık geçersiz bir günlük dosyasına sahip olacak. Öbek üzerinde dosya oluşturabilir ve hem foo hem de çubuğa dosyaya bir işaretçi aktarabiliriz:
void setLog(const Foo & foo, const Bar & bar) {
File* file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Ama sonra dosyayı silmekten kim sorumlu? Her iki dosyayı da silemezseniz, hem bellek hem de kaynak sızıntısı olur. Foo'nun veya çubuğun önce dosyayla bitip bitmeyeceğini bilmiyoruz, bu nedenle her ikisinin de dosyayı silmesini bekleyemeyiz. Örneğin, foo, çubuk tamamlanmadan dosyayı silerse, çubuk artık geçersiz bir işaretçiye sahiptir.
Tahmin edebileceğiniz gibi, bize yardımcı olması için akıllı işaretçiler kullanabiliriz.
void setLog(const Foo & foo, const Bar & bar) {
shared_ptr<File> file = new File("/path/to/file", File::append);
foo.setLogFile(file);
bar.setLogFile(file);
}
Şimdi, hiç kimsenin dosyayı silme konusunda endişelenmesine gerek yok - hem foo hem de çubuk bittiğinde ve artık dosyaya herhangi bir referansı olmadığında (muhtemelen foo ve bar imha edildiğinden), dosya otomatik olarak silinecektir.
RAII Bu basit ama harika bir konsept için garip bir isim. Scope Bound Kaynak Yönetimi (SBRM) adı daha iyidir . Fikir, genellikle bir bloğun başlangıcında kaynakları tahsis etmeniz ve bir bloğun çıkışında serbest bırakmanız gerektiğidir. Bloktan çıkmak normal akış kontrolüyle, dışarı atlayarak ve hatta bir istisna ile olabilir. Tüm bu durumları kapsamak için, kod daha karmaşık ve gereksiz hale gelir.
SBRM olmadan bunu yapan bir örnek:
void o_really() {
resource * r = allocate_resource();
try {
// something, which could throw. ...
} catch(...) {
deallocate_resource(r);
throw;
}
if(...) { return; } // oops, forgot to deallocate
deallocate_resource(r);
}
Gördüğünüz gibi, kandırmanın birçok yolu var. Fikir, kaynak yönetimini bir sınıfa yerleştirmemiz. Nesnesinin başlatılması kaynağı alır ("Kaynak Edinimi Başlatmadır"). Bloktan çıktığımızda (blok kapsamı), kaynak tekrar serbest bırakılır.
struct resource_holder {
resource_holder() {
r = allocate_resource();
}
~resource_holder() {
deallocate_resource(r);
}
resource * r;
};
void o_really() {
resource_holder r;
// something, which could throw. ...
if(...) { return; }
}
Sadece kaynakları tahsis etme / dağıtma amacı taşımayan kendi sınıflarınız varsa bu güzeldir. Tahsis, işlerini yapmak için sadece ek bir endişe olacaktır. Ancak, kaynakları ayırmak / dağıtmak istediğinizde, yukarıdakiler kullanışlı olmaz. Aldığınız her türlü kaynak için bir sarma sınıfı yazmalısınız. Bunu kolaylaştırmak için akıllı işaretçiler bu işlemi otomatikleştirmenize izin verir:
shared_ptr<Entry> create_entry(Parameters p) {
shared_ptr<Entry> e(Entry::createEntry(p), &Entry::freeEntry);
return e;
}
Normalde akıllı işaretçiler, sahip delete
oldukları kaynak kapsam dışına çıktığında çağıran yeni / sil etrafında ince sarmalayıcılardır . Shared_ptr gibi bazı akıllı işaretçiler, onlara bunun yerine kullanılan bir silici söylemenizi sağlar delete
. Örneğin, shared_ptr öğesine doğru silme hakkında bilgi verdiğiniz sürece pencere tutamaçlarını, normal ifade kaynaklarını ve diğer rasgele şeyleri yönetmenizi sağlar.
Farklı amaçlar için farklı akıllı işaretçiler vardır:
Kod:
unique_ptr<plot_src> p(new plot_src); // now, p owns
unique_ptr<plot_src> u(move(p)); // now, u owns, p owns nothing.
unique_ptr<plot_src> v(u); // error, trying to copy u
vector<unique_ptr<plot_src>> pv;
pv.emplace_back(new plot_src);
pv.emplace_back(new plot_src);
Auto_ptr öğesinin aksine, unique_ptr bir kaba konabilir, çünkü kaplar akışlar ve unique_ptr gibi kopyalanamayan (ancak taşınabilir) türleri de tutabilir.
Kod:
void do_something() {
scoped_ptr<pipe> sp(new pipe);
// do something here...
} // when going out of scope, sp will delete the pointer automatically.
Kod:
shared_ptr<plot_src> p(new plot_src(&fx));
plot1->add(p)->setColor("#00FF00");
plot2->add(p)->setColor("#FF0000");
// if p now goes out of scope, the src won't be freed, as both plot1 and
// plot2 both still have references.
Gördüğünüz gibi, arsa kaynağı (işlev fx) paylaşılır, ancak her birinin rengini ayarladığımız ayrı bir girişi vardır. Kodun akıllı bir işaretçinin sahip olduğu kaynağa başvurması gerektiğinde kullanılan ancak kaynağın sahibi olması gerekmeyen bir zayıf_ptr sınıfı vardır. Ham bir işaretçiyi iletmek yerine, bir poor_ptr oluşturmanız gerekir. Kaynağa artık paylaşılan bir shareptr olmasa bile, kaynağa zayıf bir_ptr erişim yolu ile erişmeye çalıştığınızı fark ettiğinde bir istisna atar.
unique_ptr
ve sort
aynı şekilde değiştirilecektir.
RAII, değişkenlerin yapıcılarında gerekli tüm başlatmayı ve yıkıcılarında gerekli tüm temizlemeyi gerçekleştirmelerini sağlayan tasarım paradigmasıdır . Bu, tüm başlatma ve temizlemeyi tek bir adıma azaltır.
C ++ RAII gerektirmez, ancak RAII yöntemlerini kullanmanın daha sağlam kod üreteceği giderek daha fazla kabul edilmektedir.
RAII'nin C ++ 'da yararlı olmasının nedeni, C ++' ın, normal kod akışı veya bir istisna tarafından tetiklenen yığın çözme yoluyla kapsama girip çıkarken değişkenlerin oluşturulmasını ve yok edilmesini kendiliğinden yönetmesidir. Bu C ++ 'da bir freebie.
Tüm başlatma ve temizleme işlemlerini bu mekanizmalara bağlayarak, C ++ 'ın da bu işi sizin için halledeceğinden emin olursunuz.
C ++ 'da RAII hakkında konuşmak genellikle akıllı işaretçilerin tartışılmasına yol açar, çünkü işaretçiler temizlik söz konusu olduğunda özellikle kırılgandır. Malloc veya new'den alınan yığın tahsisli belleği yönetirken, imleç imha edilmeden önce bu belleği boşaltmak veya silmek genellikle programcının sorumluluğundadır. Akıllı işaretçiler, işaretçi değişkeni her yok edildiğinde yığın tahsisli nesnelerin yok edilmesini sağlamak için RAII felsefesini kullanır.
Akıllı işaretçi RAII'nin bir varyasyonudur. RAII, kaynak edinmenin başlatma olduğu anlamına gelir. Akıllı işaretçi kullanımdan önce bir kaynak (bellek) alır ve daha sonra otomatik olarak bir yıkıcıya atar. İki şey olur:
Örneğin, başka bir örnek ağ soketi RAII'dir. Bu durumda:
Gördüğünüz gibi, RAII çoğu durumda insanların atılmasına yardımcı olduğu için çok yararlı bir araçtır.
Akıllı işaretçilerin C ++ kaynakları, üstümdeki yanıtlar da dahil olmak üzere, net milyonlarca kişidir.
Boost, Boost'da olanlar da dahil olmak üzere bunlardan birkaçına sahiptir . Özellikle aynı veri yapısını paylaşan 5 işleminiz olduğu gibi baş ağrısına neden olan durumlarda bellek yönetimini büyük ölçüde basitleştirir: Herkes bir bellek yığını ile işini bitirdiğinde, otomatik olarak serbest kalmasını ve anlamaya çalışarak orada oturmasını istemezsiniz. delete
bir bellek hafızasını çağırmaktan kim sorumlu olmalı , bir bellek sızıntısıyla sonuçlanmasa veya yanlışlıkla iki kez serbest bırakılan ve tüm yığını bozabilecek bir işaretçi ile sonuçlanmalıdır.
geçersiz foo () { std :: dize çubuğu; // // burada daha fazla kod // }
Ne olursa olsun, foo () işlevinin kapsamı geride bırakıldıktan sonra çubuk düzgün bir şekilde silinecektir.
Dahili olarak std :: string uygulamaları genellikle referans sayılan işaretçiler kullanır. Bu nedenle, iç dizginin yalnızca dizelerin kopyalarından biri değiştiğinde kopyalanması gerekir. Bu nedenle referans olarak sayılan akıllı işaretçi, yalnızca gerektiğinde bir şey kopyalamayı mümkün kılar.
Ayrıca, dahili referans sayımı, dahili dizenin kopyası artık gerekli olmadığında belleğin düzgün bir şekilde silinmesini mümkün kılar.