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


1029

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 sadece çalışma zamanı istisnaları olmadan çalışıyor!

Çıktı 58

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


14
bu olduğu gibi derlenmeyecektir; biçimlendirilmemiş işi düzeltirseniz, gcc yine de uyarır address of local variable ‘a’ returned; valgrind gösterileriInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
sehe

76
@Serge: Gençliğimde bir zamanlar Netware işletim sisteminde, yığın işaretçisi etrafında akıllıca işletim sistemi tarafından onaylanmayan bir şekilde hareket etmeyi içeren bazı zor sıfır halka kodları üzerinde çalıştım. Ne zaman hata yaptığımı bilirdim çünkü yığın genellikle ekran belleğiyle çakışır ve baytların doğrudan ekrana yazılmasını izleyebilirim. Bugünlerde bu tür şeylerden kurtulamazsın.
Eric Lippert

23
lol. Sorunun nerede olduğunu anlamadan önce soruyu ve bazı cevapları okumam gerekiyordu. Bu aslında değişkenin erişim kapsamıyla ilgili bir soru mu? Fonksiyonunuzun dışında 'a' bile kullanmazsınız. Ve hepsi bu kadar. Bazı bellek referanslarını atmak değişken kapsamdan tamamen farklı bir konudur.
erikbwork

10
Dupe cevabı dupe sorusu anlamına gelmez. Burada önerilen insanların dupe sorularının çoğu, aynı altta yatan semptomlara atıfta bulunan tamamen farklı sorulardır ... ancak soru soran, bunu bilmeleri için açık bir yol biliyor. Eski bir kubbeyi kapattım ve çok iyi bir cevabı olduğu için açık kalması gereken bu soruya birleştirdim.
Joel Spolsky

16
@Joel: Buradaki cevap iyi ise, bunun bir dupe olduğu daha eski sorularla birleştirilmeli , tersi değil. Ve bu soru aslında burada ve sonra önerilen diğer soruların bir çiftidir (önerilenlerin bazıları diğerlerinden daha iyi olsa da). Eric'in cevabının iyi olduğunu düşünüyorum. (Aslında, bu soruyu daha eski soruları kurtarmak için cevapları daha eski sorulardan birine birleştirmek için işaretledim.)
sbi

Yanıtlar:


4801

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

Bir otel odası kiralıyorsunuz. Komodinin üst çekmecesine bir kitap koyup uyuyorsun. Ertesi sabah kontrol, ama anahtarınızı geri vermek "unutmak". Anahtarı çalıyorsun!

Bir hafta sonra, otele geri dönersiniz, check-in yapmaz, çalıntı anahtarınızla eski odanıza girer ve çekmeceye bakarsınız. Kitabınız hala orada. Şaşırtıcı!

Nasıl olabilir? Eğer oda kiralamadıysanız bir otel odası çekmecesinin içeriğine erişilemiyor mu?

Tabii ki, bu senaryo gerçek dünyada sorun olabilir. Artık odada bulunma yetkiniz olmadığında kitabınızın kaybolmasına neden olan gizemli bir güç yoktur. Çalınan bir anahtarla bir odaya girmenizi engelleyen gizemli bir güç de yoktur.

Otel yönetimi kitabınızı kaldırmak için gerekli değildir . Onlarla bir anlaşma yapmadınız, eğer bir şeyleri geride bırakırsanız, sizin için parçalayacaklarını söylediler. Yasadışı bir şekilde odanıza geri dönmek için çalınan bir anahtarla tekrar girerseniz, otel güvenlik personelinin sizi gizlice yakalaması gerekmez . Onlarla bir sözleşme yapmadınız. sonradan beni durdurman gerekiyor. " Bunun yerine, bir sözleşme "Daha sonra odama gizlice geri dair söz" dedi onlarla bir sözleşme imzaladı sen kırdı .

