Yerel bir değişkenin belleğine kapsamı dışında erişilebilir mi?

Jun 22 2011

Takip koduna sahibim.

#include <iostream>

int * foo()
{
    int a = 5;
    return &a;
}

int main()
{
    int* p = foo();
    std::cout << *p;
    *p = 8;
    std::cout << *p;
}

Ve kod, çalışma zamanı istisnaları olmadan sadece çalışıyor!

Çıktı 58

Nasıl olabilir? Yerel bir değişkenin belleğine işlevi dışında erişilemez değil mi?

Yanıtlar

4836 EricLippert Jun 23 2011 at 03:01

Nasıl olabilir? Yerel bir değişkenin belleğine işlevi dışında erişilemez değil mi?

Bir otel odası kiralıyorsunuz. Komodinin üst çekmecesine bir kitap koyup uyuyorsunuz. Ertesi sabah kontrol ediyorsun, ama anahtarını geri vermeyi "unut". Anahtarı çalıyorsun!

Bir hafta sonra otele dönüyorsunuz, giriş yapmıyorsunuz, çalıntı anahtarınızla eski odanıza gizlice giriyorsunuz ve çekmeceye bakıyorsunuz. Kitabınız hala orada. Şaşırtıcı!

Nasıl olabilir? Odayı kiralamadıysanız, bir otel odası çekmecesinin içeriğine erişilemez mi?

Açıkçası bu senaryo gerçek dünyada problemsiz olabilir. Artık odada olma yetkiniz olmadığında kitabınızın kaybolmasına neden olan hiçbir gizemli güç yoktur. Çalınan bir anahtarla bir odaya girmenizi engelleyen gizemli bir güç de yoktur.

Otel yönetiminin kitabınızı kaldırması gerekmez . Onlarla bir şeyi geride bırakırsanız, sizin için parçalayacaklarını söyleyen bir sözleşme yapmadınız. Eğer yasadışı geri almak için çalıntı bir anahtarla odanızı yeniden girerseniz, otel güvenlik personeli değil gerekli içeri gizlice yakalamak için. Ben içine gizlice geri denerseniz" dedi onlarla bir sözleşme yapmadığını benim oda sonra, beni durdurman gerekiyor. " Aksine, onlarla "daha sonra gizlice odama girmeyeceğime söz veriyorum" diyen bir sözleşme imzaladın, bu senin sözleşmeyi bozdu .

Bu durumda her şey olabilir . Kitap orada olabilir - şanslısın. Başkasının kitabı orada olabilir ve sizinki otelin fırınında olabilir. İçeri girdiğiniz anda biri kitabınızı parçalara ayırarak orada olabilir. Otel masayı kaldırıp tamamen rezerve edip yerine bir gardırop koyabilirdi. Otelin tamamı yıkılıp yerine bir futbol stadyumu konulmak üzere olabilir ve siz gizlice dolaşırken bir patlamada öleceksiniz.

Ne olacağını bilmiyorsun; otelin kullanıma ve yasadışı sonra kullanmak için bir anahtar çaldığında, sen çünkü öngörülebilir, güvenli dünyada yaşama hakkı vazgeçti Eğer sistemin kurallarını kırmak için seçti.

C ++ güvenli bir dil değildir . Sistemin kurallarını neşeyle çiğnemenize izin verecektir. İçine girmeye yetkili olmadığınız bir odaya geri dönmek gibi yasadışı ve aptalca bir şey yapmaya çalışırsanız ve artık orada bile olmayan bir masayı karıştırırsanız, C ++ sizi durdurmayacaktır. C ++ 'dan daha güvenli diller, örneğin tuşlar üzerinde çok daha sıkı denetime sahip olarak gücünüzü kısıtlayarak bu sorunu çözer.

GÜNCELLEME

Aman Tanrım, bu cevap çok dikkat çekiyor. (Neden olduğundan emin değilim - bunun sadece "eğlenceli" küçük bir benzetme olduğunu düşündüm, ama her neyse.)

Bunu biraz daha teknik düşüncelerle güncellemenin daha uygun olacağını düşündüm.

