Saya memiliki kode berikut.
#include <iostream>
int * foo()
{
int a = 5;
return &a;
}
int main()
{
int* p = foo();
std::cout << *p;
*p = 8;
std::cout << *p;
}
Dan kodenya hanya berjalan tanpa pengecualian runtime!
Outputnya adalah 58
Bagaimana bisa? Bukankah memori variabel lokal tidak dapat diakses di luar fungsinya?
c++
memory-management
local-variables
dangling-pointer
tidak diketahui
sumber
sumber
address of local variable ‘a’ returned
; valgrind showsInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
Jawaban:
Anda menyewa kamar hotel. Anda meletakkan buku di laci atas meja samping tempat tidur dan pergi tidur. Anda memeriksa keesokan paginya, tetapi "lupa" untuk mengembalikan kunci Anda. Anda mencuri kuncinya!
Seminggu kemudian, Anda kembali ke hotel, jangan check-in, menyelinap ke kamar lama Anda dengan kunci curian Anda, dan mencari di laci. Buku Anda masih ada di sana. Mengherankan!
Bagaimana itu bisa terjadi? Bukankah isi laci kamar hotel tidak dapat diakses jika Anda belum menyewa kamar?
Nah, jelas skenario itu bisa terjadi di dunia nyata tanpa masalah. Tidak ada kekuatan misterius yang menyebabkan buku Anda menghilang ketika Anda tidak lagi berwenang berada di ruangan itu. Juga tidak ada kekuatan misterius yang mencegah Anda memasuki ruangan dengan kunci curian.
Manajemen hotel tidak diharuskan untuk menghapus buku Anda. Anda tidak membuat kontrak dengan mereka yang mengatakan bahwa jika Anda meninggalkan barang di belakang, mereka akan menghancurkannya untuk Anda. Jika Anda secara ilegal memasuki kembali kamar Anda dengan kunci yang dicuri untuk mendapatkannya kembali, staf keamanan hotel tidak diharuskan menangkap Anda menyelinap masuk. Anda tidak membuat kontrak dengan mereka yang mengatakan "jika saya mencoba menyelinap kembali ke kamar saya kamar nanti, Anda diminta untuk menghentikan saya. " Sebaliknya, Anda menandatangani kontrak dengan mereka yang mengatakan "Saya berjanji untuk tidak menyelinap kembali ke kamar saya nanti", sebuah kontrak yang Anda pecahkan .
Dalam situasi ini apa pun bisa terjadi . Buku itu bisa ada di sana - Anda beruntung. Buku orang lain bisa ada di sana dan buku Anda bisa ada di tungku hotel. Seseorang bisa berada di sana tepat ketika Anda masuk, merobek-robek buku Anda. Hotel bisa saja menghapus meja dan memesan seluruhnya dan menggantinya dengan lemari pakaian. Seluruh hotel bisa saja akan dirobohkan dan diganti dengan stadion sepak bola, dan Anda akan mati dalam ledakan saat Anda menyelinap di sekitar.
Anda tidak tahu apa yang akan terjadi; ketika Anda keluar dari hotel dan mencuri kunci untuk digunakan secara ilegal nanti, Anda menyerahkan hak untuk hidup di dunia yang dapat diprediksi dan aman karena Anda memilih untuk melanggar aturan sistem.
C ++ bukan bahasa yang aman . Dengan riang akan memungkinkan Anda untuk melanggar aturan sistem. Jika Anda mencoba melakukan sesuatu yang ilegal dan bodoh seperti kembali ke ruangan yang tidak diizinkan untuk Anda masuki dan menggeledah meja yang bahkan mungkin tidak ada lagi, C ++ tidak akan menghentikan Anda. Bahasa yang lebih aman daripada C ++ menyelesaikan masalah ini dengan membatasi kekuatan Anda - dengan memiliki kontrol yang lebih ketat terhadap kunci, misalnya.
MEMPERBARUI
Ya ampun, jawaban ini mendapat banyak perhatian. (Saya tidak yakin mengapa - saya menganggapnya sebagai analogi kecil yang "menyenangkan", tetapi apa pun.)
Saya pikir mungkin akan sedikit lebih baik untuk memperbaruinya dengan beberapa pemikiran teknis.
Kompiler dalam bisnis menghasilkan kode yang mengelola penyimpanan data yang dimanipulasi oleh program itu. Ada banyak cara berbeda untuk menghasilkan kode untuk mengelola memori, tetapi seiring waktu dua teknik dasar telah mengakar.
Yang pertama adalah memiliki semacam area penyimpanan "berumur panjang" di mana "masa pakai" setiap byte dalam penyimpanan - yaitu, periode waktu ketika dikaitkan secara sah dengan beberapa variabel program - tidak dapat dengan mudah diprediksi ke depan waktu. Kompiler menghasilkan panggilan menjadi "heap manager" yang tahu bagaimana mengalokasikan penyimpanan secara dinamis saat diperlukan dan mengklaimnya kembali saat tidak lagi diperlukan.
Metode kedua adalah memiliki area penyimpanan "berumur pendek" di mana masa pakai setiap byte dikenal. Di sini, masa hidup mengikuti pola "bersarang". Variabel yang berumur pendek paling lama akan dialokasikan sebelum variabel berumur pendek lainnya, dan akan dibebaskan terakhir. Variabel berumur pendek akan dialokasikan setelah variabel berumur panjang, dan akan dibebaskan sebelum mereka. Masa hidup dari variabel-variabel yang berumur pendek ini adalah "bersarang" dalam masa hidup variabel-variabel yang berumur lebih panjang.
Variabel lokal mengikuti pola yang terakhir; ketika suatu metode dimasukkan, variabel lokalnya menjadi hidup. Ketika metode itu memanggil metode lain, variabel lokal metode baru menjadi hidup. Mereka akan mati sebelum variabel lokal metode pertama mati. Urutan relatif dari awal dan akhir masa pakai penyimpanan yang terkait dengan variabel lokal dapat diselesaikan sebelumnya.
Untuk alasan ini, variabel lokal biasanya dihasilkan sebagai penyimpanan pada struktur data "stack", karena stack memiliki properti yang didorong oleh hal pertama yang akan menjadi hal terakhir yang muncul.
Ini seperti hotel memutuskan untuk hanya menyewakan kamar secara berurutan, dan Anda tidak dapat check-out sampai semua orang dengan nomor kamar lebih tinggi dari yang Anda periksa.
Jadi mari kita pikirkan tentang stack. Dalam banyak sistem operasi, Anda mendapatkan satu tumpukan per utas dan tumpukan dialokasikan untuk ukuran tetap tertentu. Ketika Anda memanggil suatu metode, barang-barang didorong ke tumpukan. Jika Anda kemudian meneruskan sebuah pointer ke stack kembali dari metode Anda, seperti yang dilakukan poster asli di sini, itu hanyalah sebuah pointer ke tengah beberapa blok memori jutaan byte yang sepenuhnya valid. Dalam analogi kami, Anda check out dari hotel; ketika Anda melakukannya, Anda baru saja keluar dari kamar yang ditempati nomor tertinggi. Jika tidak ada orang lain yang check-in setelah Anda, dan Anda kembali ke kamar Anda secara ilegal, semua barang Anda dijamin masih ada di hotel ini .
Kami menggunakan tumpukan untuk toko sementara karena sangat murah dan mudah. Implementasi C ++ tidak diperlukan untuk menggunakan tumpukan untuk penyimpanan penduduk setempat; bisa menggunakan heap. Tidak, karena itu akan membuat program lebih lambat.
Implementasi C ++ tidak diperlukan untuk membiarkan sampah yang Anda tinggalkan di tumpukan tidak tersentuh sehingga Anda dapat kembali lagi nanti secara ilegal; sangat legal bagi kompiler untuk membuat kode yang mengembalikan semua nol pada "ruang" yang baru saja Anda tinggalkan. Bukan karena lagi, itu akan mahal.
Implementasi C ++ tidak diperlukan untuk memastikan bahwa ketika tumpukan menyusut secara logis, alamat yang dulu valid masih dipetakan ke dalam memori. Implementasinya diizinkan untuk memberi tahu sistem operasi "kami selesai menggunakan halaman stack ini sekarang. Sampai saya katakan sebaliknya, keluarkan pengecualian yang menghancurkan proses jika ada yang menyentuh halaman stack yang sebelumnya valid". Sekali lagi, implementasi tidak benar-benar melakukan itu karena lambat dan tidak perlu.
Sebaliknya, implementasi membuat Anda melakukan kesalahan dan lolos begitu saja. Sebagian besar waktu. Hingga suatu hari terjadi sesuatu yang sangat buruk dan prosesnya meledak.
Ini bermasalah. Ada banyak aturan dan sangat mudah untuk melanggarnya secara tidak sengaja. Saya sudah pasti berkali-kali. Dan lebih buruk lagi, masalah sering hanya muncul ketika memori terdeteksi menjadi milyaran milidetik rusak setelah korupsi terjadi, ketika sangat sulit untuk mencari tahu siapa yang mengacaukannya.
Lebih banyak bahasa yang aman-memori menyelesaikan masalah ini dengan membatasi daya Anda. Dalam "normal" C # tidak ada cara untuk mengambil alamat lokal dan mengembalikannya atau menyimpannya untuk nanti. Anda dapat mengambil alamat lokal, tetapi bahasa tersebut dirancang dengan cerdik sehingga tidak mungkin untuk menggunakannya setelah masa pakai lokal berakhir. Untuk mengambil alamat lokal dan mengembalikannya, Anda harus meletakkan kompiler dalam mode "tidak aman" khusus, dan meletakkan kata "tidak aman" di program Anda, untuk menarik perhatian pada fakta bahwa Anda mungkin melakukan sesuatu yang berbahaya yang bisa melanggar aturan.
Untuk bacaan lebih lanjut:
Bagaimana jika C # mengizinkan referensi yang dikembalikan? Secara kebetulan itulah yang menjadi topik posting blog hari ini:
https://ericlippert.com/2011/06/23/ref-returns-and-ref-locals/
Mengapa kita menggunakan tumpukan untuk mengelola memori? Apakah tipe nilai dalam C # selalu disimpan di stack? Bagaimana cara kerja memori virtual? Dan masih banyak lagi topik tentang cara kerja manajer memori C #. Banyak dari artikel ini juga berhubungan dengan programmer C ++:
https://ericlippert.com/tag/memory-management/
sumber
Apa yang Anda lakukan di sini hanyalah membaca dan menulis ke memori yang dulunya alamat
a
. Sekarang Anda berada di luarfoo
, itu hanya pointer ke beberapa area memori acak. Kebetulan di contoh Anda, area memori memang ada dan tidak ada yang menggunakannya saat ini. Anda tidak merusak apa pun dengan terus menggunakannya, dan belum ada yang menimpanya. Karena itu,5
masih ada di sana. Dalam program nyata, memori itu akan segera digunakan kembali dan Anda akan memecahkan sesuatu dengan melakukan ini (meskipun gejalanya mungkin tidak muncul sampai nanti!)Ketika Anda kembali dari
foo
, Anda memberi tahu OS bahwa Anda tidak lagi menggunakan memori itu dan dapat dipindahkan ke sesuatu yang lain. Jika Anda beruntung dan tidak pernah dipindahkan, dan OS tidak menangkap Anda menggunakannya lagi, maka Anda akan lolos dari kebohongan. Kemungkinannya adalah Anda akhirnya akan menulis apa pun yang berakhir dengan alamat itu.Sekarang jika Anda bertanya-tanya mengapa kompiler tidak mengeluh, itu mungkin karena
foo
dihilangkan dengan optimasi. Biasanya akan memperingatkan Anda tentang hal semacam ini. C menganggap Anda tahu apa yang Anda lakukan, dan secara teknis Anda belum melanggar cakupan di sini (tidak ada referensi untuka
dirinya sendiri di luarfoo
), hanya aturan akses memori, yang hanya memicu peringatan daripada kesalahan.Singkatnya: ini biasanya tidak akan berhasil, tetapi kadang-kadang akan terjadi secara kebetulan.
sumber
Karena ruang penyimpanan belum diinjak. Jangan mengandalkan perilaku itu.
sumber
Sedikit tambahan untuk semua jawaban:
jika Anda melakukan sesuatu seperti itu:
outputnya mungkin adalah: 7
Itu karena setelah kembali dari foo () tumpukan dibebaskan dan kemudian digunakan kembali oleh boo (). Jika Anda deassemble executable Anda akan melihatnya dengan jelas.
sumber
boo
menggunakan kembalifoo
tumpukan? tidak fungsi tumpukan terpisah satu sama lain, juga saya mendapatkan sampah menjalankan kode ini di Visual Studio 2015foo()
, ada, lalu turun keboo()
.Foo()
danBoo()
keduanya masuk dengan stack pointer di lokasi yang sama. Namun ini bukan, perilaku yang harus diandalkan. 'Barang' lainnya (seperti interupsi, atau OS) dapat menggunakan tumpukan antara panggilanboo()
danfoo()
, mengubah isinya ...Di C ++, Anda dapat mengakses alamat apa pun, tetapi itu tidak berarti Anda harus melakukannya . Alamat yang Anda akses tidak lagi valid. Ini bekerja karena tidak ada lagi yang mengacak memori setelah foo kembali, tetapi bisa crash dalam banyak keadaan. Cobalah menganalisis program Anda dengan Valgrind , atau bahkan hanya mengompilasinya dioptimalkan, dan lihat ...
sumber
Anda tidak pernah melempar pengecualian C ++ dengan mengakses memori yang tidak valid. Anda hanya memberikan contoh gagasan umum tentang referensi lokasi memori yang berubah-ubah. Saya bisa melakukan hal yang sama seperti ini:
Di sini saya hanya memperlakukan 123456 sebagai alamat ganda dan menulis untuk itu. Sejumlah hal dapat terjadi:
q
mungkin sebenarnya benar-benar menjadi alamat ganda yang valid, misalnyadouble p; q = &p;
.q
mungkin menunjuk suatu tempat di dalam memori yang dialokasikan dan saya hanya menimpa 8 byte di sana.q
menunjuk ke luar memori yang dialokasikan dan manajer memori sistem operasi mengirimkan sinyal kesalahan segmentasi ke program saya, menyebabkan runtime menghentikannya.Cara Anda mengaturnya sedikit lebih masuk akal bahwa alamat yang dikembalikan menunjuk ke area memori yang valid, karena mungkin hanya akan sedikit lebih jauh ke bawah tumpukan, tetapi masih merupakan lokasi yang tidak valid yang tidak dapat Anda akses di mode deterministik.
Tidak ada yang akan secara otomatis memeriksa validitas semantik dari alamat memori seperti itu untuk Anda selama eksekusi program normal. Namun, debugger memori seperti
valgrind
akan dengan senang hati melakukan ini, jadi Anda harus menjalankan program Anda melalui itu dan menyaksikan kesalahan.sumber
4) I win the lottery
Apakah Anda mengkompilasi program Anda dengan pengoptimal diaktifkan? The
foo()
Fungsi ini cukup sederhana dan mungkin telah inline atau diganti dalam kode yang dihasilkan.Tetapi saya setuju dengan Mark B bahwa perilaku yang dihasilkan tidak terdefinisi.
sumber
5
akan diubah ...Masalah Anda tidak ada hubungannya dengan ruang lingkup . Dalam kode yang Anda tunjukkan, fungsi
main
tidak melihat nama-nama dalam fungsifoo
, jadi Anda tidak dapat mengaksesa
di foo langsung dengan nama ini di luarfoo
.Masalah yang Anda alami adalah mengapa program tidak memberi sinyal kesalahan ketika merujuk memori ilegal. Ini karena standar C ++ tidak menentukan batas yang sangat jelas antara memori ilegal dan memori legal. Merujuk sesuatu di tumpukan keluar terkadang menyebabkan kesalahan dan terkadang tidak. Tergantung. Jangan mengandalkan perilaku ini. Asumsikan itu akan selalu menghasilkan kesalahan saat Anda memprogram, tetapi menganggap itu tidak akan pernah menandakan kesalahan saat Anda debug.
sumber
Anda baru saja mengembalikan alamat memori, diizinkan tetapi mungkin kesalahan.
Ya, jika Anda mencoba untuk meringkas alamat memori itu, Anda akan memiliki perilaku yang tidak jelas.
sumber
cout
.*a
menunjuk ke memori yang tidak terisi (dibebaskan). Bahkan jika Anda tidak menolaknya, itu masih berbahaya (dan kemungkinan palsu).Itu perilaku klasik yang tidak terdefinisi yang telah dibahas di sini dua hari lalu - cari di sekitar situs untuk sedikit. Singkatnya, Anda beruntung, tetapi apa pun bisa terjadi dan kode Anda membuat akses ke memori tidak valid.
sumber
Perilaku ini tidak terdefinisi, seperti yang ditunjukkan oleh Alex - pada kenyataannya, kebanyakan kompiler akan memperingatkan untuk tidak melakukan ini, karena ini adalah cara mudah untuk mendapatkan crash.
Untuk contoh jenis perilaku seram yang mungkin Anda dapatkan, coba sampel ini:
Ini mencetak "y = 123", tetapi hasil Anda mungkin bervariasi (sungguh!). Pointer Anda menghancurkan variabel lokal lain yang tidak terkait.
sumber
Perhatikan semua peringatan. Jangan hanya menyelesaikan kesalahan.
GCC menunjukkan Peringatan ini
Ini adalah kekuatan C ++. Anda harus peduli dengan ingatan. Dengan
-Werror
bendera, peringatan ini menjadi kesalahan dan sekarang Anda harus men-debug-nya.sumber
Ini bekerja karena tumpukan belum diubah (belum) sejak diletakkan di sana. Panggil beberapa fungsi lain (yang juga memanggil fungsi lain) sebelum mengakses
a
lagi dan Anda mungkin tidak akan seberuntung itu lagi ... ;-)sumber
Anda benar-benar memanggil perilaku yang tidak terdefinisi.
Mengembalikan alamat karya sementara, tetapi karena temporari dihancurkan pada akhir fungsi, hasil mengaksesnya tidak akan ditentukan.
Jadi Anda tidak memodifikasi
a
melainkan lokasi memori di manaa
dulu. Perbedaan ini sangat mirip dengan perbedaan antara menabrak dan tidak menabrak.sumber
Dalam implementasi kompiler tipikal, Anda dapat menganggap kode sebagai "cetak nilai blok memori dengan alamat yang dulu ditempati oleh". Juga, jika Anda menambahkan pemanggilan fungsi baru ke fungsi yang membatasi lokal
int
, kemungkinan besar nilaia
(atau alamat memori yanga
digunakan untuk menunjuk) berubah. Ini terjadi karena tumpukan akan ditimpa dengan bingkai baru yang berisi data berbeda.Namun, ini adalah perilaku yang tidak terdefinisi dan Anda tidak harus bergantung padanya untuk bekerja!
sumber
a
, pointer memegang alamata
. Meskipun Standar tidak mensyaratkan bahwa implementasi mendefinisikan perilaku alamat setelah masa hidup target mereka telah berakhir, Standar juga mengakui bahwa pada beberapa platform UB diproses dengan cara yang terdokumentasi dengan karakteristik lingkungan. Sementara alamat variabel lokal umumnya tidak akan banyak berguna setelah keluar dari ruang lingkup, beberapa jenis alamat lain mungkin masih bermakna setelah masa pakai target masing-masing.realloc
dibandingkan dengan nilai kembali, juga tidak memungkinkan pointer ke alamat dalam blok lama disesuaikan untuk menunjuk ke yang baru, beberapa implementasi melakukannya , dan kode yang mengeksploitasi fitur semacam itu mungkin lebih efisien daripada kode yang harus menghindari tindakan apa pun - bahkan perbandingan - yang melibatkan pointer ke alokasi yang diberikanrealloc
.Itu bisa, karena
a
merupakan variabel yang dialokasikan sementara untuk masa lingkupnya (foo
fungsi). Setelah kamu kembali darifoo
memori gratis dan dapat ditimpa.Apa yang Anda lakukan digambarkan sebagai perilaku yang tidak terdefinisi . Hasilnya tidak dapat diprediksi.
sumber
Hal-hal dengan output konsol yang benar (?) Dapat berubah secara dramatis jika Anda menggunakan :: printf tetapi tidak cout. Anda dapat bermain-main dengan debugger dalam kode di bawah ini (diuji pada x86, 32-bit, MSVisual Studio):
sumber
Setelah kembali dari suatu fungsi, semua pengidentifikasi dihancurkan alih-alih disimpan nilai di lokasi memori dan kami tidak dapat menemukan nilai-nilai tanpa memiliki pengenal. Tetapi lokasi itu masih berisi nilai yang disimpan oleh fungsi sebelumnya.
Jadi, di sini fungsi
foo()
mengembalikan alamata
dana
dihancurkan setelah mengembalikan alamatnya. Dan Anda dapat mengakses nilai yang dimodifikasi melalui alamat yang dikembalikan.Biarkan saya mengambil contoh dunia nyata:
Misalkan seorang pria menyembunyikan uang di suatu lokasi dan memberi tahu Anda lokasi itu. Setelah beberapa waktu, pria yang memberi tahu Anda lokasi uang mati. Tetapi Anda masih memiliki akses uang tersembunyi itu.
sumber
Ini cara 'Kotor' menggunakan alamat memori. Ketika Anda mengembalikan alamat (penunjuk) Anda tidak tahu apakah itu termasuk dalam cakupan fungsi lokal. Itu hanya alamat. Sekarang setelah Anda memanggil fungsi 'foo', alamat itu (lokasi memori) dari 'a' telah dialokasikan di sana di (aman, setidaknya untuk saat ini) memori yang dapat dialamatkan dari aplikasi (proses) Anda. Setelah fungsi 'foo' kembali, alamat 'a' dapat dianggap 'kotor' tetapi ada di sana, tidak dibersihkan, atau diganggu / dimodifikasi oleh ekspresi di bagian lain dari program (dalam kasus khusus ini setidaknya). Kompiler AC / C ++ tidak menghentikan Anda dari akses 'kotor' tersebut (mungkin memperingatkan Anda, jika Anda peduli).
sumber
Kode Anda sangat berisiko. Anda sedang membuat variabel lokal (yang dianggap hancur setelah fungsi berakhir) dan Anda mengembalikan alamat memori variabel itu setelah itu dihancurkan.
Itu berarti alamat memori bisa valid atau tidak, dan kode Anda akan rentan terhadap kemungkinan masalah alamat memori (misalnya kesalahan segmentasi).
Ini berarti bahwa Anda melakukan hal yang sangat buruk, karena Anda memberikan alamat memori ke sebuah penunjuk yang sama sekali tidak dapat dipercaya.
Pertimbangkan contoh ini, sebagai gantinya, dan uji:
Berbeda dengan contoh Anda, dengan contoh ini Anda adalah:
sumber
new
.new
. Anda mengajar mereka untuk menggunakannew
. Tetapi Anda tidak harus menggunakannyanew
.new
pada tahun 2019 (kecuali Anda sedang menulis kode perpustakaan) dan jangan mengajari pendatang baru untuk melakukannya juga! Bersulang.