Bu durumda her şey olabilir . Kitap orada olabilir - şanslısın. Başkasının kitabı orada olabilir ve sizinki otelin fırında olabilir. İçeri girdiğinizde birisi orada olabilir, kitabınızı parçalara ayırabilir. Otel tablo ve kitap tamamen kaldırılmış ve bir gardırop ile değiştirmiş olabilir. Tüm otel hemen parçalanmış ve yerine bir futbol stadyumu olabilir ve sinsice dolaşırken bir patlamada ölecek.

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 . Neşeyle sistemin kurallarını çiğnemenize izin verecektir. Yasadışı ve aptalca bir şey yapmaya çalıştığınız bir odaya geri dönme ve artık orada bulunmayan bir masadan dolaşmak gibi, C ++ sizi durdurmayacak. 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

Kutsal iyilik, bu cevap çok dikkat çekiyor. (Neden olduğundan emin değilim - bunu sadece "eğlenceli" küçük bir benzetme olarak gördüm, ama her neyse.)

Bunu biraz daha teknik düşüncelerle güncellemenin almanca olabileceğini düşündüm.

Derleyiciler, bu program tarafından manipüle edilen verilerin depolanmasını yöneten kod üretme işindedir. Belleği yönetmek için kod üretmenin birçok farklı yolu vardır, ancak zamanla iki temel teknik yerleşmiştir.

Birincisi, depolamadaki her baytın "kullanım ömrünün" - yani, bazı program değişkenleriyle geçerli olarak ilişkilendirildiği zaman periyodunun - önceden kolayca tahmin edilemediği bir çeşit "uzun ömürlü" depolama alanına sahip olmaktır. zaman. Derleyici, gerektiğinde depolamayı dinamik olarak nasıl ayıracağını ve artık gerekmediğinde geri almayı bilen bir "yığın yöneticisine" çağrı oluşturur.

İ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 “iç içe geçme” modelini izler. Bu kısa ömürlü değişkenlerin en uzun ömürlüleri, diğer kısa ömürlü değişkenlerden önce tahsis edilecek ve en son serbest bırakılacaktır. Daha kısa ömürlü değişkenler, en uzun ömürlü değişkenlerden sonra tahsis edilecek ve önlerinde serbest bırakılacaktır. Bu daha kısa ömürlü değişkenlerin ömrü, daha uzun ömürlü değişkenlerin ömrü içinde “iç içe geçmiştir”.

Yerel değişkenler ikinci paterni takip eder; bir yöntem girildiğinde, yerel değişkenleri canlanır. Bu yöntem başka bir yöntem çağırdığında, yeni yöntemin yerel değişkenleri canlanır. İlk yöntemin yerel değişkenleri ölmeden ölürler. Lokal değişkenlerle ilişkili depoların yaşam sürelerinin başlangıcı ve bitişlerinin göreceli sırası önceden çalışılabilir.

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

Otel sadece oda sırayla kiraya karar verir gibi ve oda sayısı daha yüksek olan herkese kadar kontrol edemez gibi.

Öyleyse yığını düşünelim. Birçok işletim sisteminde, iş parçacığı başına bir yığın alırsınız ve yığın belirli bir sabit boyuta ayrılır. Bir yöntemi çağırdığınızda, şeyler yığının üzerine itilir. Daha sonra orijinal poster burada olduğu gibi yığına bir işaretçi iletirseniz, bu tamamen geçerli bir milyon bayt bellek bloğunun ortasına bir işaretçi olur. Bizim benzetimizde, otelden çıkış yaparsınız; bunu yaptığınızda, sadece en yüksek numaralı odadan kontrol ettiniz. Kimse sizden sonra check-in yapar ve odanıza yasadışı olarak geri dönerseniz, tüm eşyalarınızın hala bu özel otelde olması garanti edilir .

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

Yığında bıraktığınız çöplere dokunulmadan bırakılması için bir C ++ uygulaması gerekli değildir, böylece daha sonra yasadışı olarak geri gelebilirsiniz; derleyicinin yeni boşalttığınız "odada" her şeyi sıfıra çeviren kod üretmesi son derece yasaldır. Bunu yapmaz çünkü tekrar, bu pahalı olurdu.

