C ++ programlarında belleği sızdırmadığımdan emin olmak için bazı genel ipuçları nelerdir? Dinamik olarak tahsis edilmiş hafızayı kimin boşaltması gerektiğini nasıl anlayabilirim?
C ++ programlarında belleği sızdırmadığımdan emin olmak için bazı genel ipuçları nelerdir? Dinamik olarak tahsis edilmiş hafızayı kimin boşaltması gerektiğini nasıl anlayabilirim?
Yanıtlar:
Belleği manuel olarak yönetmek yerine, uygun olan yerlerde akıllı işaretçiler kullanmaya çalışın. Boost lib , TR1 ve akıllı işaretleyicilere
bir göz atın .
Ayrıca akıllı işaretçiler artık C ++ 11 adlı C ++ standardının bir parçasıdır .
RAII ve akıllı işaretçiler hakkındaki tüm tavsiyeleri tamamen onaylıyorum, ancak biraz daha yüksek seviyeli bir ipucu da eklemek istiyorum: yönetilmesi en kolay bellek, asla ayırmadığınız bellektir. Hemen hemen her şeyin bir referans olduğu C # ve Java gibi dillerin aksine, C ++ 'da mümkün olduğunca nesneler yığına koymalısınız. Birkaç kişinin (Dr Stroustrup dahil) işaret ettiği gibi, C ++ 'da çöp toplamanın hiç popüler olmamasının ana nedeni, iyi yazılmış C ++' nın ilk etapta fazla çöp üretmemesidir.
Yazma
Object* x = new Object;
ya da
shared_ptr<Object> x(new Object);
ne zaman yazabilirsin
Object x;
Bu gönderi tekrarlı gibi görünüyor, ancak C ++ 'da bilinmesi gereken en temel kalıp RAII'dir .
Hem boost, TR1 hem de düşük (ancak genellikle yeterince verimli) auto_ptr'den (ancak sınırlamalarını bilmelisiniz) akıllı işaretçileri kullanmayı öğrenin.
RAII, C ++ 'da hem istisnai güvenlik hem de kaynak kullanımının temelidir ve başka hiçbir model (sandviç vb.) Size ikisini birden vermez (ve çoğu zaman size hiçbir şey vermez).
Aşağıda RAII ve RAII olmayan kodun karşılaştırmasına bakın:
void doSandwich()
{
T * p = new T() ;
// do something with p
delete p ; // leak if the p processing throws or return
}
void doRAIIDynamic()
{
std::auto_ptr<T> p(new T()) ; // you can use other smart pointers, too
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
void doRAIIStatic()
{
T p ;
// do something with p
// WON'T EVER LEAK, even in case of exceptions, returns, breaks, etc.
}
Özetlemek gerekirse ( Ogre Mezmur 33'ün yorumundan sonra ), RAII üç kavrama dayanır:
Bu, doğru C ++ kodunda çoğu nesnenin oluşturulmayacağı new
ve bunun yerine yığın üzerinde bildirileceği anlamına gelir . Ve kullanılarak inşa edilenler için new
, tümü bir şekilde kapsamlı olacaktır (örneğin, akıllı bir işaretçiye iliştirilmiş).
Bir geliştirici olarak, bu gerçekten çok güçlüdür, çünkü manuel kaynak işlemeyi önemsemeniz gerekmeyecektir (C'de yapıldığı gibi veya Java'daki bazı nesneler için try
/ finally
bu durumda yoğun şekilde kullanılır ) ...
"kapsamlı nesneler ... çıkış ne olursa olsun ... yok edilecek" bu tamamen doğru değil. RAII'yi aldatmanın yolları vardır. herhangi bir terminate () çeşidi temizlemeyi atlayacaktır. exit (EXIT_SUCCESS) bu bağlamda bir tezattır.
wilhelmtell bu konuda oldukça haklı: RAII'yi aldatmak için istisnai yollar vardır , bunların hepsi sürecin aniden durmasına neden olur.
Bunlar istisnai yollardır çünkü C ++ kodu sonlandırma, çıkış vb. İle karıştırılmamıştır veya istisnalar olması durumunda, işlenmemiş bir istisna istiyoruz. , işlemin çökmesi ve çekirdeğin bellek görüntüsünü temizledikten sonra değil olduğu gibi çökertmesi için .
Ancak yine de bu vakaları bilmeliyiz çünkü nadiren olsa da yine de olabilirler.
( C ++ kodunu kim arıyor terminate
ya exit
da sıradan mı? ... GLUT ile oynarken bu problemle uğraşmak zorunda olduğumu hatırlıyorum : Bu kütüphane, C ++ geliştiricileri için işleri önemsememek gibi zorlaştıracak şekilde aktif olarak tasarlamaya kadar çok C odaklıdır. Yığın ayrılmış veriler hakkında veya ana döngülerinden asla geri dönmemek konusunda "ilginç" kararlar almak hakkında ... Bu konuda yorum yapmayacağım) .
Aşağıdaki gibi akıllı işaretçiler, bakmak isteyeceksiniz Boost akıllı işaretçiler .
Onun yerine
int main()
{
Object* obj = new Object();
//...
delete obj;
}
boost :: shared_ptr, referans sayısı sıfır olduğunda otomatik olarak silinir:
int main()
{
boost::shared_ptr<Object> obj(new Object());
//...
// destructor destroys when reference count is zero
}
Son notuma dikkat edin, "referans sayısı sıfır olduğunda, bu en havalı kısımdır. Dolayısıyla, nesnenizin birden fazla kullanıcısına sahipseniz, nesnenin hala kullanımda olup olmadığını takip etmek zorunda kalmayacaksınız. Hiç kimse sizin paylaşılan işaretçi yok edilir.
Ancak bu her derde deva değil. Temel işaretçiye erişebilmenize rağmen, ne yaptığından emin olmadığınız sürece onu bir 3. parti API'ye geçirmek istemezsiniz. Çoğu zaman, kapsam oluşturma bittikten SONRA yapılacak işler için başka bir iş parçacığına "gönderdiğiniz" şeyler. Bu, Win32'deki PostThreadMessage ile ortaktır:
void foo()
{
boost::shared_ptr<Object> obj(new Object());
// Simplified here
PostThreadMessage(...., (LPARAM)ob.get());
// Destructor destroys! pointer sent to PostThreadMessage is invalid! Zohnoes!
}
Her zaman olduğu gibi, düşünme kapınızı herhangi bir aletle kullanın ...
Bellek sızıntılarının çoğu, nesne sahipliği ve kullanım ömrü konusunda net olmamanın sonucudur.
Yapmanız gereken ilk şey, mümkün olduğunca Yığın üzerinde ayırmaktır. Bu, bir amaç için tek bir nesne ayırmanız gereken çoğu durumla ilgilidir.
Bir nesneyi 'yenileştirmeniz' gerekiyorsa, çoğu zaman ömrünün geri kalanında tek bir açık sahibi olacaktır. Bu durum için, işaretçi tarafından içlerinde depolanan nesnelere 'sahip olmak' için tasarlanmış bir dizi koleksiyon şablonu kullanma eğilimindeyim. STL vektörü ve harita kapsayıcıları ile uygulanırlar ancak bazı farklılıkları vardır:
STL ile aradığım şey, çoğu uygulamada nesneler, bu kaplarda kullanım için gerekli olan anlamlı kopya anlamlarına sahip olmayan benzersiz varlıklar iken, Değer nesnelerine çok odaklanmış olmasıdır.
Bah, siz küçük çocuklar ve yeni moda çöp toplayıcılarınız ...
"Mülkiyet" konusunda çok güçlü kurallar - yazılımın hangi nesnesinin veya bölümünün nesneyi silme hakkına sahip olduğu. Bir işaretçinin "sahibi" veya "sadece bak, dokunma" olup olmadığını anlamak için yorumları ve akıllı değişken adlarını temizleyin. Kimin neye sahip olduğuna karar vermeye yardımcı olmak için, her alt program veya yöntemdeki "sandviç" modelini olabildiğince izleyin.
create a thing
use that thing
destroy that thing
Bazen çok farklı yerlerde yaratmak ve yok etmek gerekir; Bundan kaçınmanın zor olduğunu düşünüyorum.
Karmaşık veri yapıları gerektiren herhangi bir programda, "sahip" işaretçileri kullanarak diğer nesneleri içeren kesin bir nesne ağacı oluşturuyorum. Bu ağaç, uygulama alanı kavramlarının temel hiyerarşisini modeller. Örnek bir 3B sahnenin nesneleri, ışıkları, dokuları vardır. Oluşturmanın sonunda program kapandığında, her şeyi yok etmenin açık bir yolu var.
Diğer birçok işaretçi, bir varlığın diğerine erişmesi gerektiğinde, gecikmeleri veya her neyse taramak için gerektiği gibi tanımlanır; bunlar "sadece bakma" dır. 3B sahne örneği için - bir nesne bir doku kullanır ancak kendisine ait değildir; diğer nesneler aynı dokuyu kullanabilir. Bir nesnenin imha etmez olmayan bir doku yıkımı çağırır.
Evet zaman alıyor ama ben öyle yapıyorum. Nadiren bellek sızıntılarım veya başka sorunlar yaşıyorum. Ancak daha sonra, yüksek performanslı bilimsel, veri toplama ve grafik yazılımlarının sınırlı alanında çalışıyorum. Bankacılık ve e-ticaret, olay odaklı GUI'ler veya yüksek ağa sahip eşzamansız kaos gibi işlemleri sık sık yapmıyorum. Belki de yeni moda yöntemlerin burada bir avantajı vardır!
Harika soru!
c ++ kullanıyorsanız ve gerçek zamanlı CPU ve bellek boud uygulaması geliştiriyorsanız (oyunlar gibi) kendi Bellek Yöneticinizi yazmanız gerekir.
Bence çeşitli yazarların bazı ilginç çalışmalarını birleştirmek ne kadar iyi olursa, size bir ipucu verebilirim:
Sabit boyutlu ayırıcı, ağın her yerinde yoğun bir şekilde tartışılıyor
Küçük Nesne Tahsisi, Alexandrescu tarafından 2001 yılında mükemmel kitabı "Modern c ++ tasarımı" nda tanıtıldı.
Dimitar Lazarov tarafından yazılan "Yüksek Performanslı Yığın ayırıcı" adlı Game Programming Gem 7'deki (2008) harika bir makalede büyük bir gelişme (kaynak kodu dağıtılmış olarak) bulunabilir.
Bu makalede harika bir kaynak listesi bulunabilir
Kendi başınıza işe yaramaz bir ayırıcı yazmaya başlamayın ... Önce KENDİNİZİ BELGELEYİN.
C ++ 'da bellek yönetimi ile popüler hale gelen tekniklerden biri RAII'dir . Temel olarak, kaynak tahsisini idare etmek için yapıcıları / yıkıcıları kullanırsınız. Elbette, istisna güvenliği nedeniyle C ++ 'da başka iğrenç ayrıntılar da var, ancak temel fikir oldukça basit.
Sorun genellikle mülkiyet meselesine bağlı. Scott Meyers'in Etkili C ++ serisini ve Andrei Alexandrescu'nun Modern C ++ Tasarımını okumanızı şiddetle tavsiye ederim.
Nasıl sızdırılmayacağına dair zaten çok şey var, ancak sızıntıları izlemenize yardımcı olacak bir araca ihtiyacınız varsa bir göz atın:
Projeniz genelinde bellek sahipliği kurallarını paylaşın ve bilin. COM kurallarını kullanmak en iyi tutarlılığı sağlar ([in] parametreler arayan kişiye aittir, arayan uç kopyalamalıdır; [dışarı] parametreler arayan kişiye aittir, arayan uç bir referans tutuyorsa bir kopya çıkarmalıdır; vb.)
valgrind , programlarınızın bellek sızıntılarını çalışma zamanında da kontrol etmek için iyi bir araçtır.
Linux'un çoğu çeşidinde (Android dahil) ve Darwin'de mevcuttur.
Programlarınız için birim testleri yazmak için kullanırsanız, testler üzerinde sistematik olarak valgrind çalıştırma alışkanlığı edinmelisiniz. Erken bir aşamada birçok bellek sızıntısını önleyecektir. Ayrıca, tam bir yazılımda olduğu gibi basit testlerde bunları belirlemek genellikle daha kolaydır.
Elbette bu tavsiye, diğer herhangi bir hafıza kontrol aracı için geçerli kalır.
Bir şey için akıllı bir işaretçi kullanamıyorsanız / kullanmıyorsanız (bu çok büyük bir kırmızı bayrak olmalıdır), kodunuzu şu şekilde yazın:
allocate
if allocation succeeded:
{ //scope)
deallocate()
}
Bu açık, ancak kapsamda herhangi bir kod yazmadan önce yazdığınızdan emin olun.
Bu hataların sık görülen bir kaynağı, bir nesneye bir referans veya işaretçi kabul eden, ancak sahipliği belirsiz bırakan bir yönteme sahip olduğunuz zamandır. Stil ve yorum kuralları bunu daha az olası hale getirebilir.
İşlevin nesnenin sahipliğini aldığı durum özel durum olsun. Bunun olduğu her durumda, başlık dosyasındaki işlevin yanına bunu belirten bir yorum yazdığınızdan emin olun. Çoğu durumda, bir nesneyi tahsis eden modülün veya sınıfın, onu serbest bırakmaktan da sorumlu olduğundan emin olmak için çabalamalısınız.
Const kullanmak bazı durumlarda çok yardımcı olabilir. Bir işlev bir nesneyi değiştirmeyecekse ve ona döndükten sonra da devam eden bir başvuru kaydetmiyorsa, bir const başvurusunu kabul edin. Arayanın kodunu okuduktan sonra, işlevinizin nesnenin sahipliğini kabul etmediği anlaşılacaktır. Aynı işlevin const olmayan bir işaretçiyi kabul etmesini sağlayabilirsiniz ve arayan uç, aranan ucun sahipliği kabul ettiğini varsayabilir veya varsaymayabilir, ancak const referansı ile soru yoktur.
Bağımsız değişken listelerinde const olmayan başvurular kullanmayın. Arayanın kodunu okurken, aranan ucun parametreye bir referans tutmuş olabileceği çok açık değildir.
Referans sayılan işaretçileri tavsiye eden yorumlara katılmıyorum. Bu genellikle iyi çalışır, ancak bir hatanız olduğunda ve çalışmadığında, özellikle yıkıcınız çok iş parçacıklı bir programda olduğu gibi önemsiz olmayan bir şey yaparsa. Kesinlikle tasarımınızı çok zor değilse referans saymaya ihtiyaç duymayacak şekilde ayarlamaya çalışın.
Önem Sırasına Göre İpuçları:
-İpucu # 1 Yıkıcılarınızı "sanal" ilan etmeyi her zaman unutmayın.
-İpucu # 2 RAII kullanın
-İpucu 3 Boost'un akıllı işaretleyicilerini kullanın
-İpucu # 4 Kendi buggy Smartpointer'larınızı yazmayın, boost kullanın (şu anda bulunduğum bir projede boost kullanamıyorum ve kendi akıllı işaretçilerimde hata ayıklamak zorunda kaldım, kesinlikle kabul etmem yine aynı rota, ancak şimdi yine bağımlılıklarımıza destek ekleyemiyorum)
-İpucu # 5 Bazı sıradan / performans dışı kritik (binlerce nesneli oyunlarda olduğu gibi) çalışıyorsa, Thorsten Ottosen'in artırma işaretçisi kabına bakın
-İpucu # 6 Görsel Kaçak Tespiti'nin "vld" başlığı gibi tercih ettiğiniz platform için bir sızıntı tespit başlığı bulun
Mümkünse, boost shared_ptr ve standart C ++ auto_ptr kullanın. Bunlar sahiplik anlamını aktarır.
Bir auto_ptr'ye döndüğünüzde, arayan kişiye hafızanın sahipliğini verdiğinizi söylüyorsunuz.
Shared_ptr döndürdüğünüzde, arayan kişiye ona bir referansınız olduğunu ve mülkiyetin bir parçası olduğunu söylüyorsunuz, ancak bu yalnızca onların sorumluluğunda değil.
Bu anlambilim, parametreler için de geçerlidir. Arayan kişi size bir auto_ptr iletirse, size sahiplik veriyor demektir.
Başkaları ilk başta bellek sızıntılarından kaçınmanın yollarından bahsetmişlerdir (akıllı işaretçiler gibi). Ancak bir profil oluşturma ve bellek analizi aracı, genellikle bellek sorunlarını bir kez elde ettiğinizde izlemenin tek yoludur.
Valgrind memcheck mükemmel bir ücretsiz olanıdır.
Yalnızca MSVC için, her .cpp dosyasının üstüne aşağıdakileri ekleyin:
#ifdef _DEBUG
#define new DEBUG_NEW
#endif
Daha sonra, VS2003 veya üstü ile hata ayıklarken, programınızdan çıkıldığında herhangi bir sızıntı olduğu söylenecektir (yeni / silme işlemini izler). Basit ama geçmişte bana yardımcı oldu.
valgrind (yalnızca * nix platformları için geçerlidir) çok güzel bir bellek denetleyicidir
Belleğinizi manuel olarak yönetecekseniz, iki durumunuz vardır:
Bu kurallardan herhangi birini ihlal etmeniz gerekiyorsa, lütfen bunu belgeleyin.
Her şey işaretçi sahipliğiyle ilgili.
Bellek ayırma işlevlerini durdurabilir ve programdan çıkıldığında bazı bellek bölgelerinin olup olmadığını görebilirsiniz (ancak tüm uygulamalar için uygun değildir ).
Ayrıca yeni ve silme operatörleri ve diğer bellek ayırma işlevleri değiştirilerek derleme zamanında da yapılabilir.
Örneğin bu siteyi kontrol edin [C ++ 'da bellek ayırmada hata ayıklama] Not: Silme operatörü için bir numara da var, bunun gibi bir şey:
#define DEBUG_DELETE PrepareDelete(__LINE__,__FILE__); delete
#define delete DEBUG_DELETE
Bazı değişkenlerde dosyanın adını saklayabilirsiniz ve aşırı yüklenmiş silme operatörü hangi yerden çağrıldığını bilecektir. Bu şekilde, programınızdan her silme ve malloc'un izini sürebilirsiniz. Bellek kontrol dizisinin sonunda, hangi tahsis edilmiş bellek bloğunun 'silinmediğini' rapor edebilmelisiniz, bunu dosya adı ve satır numarasıyla tanımlamalısınız ki bu sizin ne istediğinizi tahmin ediyorum.
Ayrıca oldukça ilginç ve kullanımı kolay olan Visual Studio altında BoundsChecker gibi bir şey de deneyebilirsiniz .
Tüm ayırma işlevlerimizi, ön tarafa kısa bir dize ve sonuna bir nöbetçi bayrak ekleyen bir katmanla sarmalıyoruz. Örneğin, "myalloc (pszSomeString, iSize, iAlignment); veya new (" description ", iSize) MyObject () için bir çağrınız olur; bu, belirtilen boyut artı başlığınız ve sentineliniz için yeterli alanı dahili olarak ayırır. , hata ayıklama olmayan yapılar için bunu yorumlamayı unutmayın! Bunu yapmak için biraz daha fazla bellek gerekir, ancak faydaları maliyetlerden çok daha ağır basar.
Bunun üç faydası vardır - birincisi, belirli 'bölgelere' tahsis edilmiş kod için hızlı aramalar yaparak, ancak bu bölgelerin serbest bırakılması gerektiğinde temizlenmeden, hangi kodun sızdığını kolayca ve hızlı bir şekilde izlemenize olanak tanır. Tüm nöbetçilerin sağlam olduğundan emin olmak için kontrol edilerek bir sınırın üzerine ne zaman yazıldığını tespit etmek de yararlı olabilir. Bu, iyi gizlenmiş çökmeleri veya dizi yanlış adımlarını bulmaya çalışırken bizi defalarca kurtardı. Üçüncü fayda, büyük oyuncuların kim olduğunu görmek için bellek kullanımını izlemektir - örneğin, bir MemDump'taki belirli açıklamaların bir harmanlaması, 'sesin' beklediğinizden çok daha fazla yer kapladığını size söyler.
C ++, RAII göz önünde bulundurularak tasarlanmıştır. Bence C ++ 'da belleği yönetmenin daha iyi bir yolu yok. Ancak yerel kapsamda çok büyük parçaları (tampon nesneleri gibi) ayırmamaya dikkat edin. Yığın taşmalarına neden olabilir ve bu öbeği kullanırken sınır kontrolünde bir kusur varsa, diğer değişkenlerin veya dönüş adreslerinin üzerine yazabilirsiniz, bu da her türlü güvenlik açığına yol açar.
Farklı yerlerde ayırma ve yok etme ile ilgili tek örneklerden biri, iş parçacığı oluşturma (geçirdiğiniz parametre). Ancak bu durumda bile kolaydır. İşte bir iş parçacığı oluşturan işlev / yöntem:
struct myparams {
int x;
std::vector<double> z;
}
std::auto_ptr<myparams> param(new myparams(x, ...));
// Release the ownership in case thread creation is successfull
if (0 == pthread_create(&th, NULL, th_func, param.get()) param.release();
...
Bunun yerine iş parçacığı işlevi
extern "C" void* th_func(void* p) {
try {
std::auto_ptr<myparams> param((myparams*)p);
...
} catch(...) {
}
return 0;
}
Oldukça kolay değil mi? İş parçacığı oluşturmanın başarısız olması durumunda kaynak auto_ptr tarafından serbest bırakılacak (silinecek), aksi takdirde sahiplik iş parçacığına geçecektir. Ya iş parçacığı o kadar hızlıysa, oluşturulduktan sonra kaynağı
param.release();
ana işlev / yöntemde çağrılır? Hiçbir şey değil! Çünkü auto_ptr'ye ayrılmayı yok saymasını 'söyleyeceğiz'. C ++ bellek yönetimi kolay değil mi? Alkış,
Ema!
Belleği, diğer kaynakları (tutamaçlar, dosyalar, db bağlantıları, soketler ...) yönettiğiniz şekilde yönetin. GC de size yardımcı olmaz.
Herhangi bir işlevden tam olarak bir dönüş. Bu şekilde orada ayırma yapabilir ve asla kaçırmazsınız.
Aksi takdirde hata yapmak çok kolaydır:
new a()
if (Bad()) {delete a; return;}
new b()
if (Bad()) {delete a; delete b; return;}
... // etc.