Derleyiciler, o program tarafından işlenen verilerin depolanmasını yöneten kod üretme işindedir. Belleği yönetmek için kod oluşturmanın birçok farklı yolu vardır, ancak zamanla iki temel teknik sağlamlaşmıştır.

Birincisi, depodaki her bir baytın "ömrünün" - yani bazı program değişkenleriyle geçerli bir şekilde ilişkilendirildiği zaman aralığının - önceden kolayca tahmin edilemeyeceği bir tür "uzun ömürlü" depolama alanına sahip olmaktır. zamanın. Derleyici, ihtiyaç duyulduğunda depolamanın dinamik olarak nasıl tahsis edileceğini bilen ve artık ihtiyaç duyulmadığında yeniden talep eden bir "yığın yöneticisine" çağrılar üretir.

İkinci yöntem, her bir baytın ömrünün iyi bilindiği "kısa ömürlü" bir depolama alanına sahip olmaktır. Burada yaşamlar bir "iç içe geçme" modelini izler. Bu kısa ömürlü değişkenlerin en uzun ömürlü olanları, diğer kısa ömürlü değişkenlerden önce tahsis edilecek ve son olarak serbest bırakılacaktır. Daha kısa ömürlü değişkenler, en uzun ömürlü olanlardan sonra tahsis edilecek ve onlardan önce serbest bırakılacaktır. Bu daha kısa ömürlü değişkenlerin yaşam süresi, daha uzun ömürlü olanların yaşam süresi içinde "iç içe geçmiştir".

Yerel değişkenler ikinci modeli izler; bir yöntem girildiğinde, yerel değişkenleri canlanır. Bu yöntem başka bir yöntemi çağırdığında, yeni yöntemin yerel değişkenleri canlanır. İlk yöntemin yerel değişkenleri ölmeden önce ölmüş olacaklar. Yerel değişkenlerle ilişkili depolama yaşamlarının başlangıç ​​ve bitişlerinin göreceli sırası önceden hesaplanabilir.

Bu nedenle, yerel değişkenler genellikle bir "yığın" veri yapısında depolama olarak oluşturulur, çünkü bir yığın, üzerine basılan ilk şeyin son çıkan şey olacağı özelliğine sahiptir.

Sanki otel odaları sırayla kiralamaya karar veriyor ve sizden daha yüksek oda numarasına sahip herkes ödünç verene kadar çıkış yapamazsınız.

Öyleyse yığın hakkında düşünelim. Birçok işletim sisteminde, iş parçacığı başına bir yığın elde edersiniz ve yığın, belirli bir sabit boyutta tahsis edilir. Bir yöntem çağırdığınızda, malzeme yığına itilir. Daha sonra, orijinal posterin burada yaptığı gibi, yönteminizden geri yığına bir işaretçi iletirseniz, bu tamamen geçerli bir milyon bayt bellek bloğunun ortasına yalnızca bir işaretçi. Bizim benzetmemize göre, otelden çıkış yapıyorsunuz; bunu yaptığınızda, az önce dolu olan en yüksek odayı kontrol ettiniz. Sizden sonra kimse check-in yapmazsa ve yasadışı olarak odanıza geri dönerseniz, tüm eşyalarınızın bu otelde kalmaya devam edeceği garanti edilir .

Geçici mağazalar için yığınlar kullanıyoruz çünkü bunlar gerçekten ucuz ve kolay. Yerellerin depolanması için bir yığın kullanmak için C ++ uygulaması gerekli değildir; yığını kullanabilir. Olmaz, çünkü bu programı yavaşlatır.

Daha sonra yasa dışı olarak geri dönebilmeniz için yığında bıraktığınız çöpü el değmeden bırakmak için C ++ uygulaması gerekli değildir; Derleyicinin yeni boşalttığınız "odadaki" her şeyi sıfıra çeviren bir kod üretmesi tamamen yasaldır. Öyle değil çünkü yine, bu pahalı olurdu.