Yığın mantıksal olarak küçüldüğünde, eskiden geçerli olan adreslerin hala belleğe eşlendiğinden emin olmak için bir C ++ uygulaması gerekli değildir. Uygulamanın işletim sistemine "bu yığının bu sayfasını şimdi yaptık. Aksi söyleyene kadar, daha önce geçerli yığın sayfasına dokunursa işlemi yok eden bir istisna yayınla" deme izni var. Yine, uygulamalar aslında bunu yapmaz, çünkü yavaş ve gereksizdir.

Bunun yerine, uygulamalar hata yapmanıza ve ondan kurtulmanıza izin verir. Çoğu zaman. Bir güne kadar gerçekten korkunç bir şey ters gider ve süreç patlar.

Bu sorunlu. Çok fazla kural var ve onları yanlışlıkla kırmak çok kolay. Kesinlikle birçok kez var. Ve daha da kötüsü, sorun genellikle, bellek, bozulma gerçekleştikten sonra milyarlarca nanosaniye olarak tespit edildiğinde, kimin berbat ettiğini anlamanın çok zor olduğu durumlarda ortaya çıkar.

Daha fazla bellek güvenli dil bu sorunu gücünüzü kısıtlayarak çözer. "Normal" C # 'da bir yerelin adresini alıp iade etmenin ya da daha sonra saklamak için bir yol yoktur. Bir yerelin adresini alabilirsiniz, ancak dil akıllıca tasarlanmıştır, böylece yerel sonların ömründen sonra kullanmak imkansızdır. Yerel adresini alıp geri geçmek için, bir ziyaretini özel "güvenli olmayan" modunu derleyici koymak zorunda ve muhtemelen yaptığını gerçeğine çağrı dikkatine, programınızda "güvenli olmayan" kelimesini koymak kurallara aykırı olabilecek tehlikeli bir şey.

Daha fazla okuma için:


56
@muntoo: Maalesef işletim sistemi bir sanal bellek sayfasını kapatmadan veya yeniden yerleştirmeden önce bir uyarı sireni çalıyor gibi değil. Artık sahip olmadığınızda bu bellekle uğraşıyorsanız, işletim sistemi, ayrılmış bir sayfaya dokunduğunuzda tüm işlemi devralma haklarına tamamen sahiptir. Boom!
Eric Lippert

83
@Kyle: Bunu sadece güvenli oteller yapıyor. Güvensiz oteller, programlama anahtarlarında zaman kaybetmek zorunda kalmadan ölçülebilir kar kazançları elde eder.
Alexander Torstling

498
@ cyberguijarro: C ++ 'nin bellek için güvenli olmadığı basitçe bir gerçek. Hiçbir şey "dayak" değil. Örneğin, "C ++, kırılgan, tehlikeli bir bellek modelinin üzerine yığılmış, belirtilen, aşırı karmaşık özelliklerin korkunç bir karmasıdır ve her gün artık kendi akıl sağlığım için çalışmıyorum için minnettarım", bu C ++ dayak olur. Belleğin güvenli olmadığını belirtmek , orijinal posterin neden bu sorunu gördüğünü açıklıyor ; soruyu cevaplıyor, editoryalleştirmiyor.
Eric Lippert

50
Kesinlikle analoji konuşan otelde resepsiyonist anahtarı almak için oldukça mutlu olduğunu belirtmek gerekir. "Ah, bu anahtarı yanımda götürürsem sakıncası var mı?" "Devam et. Neden umurumda? Sadece burada çalışıyorum". Siz kullanmaya çalışana kadar yasa dışı olmaz.
philsquared

140
Lütfen, en azından bir gün bir kitap yazmayı düşünün. Sadece gözden geçirilmiş ve genişletilmiş blog gönderilerinden oluşan bir koleksiyon olsa bile satın alırdım ve eminim bir sürü insan olurdu. Ancak, programlama ile ilgili çeşitli konularda orijinal düşüncelerinizi içeren bir kitap harika bir okuma olacaktır. Bunun için zaman bulmanın inanılmaz zor olduğunu biliyorum, ama lütfen bir tane yazmayı düşünün.
Dyppl