Yığın mantıksal olarak küçüldüğünde, geçerli olan adreslerin hala belleğe eşlenmesini sağlamak için C ++ uygulaması gerekli değildir. Uygulamanın işletim sistemine "artık bu yığın sayfasını kullanarak işimiz bitti. Ben aksini söyleyene kadar, herhangi biri önceden geçerli yığın sayfasına dokunursa işlemi yok eden bir istisna yayınlayın" demesine izin verilir. Yine, uygulamalar yavaş ve gereksiz olduğu için aslında bunu yapmaz.

Bunun yerine, uygulamalar hata yapmanıza ve bundan kurtulmanıza izin verir. Çoğu zaman. Bir gün gerçekten korkunç bir şeyler ters gidene ve süreç patlayana kadar.

Bu sorunludur. Çok fazla kural var ve bunları yanlışlıkla kırmak çok kolay. Kesinlikle birçok kez yaşadım. Daha da kötüsü, sorun genellikle yalnızca belleğin bozulma meydana geldikten sonra milyarlarca nanosaniye bozulmuş olduğu tespit edildiğinde, bunu kimin mahvettiğini anlamak çok zor olduğunda ortaya çıkar.

Daha fazla bellek güvenli dil, gücünüzü kısıtlayarak bu sorunu çözer. "Normal" C # 'da bir yerelin adresini alıp iade etmenin veya daha sonrası için saklamanın bir yolu yoktur. Bir yerelin adresini alabilirsiniz, ancak dil akıllıca tasarlanmıştır, böylece yerel sonların ömrü sona erdikten sonra onu kullanmak imkansızdır. Bir yerelin adresini alıp geri vermek için, derleyiciyi özel bir "güvenli olmayan" moda sokmanız ve muhtemelen yaptığınız gerçeğe dikkat çekmek için programınıza "güvensiz" kelimesini koymanız gerekir. kuralları çiğneyen tehlikeli bir şey.

Daha fazla okumak için:

  • Ya C # referansların döndürülmesine izin verdiyse? Tesadüfen bugünkü blog yazısının konusu bu:

    https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/

  • Belleği yönetmek için neden yığınları kullanıyoruz? C # 'daki değer türleri her zaman yığında mı depolanır? Sanal bellek nasıl çalışır? Ve C # bellek yöneticisinin nasıl çalıştığına dair daha birçok konu. Bu makalelerin çoğu aynı zamanda C ++ programcıları için de geçerlidir:

    https://ericlippert.com/tag/memory-management/

278 Rena Jun 23 2011 at 12:43

Burada ne yapıyoruz hemen okumaya ve bu belleğe yazıyor için kullanılan bir adresi a. Artık dışarıda olduğunuza göre foo, bu sadece rastgele bir hafıza alanına işaret ediyor. Öyle oluyor ki, örneğinizde o hafıza alanı var ve şu anda onu başka hiçbir şey kullanmıyor. Kullanmaya devam ederek hiçbir şeyi bozmazsınız ve henüz başka hiçbir şey onun üzerine yazmamıştır. Bu nedenle, 5hala orada. Gerçek bir programda, bu bellek hemen yeniden kullanılır ve bunu yaparak bir şeyi kırarsınız (belirtiler çok daha sonraya kadar görünmeyebilir!)

Geri döndüğünüzde foo, işletim sistemine artık bu belleği kullanmadığınızı ve başka bir şeye yeniden atanabileceğini söylersiniz. Şanslıysanız ve asla yeniden atanmazsa ve işletim sistemi sizi tekrar kullanırken yakalamazsa, o zaman yalandan kurtulursunuz. Muhtemelen, bu adresle sonuçlanan başka her şeyin üzerine yazacaksın.

Şimdi, derleyicinin neden şikayet etmediğini merak ediyorsanız, bunun nedeni muhtemelen foooptimizasyonla ortadan kaldırılmış olmasıdır. Genellikle bu tür şeyler hakkında sizi uyaracaktır. C, ne yaptığınızı bildiğinizi varsayar ve teknik olarak burada kapsamı ihlal etmemişsinizdir ( adışında kendisine referans yoktur foo), yalnızca bellek erişim kuralları, bir hata yerine yalnızca bir uyarıyı tetikler.