276

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 bazı rastgele bellek alanlarına bir işaretçi. Öyle ki, örneğin, bellek alanı var ve şu anda başka bir şey kullanmıyor. Kullanmaya devam ederek hiçbir şeyi kırmazsınız ve başka hiçbir şeyin üzerine yazmamıştır. Bu nedenle, 5hala orada. Gerçek bir programda, bu bellek neredeyse hemen yeniden kullanılacaktı ve bunu yaparak bir şeyleri kıracaksınız (belirtiler 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. Eğer şanslıysanız ve asla yeniden atanmazsa ve işletim sistemi sizi tekrar kullanarak yakalamazsa, yalandan kurtulacaksınız. Muhtemelen bu adresle sonuçlanan her şeyi yazacaksınız.

Derleyicinin neden şikayet etmediğini merak ediyorsanız, bunun nedeni büyük olasılıkla foooptimizasyon tarafından ortadan kaldırılmış olmasıdır. Genellikle bu tür şeyler hakkında sizi uyarır. C, ne yaptığınızı bildiğinizi varsayar ve teknik olarak burada kapsamı ihlal etmediyseniz ( akendisi dışında bir referans yoktur foo), sadece bir hata yerine bir uyarıyı tetikleyen bellek erişim kuralları.

Kısacası: bu genellikle işe yaramaz, ancak bazen tesadüfen olur.


152

Çünkü depolama alanı henüz açılmadı. Bu davranışa güvenmeyin.


1
Dostum, bu bir yorum için en uzun bekleyişti, çünkü “Gerçek nedir? Belki de o otel çekmecesinde bir Gideon İnciliydi. Zaten onlara ne oldu? En azından Londra'da artık mevcut olmadıklarına dikkat edin. Eşitlikler yasası uyarınca bir dini kanallar kütüphanesine ihtiyacınız olacağını düşünüyorum.
Rob Kent

Bunu uzun zaman önce yazdığımı yemin edebilirdim, ama son zamanlarda ortaya çıktı ve cevabımın orada olmadığını buldu. Şimdi gitmem gerektiğinde yukarıdaki imalarınızı bulmalıyım>. <
msw

1
Haha. İngiltere'nin en büyük denemecilerinden biri olan ve bazı insanların Shakespeare'in oyunlarını yazdığından şüphelenen Francis Bacon, bir torbanın oğlu olan ülkeden bir dilbilgisi okulu çocuğunun bir dahi olabileceğini kabul edemezler. İngilizce sınıfı sistem böyle. İsa, 'Ben Gerçek'im' dedi. oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Rob Kent

84

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

böyle bir şey yaparsanız:

#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: 7

Çünkü foo () 'dan döndükten sonra yığın serbest bırakılır ve sonra boo () tarafından tekrar kullanılır. Yürütülebilir dosyayı birleştirirseniz, açıkça göreceksiniz.


2
Temel yığın teorisini anlamak için basit ama harika bir örnek. "İnt a = 5;" foo () içinde "statik int a = 5"; statik bir değişkenin kapsamını ve ömrünü anlamak için kullanılabilir.
kontrol

15
-1 "için muhtemelen 7 " olacaktır. Derleyici boo a kaydolabilir. Gereksiz olduğu için kaldırabilir. * P'nin 5 olmaması için iyi bir şans vardır , ancak bu muhtemelen 7 olması için özellikle iyi bir neden olduğu anlamına gelmez .
Matt

2
Buna tanımsız davranış denir!
Francis Cugler

yığını neden ve nasıl booyeniden kullanır foo? işlev yığınları birbirinden ayrılmış değil, ayrıca Visual Studio 2015'te bu kodu çalıştıran çöp
alıyorum

1
@ampawd neredeyse bir yaşında, ama hayır, "işlev yığınları" birbirinden ayrılmamış. Bir CONTEXT'in bir yığını vardır. Bu bağlam yığına main girmek için kullanır, sonra iner foo(), var olur, sonra iner boo(). Foo()ve Boo()her ikisi de yığın işaretçisiyle aynı konuma girilir. Ancak bu, güvenilmesi gereken davranış değildir. Diğer 'şeyler' (kesmeler veya işletim sistemi gibi), çağrılması boo()ve foo()içeriğini değiştirmesi arasındaki yığını kullanabilir ...
Russ Schultz

72

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 deneyin , hatta sadece optimize edilmiş derlemeyi deneyin ve görün ...


5
Muhtemelen herhangi bir adrese erişmeyi deneyebilirsiniz. Çünkü bugün işletim sistemlerinin çoğu herhangi bir programın herhangi bir adrese erişmesine izin vermeyecektir; adres alanını korumak için tonlarca güvence vardır. Bu yüzden orada başka bir LOADLIN.EXE olmayacak.
v010dya

67

Geçersiz belleğe erişerek hiçbir zaman bir C ++ istisnası atmazsınız. Sadece keyfi bir hafıza konumuna başvurma genel fikrinin bir örneğini veriyorsunuz. Ben de aynısını yapabilirdim:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Burada 123456'yı bir çifte adres olarak ele alıyorum ve ona yazıyorum. Çok sayıda şey olabilir:

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

Ayarlama şekliniz, döndürülen adresin geçerli bir bellek alanına 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 deterministik moda.

Normal program yürütme sırasında hiç kimse sizin gibi bellek adreslerinin anlamsal geçerliliğini otomatik olarak kontrol etmez. Ancak, böyle bir bellek hata ayıklayıcı valgrindbunu mutlu bir şekilde yapacak, bu yüzden programınızı çalıştırmalı ve hatalara şahit olmalısınız.


9
Şimdi bu programı çalıştırmaya devam eden bir program yazacağım, böylece4) I win the lottery
Aidiakapi