Kısacası: bu genellikle işe yaramaz, ancak bazen şans eseri olur.

152 msw May 19 2010 at 09:33

Çünkü depolama alanı henüz sıkıştırılmamıştı. Bu davranışa güvenmeyin.

84 Michael Jun 25 2011 at 21:19

Tüm cevaplara küçük bir ekleme:

eğer böyle bir şey yaparsan:

#include<stdio.h>
#include <stdlib.h>
int * foo(){
    int a = 5;
    return &a;
}
void boo(){
    int a = 7;

}
int main(){
    int * p = foo();
    boo();
    printf("%d\n",*p);
}

çıktı muhtemelen şöyle olacaktır: 7

Bunun nedeni, foo () 'dan döndükten sonra yığının serbest bırakılması ve boo () tarafından yeniden kullanılmasıdır. Yürütülebilir dosyayı sökerseniz, açıkça göreceksiniz.

72 CharlesBrunet Jun 22 2011 at 21:15

C ++, sen yapabilirsiniz herhangi adrese erişim, ancak bu demek değildir gerekir . Erişmekte olduğunuz adres artık geçerli değil. Bu işleri foo döndükten sonra başka bir şey bellek şifreli çünkü ancak birçok koşulda kilitlenmesine. Programınızı Valgrind ile analiz etmeyi veya hatta optimize edilmiş olarak derlemeyi deneyin ve görün ...

67 KerrekSB Jun 22 2011 at 21:15

Geçersiz belleğe erişerek asla bir C ++ istisnası atmazsınız. Sadece gelişigüzel bir hafıza konumuna gönderme yapma genel fikrine bir örnek veriyorsunuz. Ben de aynısını şöyle yapabilirim:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Burada 123456'yı bir çiftin adresi olarak görüyorum ve ona yazıyorum. Herhangi bir sayıda şey olabilir:

  1. qaslında bir çiftin gerçekten geçerli bir adresi olabilir, ör double p; q = &p;.
  2. q ayrılmış bellek içinde bir yere işaret edebilir ve ben sadece orada 8 baytın üzerine yazıyorum.
  3. q ayrılan belleğin dışındaki noktalar ve işletim sisteminin bellek yöneticisi programıma bir bölümleme hatası sinyali göndererek çalışma zamanının onu sonlandırmasına neden oluyor.
  4. Piyangoyu kazandınız.

Bunu kurma şeklinize göre, döndürülen adresin geçerli bir bellek alanını işaret etmesi biraz daha mantıklıdır, çünkü muhtemelen yığının biraz daha aşağısında olacaktır, ancak yine de bir yerde erişemeyeceğiniz geçersiz bir konumdur. deterministik moda.

Normal program yürütme sırasında kimse sizin için bellek adreslerinin anlamsal geçerliliğini otomatik olarak kontrol etmeyecektir. Ancak, böyle bir bellek hata ayıklayıcısı valgrindbunu mutlu bir şekilde yapacaktır, bu nedenle programınızı onun üzerinden çalıştırmalı ve hatalara tanık olmalısınız.

29 gastush Jun 22 2011 at 21:12

Programınızı optimize edici etkin olarak mı derlediniz? foo()Fonksiyon oldukça basit hem de satır içi veya çıkan kodu değiştirilmiş olabilir.

Ancak ortaya çıkan davranışın tanımsız olduğu konusunda Mark B'ye katılıyorum.

23 ChangPeng Jun 23 2011 at 11:45

Sorunun kapsamla ilgisi yok . Gösterdiğiniz kodda, işlev işlevdeki mainisimleri görmez foo, bu nedenle afoo'da bu isim dışında doğrudan erişemezsiniz foo.

Yaşadığınız sorun, programın neden yasadışı belleğe başvururken bir hata sinyali vermemesidir. Bunun nedeni, C ++ standartlarının yasa dışı bellek ile yasal bellek arasında çok net bir sınır belirlememesidir. Dışarı çıkan yığındaki bir şeye başvurmak bazen hataya neden olurken bazen de neden olmaz. Değişir. Bu davranışa güvenmeyin. Program yaptığınızda her zaman hatayla sonuçlanacağını varsayın, ancak hata ayıkladığınızda asla hata sinyali vermeyeceğini varsayın.