29

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

Ancak Mark B ile ortaya çıkan davranışın tanımsız olduğuna katılıyorum.


Bu benim bahisim. Doktor işlev çağrısını attı.
Erik Aronesty

9
Bu gerekli değil. Foo () işlevinden sonra yeni işlev çağrılmadığından, yerel yığın çerçevesinin işlevlerinin henüz üzerine yazılmaz. Foo () 'dan sonra başka bir işlev çağırma ekleyin ve 5değiştirilecek ...
Tomas

Programı GCC 4.8 ile çalıştırdım, cout'u printf (ve stdio dahil) ile değiştirdim. Haklı olarak "uyarı: yerel değişken 'a' dönen [-Wreturn-local-addr]" adresini uyarır. Optimizasyon olmadan 58 ve -O3 ile 08 çıktılar. Tuhaf bir şekilde değeri 0 olmasına rağmen P'nin bir adresi var. Adres olarak NULL (0) bekleniyor.
kevinf

23

Sorununuzun kapsamla ilgisi yoktur . Gösterdiğiniz kodda, işlev işlevdeki mainadları görmez foo, bu nedenle dışarıda bu adla adoğrudan foo'ya erişemezsiniz .foo

Karşılaştığınız sorun, programın neden geçersiz belleğe başvururken bir hata sinyali vermediğidir. Bunun nedeni, C ++ standartlarının yasadışı bellek ile yasal bellek arasında çok açık bir sınır belirtmemesidir. Açılan yığındaki bir şeye başvurmak bazen hataya neden olur, bazen değil. Değişir. Bu davranışa güvenmeyin. Programladığınızda her zaman hataya neden olacağını varsayın, ancak hata ayıkladığınızda hiçbir zaman hata sinyali vermeyeceğini varsayın.


IBM için Turbo C Programlama'nın eski bir kopyasından hatırlıyorum ; bu, grafik belleğini ve IBM'in metin modu video belleğinin düzenini nasıl ayrıntılı bir şekilde tanımlandığında, bir şekilde geri oynadığım. Tabii ki, kodun üzerinde çalıştığı sistem, bu adreslere yazmanın ne anlama geldiğini açıkça tanımladı, böylece diğer sistemlere taşınabilirlik konusunda endişelenmedikçe, her şey iyiydi. IIRC, geçersiz işaretçiler bu kitapta ortak bir temaydı.
CVn