18 BrianR.Bondy May 19 2010 at 09:33

Sadece bir hafıza adresini döndürüyorsunuz, buna izin verilir, ancak muhtemelen bir hata.

Evet, bu hafıza adresinden referans almayı kaldırmaya çalışırsanız, tanımlanmamış davranışınız olacaktır.

int * ref () {

 int tmp = 100;
 return &tmp;
}

int main () {

 int * a = ref();
 //Up until this point there is defined results
 //You can even print the address returned
 // but yes probably a bug

 cout << *a << endl;//Undefined results
}
18 KerrekSB Jun 25 2011 at 04:57

Bu, iki gün önce burada tartışılan klasik tanımlanmamış davranış - siteyi biraz araştırın. Özetle, şanslıydınız, ancak her şey olabilirdi ve kodunuz belleğe geçersiz erişim sağlıyor.

18 AHelps Jun 25 2011 at 05:04

Alex'in belirttiği gibi bu davranış tanımsızdır - aslında çoğu derleyici bunu yapmaya karşı uyaracaktır, çünkü bu, çökmeleri almanın kolay bir yoludur.

Sen ürkütücü davranış türünün örneği için muhtemel olsun, bu örnek deneyin:

int *a()
{
   int x = 5;
   return &x;
}

void b( int *c )
{
   int y = 29;
   *c = 123;
   cout << "y=" << y << endl;
}

int main()
{
   b( a() );
   return 0;
}

Bu, "y = 123" yazdırır, ancak sonuçlarınız değişebilir (gerçekten!). İşaretçiniz diğer, ilgisiz yerel değişkenleri bozuyor.

18 sam Aug 17 2015 at 13:30

Tüm uyarılara dikkat edin. Sadece hataları çözmeyin.
GCC bu Uyarıyı gösteriyor

uyarı: yerel değişken 'a' adresi döndürüldü

Bu, C ++ 'nın gücüdür. Hafızayı önemsemelisin. İle -Werrorbayrak, bu uyarı bir hata becames ve şimdi bunu hata ayıklama gerekiyor.

17 AdrianGrigore Jun 23 2011 at 22:31

Çalışır çünkü yığın, oraya yerleştirildiğinden beri (henüz) değiştirilmemiştir. aTekrar erişmeden önce birkaç başka işlevi (diğer işlevleri de çağıran) çağırın ve muhtemelen artık o kadar şanslı olmayacaksınız ... ;-)

16 AlexanderGessler Jun 25 2011 at 04:57

Aslında tanımlanmamış davranışa başvurdunuz.

Geçici bir eserin adresini döndürmek, ancak bir işlevin sonunda geçiciler yok edildiğinden, bunlara erişmenin sonuçları tanımlanmayacaktır.

Yani bir zamanlar olduğu ahafıza konumunu değiştirmediniz a. Bu fark, çökme ile çökmeme arasındaki farka çok benzer.

14 larsmoa Jun 22 2011 at 21:18

"O adres ile hafıza bloğunun değerini çıktısını gibi tipik derleyici uygulamalarında, kod aklınıza gelebilecek eskiden bir tarafından işgal". Ayrıca, bir yerelliği sınırlayan bir işleve yeni bir işlev çağrısı eklerseniz int, değerinin a(veya aişaret etmek için kullanılan bellek adresinin ) değişme olasılığı yüksektir . Bunun nedeni, yığının üzerine farklı veriler içeren yeni bir çerçeve yazılacak olmasıdır.

Ancak, bu tanımlanmamış bir davranıştır ve çalışması için ona güvenmemelisiniz!

14 littleadv Jun 25 2011 at 04:57

Olabilir, çünkü akapsamının ( foofonksiyonunun) ömrü boyunca geçici olarak tahsis edilmiş bir değişkendir . fooHafızadan döndükten sonra ücretsizdir ve üzerine yazılabilir.

Yaptığınız şey tanımlanmamış davranış olarak tanımlanıyor . Sonuç tahmin edilemez.

12 Mykola Jun 24 2011 at 22:07

Doğru (?) Konsol çıktısına sahip olan şeyler, cout kullanmadan :: printf kullanırsanız önemli ölçüde değişebilir. Aşağıdaki kodda hata ayıklayıcı ile oynayabilirsiniz (x86, 32-bit, MSVisual Studio üzerinde test edilmiştir):

char* foo() 
{
  char buf[10];
  ::strcpy(buf, "TEST”);
  return buf;
}

int main() 
{
  char* s = foo();    //place breakpoint & check 's' varialbe here
  ::printf("%s\n", s); 
}
5 GhulamMoinulQuadir Jul 19 2017 at 14:07

Bir işlevden döndükten sonra, bir bellek konumunda tutulan değerler yerine tüm tanımlayıcılar yok edilir ve bir tanımlayıcı olmadan değerleri bulamayız, ancak bu konum hala önceki işlev tarafından saklanan değeri içerir.

Yani, burada fonksiyon foo()adresini dönüyor ave aadresini döndükten sonra imha edilir. Ve değiştirilen değere bu döndürülen adresten erişebilirsiniz.

Gerçek dünyadan bir örnek vereyim:

Bir adamın bir yerde parayı sakladığını ve size konumu söylediğini varsayalım. Bir süre sonra size paranın yerini söyleyen adam ölür. Ama yine de o gizli paraya erişim hakkına sahipsiniz.

4 Ayub Mar 08 2017 at 22:25

Bellek adreslerini kullanmanın 'Kirli' yolu. Bir adres (işaretçi) döndürdüğünüzde, bunun bir işlevin yerel kapsamına ait olup olmadığını bilemezsiniz. Bu sadece bir adres. Artık 'foo' işlevini çalıştırdığınıza göre, bu 'a' adresi (bellek konumu), uygulamanızın (işleminizin) (güvenli, en azından şimdilik) adreslenebilir belleğine zaten tahsis edilmiştir. 'Foo' işlevi döndürüldükten sonra, 'a' adresi 'kirli' olarak kabul edilebilir, ancak oradadır, temizlenmez veya programın diğer bölümlerindeki ifadeler tarafından değiştirilmez (en azından bu özel durumda). AC / C ++ derleyicisi sizi bu kadar 'kirli' erişimden alıkoymaz (eğer umursuyorsanız sizi uyarabilir). Adresi bir şekilde korumadığınız sürece, program örneğinizin (işleminizin) veri bölümünde bulunan herhangi bir bellek konumunu güvenle kullanabilirsiniz (güncelleyebilirsiniz).

1 Nobun May 02 2019 at 17:17

Kodunuz çok riskli. Yerel bir değişken oluşturuyorsunuz (ki bu işlev sona erdikten sonra yok edilmiş sayılır) ve bu değişkenin hafızasının adresini temizledikten sonra döndürürsünüz.

Bu, bellek adresinin geçerli olabileceği veya olmayabileceği ve kodunuzun olası bellek adresi sorunlarına (örneğin, bölümleme hatası) karşı savunmasız olacağı anlamına gelir.

Bu, çok kötü bir şey yaptığınız anlamına gelir, çünkü bir bellek adresini hiç güvenilir olmayan bir işaretçiye iletiyorsunuz.

Bunun yerine bu örneği düşünün ve test edin:

int * foo()
{
   int *x = new int;
   *x = 5;
   return x;
}

int main()
{
    int* p = foo();
    std::cout << *p << "\n"; //better to put a new-line in the output, IMO
    *p = 8;
    std::cout << *p;
    delete p;
    return 0;
}

Örneğinizin aksine, bu örnekle siz:

  • int için belleği yerel bir işleve ayırmak
  • bu hafıza adresi fonksiyonun süresi dolduğunda da geçerlidir (kimse tarafından silinmez)
  • bellek adresi güvenilirdir (bu bellek bloğu boş kabul edilmez, bu nedenle silinene kadar geçersiz kılınmayacaktır)
  • hafıza adresi kullanılmadığında silinmelidir. (programın sonundaki silme bölümüne bakın)