@Michael Kjörling: Elbette! İnsanlar arada bir kirli işler yapmaktan hoşlanırlar;)
Chang Peng

18

Sadece bir bellek adresi döndürüyorsunuz, buna izin veriliyor ama muhtemelen bir hata.

Bu bellek adresinden vazgeçmeye ç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
}

Kabul etmiyorum: 'dan önce bir sorun var cout. *aayrılmamış (serbest bırakılmış) belleği gösterir. Derefence olmasanız bile, yine de tehlikelidir (ve muhtemelen sahte).
ereOn

@ereOn: Sorunla ne demek istediğimi daha fazla açıkladım, ancak hayır geçerli c ++ kodu açısından tehlikeli değil. Ancak, kullanıcının bir hata yapması ve kötü bir şey yapması açısından tehlikelidir. Belki örneğin, yığının nasıl büyüdüğünü görmeye çalışıyorsunuz ve sadece adres değerini önemsiyorsunuz ve asla ondan vazgeçmeyeceksiniz.
Brian R. Bondy

18

Bu, iki gün önce tartışılmamış klasik tanımsız davranış - sitede biraz arama yapın. Özetle, şanslıydınız, ama her şey olabilirdi ve kodunuz belleğe geçersiz erişim sağlıyor.


18

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

Muhtemelen alacağınız ürkütücü davranışların bir örneği için bu örneği 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" çıktısını alır, ancak sonuçlarınız değişebilir (gerçekten!). İşaretçiniz diğer ilişkili olmayan yerel değişkenleri gizliyor.


18

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

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

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


17

Çalışır, çünkü oraya atandığından beri yığın değiştirilmemiştir (henüz). aTekrar erişmeden önce birkaç başka işlevi (diğer işlevleri de çağırır) arayın ve artık çok şanslı olmayacaksınız ... ;-)


16

Aslında tanımsız davranışlar başlattınız.

Geçici bir işin adresini döndürmek, ancak bir fonksiyonun sonunda geçiciler yok edildiğinde, bunlara erişmenin sonuçları tanımsız olacaktır.

Böylece a, bir azamanlar olduğu yerde bellek konumunu değiştirmediniz . Bu fark, çökme ile çökmeme arasındaki farka çok benzer.


14

Tipik derleyici uygulamalarında, kodu " eskiden kullanılan bir adresle bellek bloğunun değerini yazdır" olarak düşünebilirsiniz . Ayrıca, yerel bir işlevi tutan bir işleve yeni bir işlev çağırma eklerseniz int, değerinin a(veya daha aönce işaret ettiği bellek adresinin ) değişmesi iyi bir olasılıktır . Bunun nedeni, yığının üzerine farklı veriler içeren yeni bir çerçeve yazılmasıdır.

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


3
"bellek bloğunun değerini eskiden kullanılan bir adresle yazdır " tam olarak doğru değildir. Bu, kodunun iyi tanımlanmış bir anlamı olduğu gibi ses çıkarır, ki durum böyle değildir. Yine de, çoğu derleyicinin bunu nasıl uygulayacağı konusunda haklısınız.
Brennan Vincent

@BrennanVincent: Depolama alanı işgal edilirken, aişaretçi adresini tuttu a. Standart, uygulamaların adreslerinin hedeflerinin ömrü bittikten sonra davranışlarını tanımlamasını gerektirmese de, bazı platformlarda UB'nin ortamın belgelenmiş bir şekilde işlendiğini de kabul eder. Yerel bir değişkenin adresi, kapsam dışına çıktıktan sonra genellikle çok fazla kullanılmayacak olsa da, diğer bazı adres türleri, ilgili hedeflerinin ömrü boyunca yine de anlamlı olabilir.
supercat

@BrennanVincent: Örneğin, Standart, uygulamaların geçirilen bir işaretçinin reallocdönüş değeriyle karşılaştırılmasına izin vermesini veya eski blok içindeki adreslerin yeni bir adrese ayarlanmasına izin vermesini gerektirmeyebilir, ancak bazı uygulamalar bunu yapar ve böyle bir özellikten yararlanan kod, verilen tahsise işaretçiler içeren herhangi bir eylemden (hatta karşılaştırmalar) kaçınmak zorunda olan koddan daha verimli olabilir realloc.
supercat

14

Çünkü akapsamı ( fooişlevi) ömrü boyunca geçici olarak tahsis edilen bir değişkendir . fooHafızadan döndükten sonra ücretsizdir ve üzerine yazılabilir.

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


12

:: printf kullanırsanız ancak cout kullanmazsanız, doğru (?) Konsol çıkışına sahip şeyler önemli ölçüde değişebilir. Aşağıdaki kod içinde hata ayıklayıcı ile oynayabilirsiniz (x86, 32 bit, MSVisual Studio'da 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

Bir işlevden döndükten sonra, bellek konumunda tutulan değerler yerine tüm tanımlayıcılar yok edilir ve 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 adres üzerinden erişebilirsiniz.

Gerçek bir dünya örneği alalım:

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


4

Bellek adreslerini kullanmanın 'Kirli' yolu. Bir adres (işaretçi) döndürdüğünüzde, adresin bir işlevin yerel kapsamına ait olup olmadığını bilmezsiniz. Bu sadece bir adres. Artık 'foo' işlevini başlattığınıza göre, 'a' nın bu adresi (bellek konumu) zaten uygulamanızın (işlem) adreslenebilir belleğinde (güvenli olarak) en azından orada tahsis edilmiştir. 'Foo' işlevi döndükten sonra, 'a' adresi 'kirli' olarak kabul edilebilir, ancak programın diğer bölümündeki ifadeler tarafından temizlenmez, rahatsız edilmez / değiştirilmez (en azından bu özel durumda). AC / C ++ derleyicisi böyle 'kirli' erişimden sizi durdurmaz (eğer ilgilenirseniz de sizi uyarabilir).


1

Kodunuz çok riskli. Yerel bir değişken oluşturuyorsunuz (işlev sona erdikten sonra yok edilen sayılır) ve bu değişkenin belleğinin adresini destoyed edildikten sonra döndürürsünüz.

Bu, bellek adresinin geçerli olup 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 geçirdiğiniz için.

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:

  • int için yerel bir işleve bellek ayırma
  • bu bellek 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ınmaz)
  • kullanılmadığında bellek adresi silinmelidir. (programın sonundaki silme bölümüne bakın)

Mevcut cevapların kapsamına girmeyen bir şey eklediniz mi? Ve lütfen ham işaretçiler kullanmayın new.
Yörüngedeki Hafiflik Yarışları

1
Asker ham işaretçiler kullandı. Güvenilmez işaretçi ve güvenilir işaretçi arasındaki farkı görmesine izin vermek için yaptığı örneği tam olarak yansıtan bir örnek yaptım. Aslında benimkine benzer başka bir cevap var, ancak strcpy wich, IMHO kullanıyor, acemi bir kodlayıcı için yeni kullanan örneğime göre daha az açık olabilir.
Nobun

Kullanmadılar new. Onlara kullanmayı öğretiyorsun new. Ama kullanmamalısın new.
Yörüngedeki Hafiflik Yarışları

Peki sizce bir fonksiyonda yok edilen bir yerel değişkene bir adres vermek aslında bellek tahsis etmekten daha mı iyidir? Bu anlamlı değil. E bellek yerleştirme kavramını anlamak önemli, imho, özellikle işaretçiler hakkında soruyorsanız (askerler yeni değil, kullanılmış işaretçiler).
Nobun

Bunu ne zaman dedim? Hayır, başvurulan kaynağın sahipliğini doğru bir şekilde belirtmek için akıllı işaretçiler kullanmak daha iyidir. new2019'da kullanmayın (kütüphane kodu yazmıyorsanız) ve yeni gelenlere de bunu yapmayı öğretmeyin! Şerefe.
Yörüngedeki Hafiflik Yarışları
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.