Bisakah memori variabel lokal diakses di luar ruang lingkupnya?

1029

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?

tidak diketahui
sumber
14
ini bahkan tidak dapat dikompilasi seperti; jika Anda memperbaiki bisnis yang tidak berfungsi, gcc akan tetap memperingatkan address of local variable ‘a’ returned; valgrind showsInvalid write of size 4 [...] Address 0xbefd7114 is just below the stack ptr
sehe
76
@Serge: Kembali ke masa muda saya, saya pernah bekerja pada beberapa kode nol-cincin yang agak rumit yang berjalan pada sistem operasi Netware yang melibatkan secara cerdik bergerak di sekitar penunjuk tumpukan dengan cara yang tidak sepenuhnya disetujui oleh sistem operasi. Saya akan tahu kapan saya melakukan kesalahan karena sering kali tumpukan itu berakhir tumpang tindih dengan memori layar dan saya hanya bisa menonton byte yang dituliskan langsung ke layar. Anda tidak dapat pergi dengan hal semacam itu hari ini.
Eric Lippert
23
lol. Saya perlu membaca pertanyaan dan beberapa jawaban sebelum saya bahkan mengerti di mana masalahnya. Apakah itu sebenarnya pertanyaan tentang ruang lingkup akses variabel? Anda bahkan tidak menggunakan 'a' di luar fungsi Anda. Dan hanya itu yang ada untuk itu. Melempar beberapa referensi memori adalah topik yang sama sekali berbeda dari ruang lingkup variabel.
erikbwork
10
Jawaban dupe tidak berarti pertanyaan dupe. Banyak pertanyaan dupe yang diajukan orang di sini adalah pertanyaan yang sama sekali berbeda yang merujuk pada gejala mendasar yang sama ... tetapi si penanya telah mengetahui cara mengetahui hal itu sehingga mereka harus tetap terbuka. Saya menutup dupe yang lebih tua dan menggabungkannya ke dalam pertanyaan ini yang seharusnya tetap terbuka karena memiliki jawaban yang sangat bagus.
Joel Spolsky
16
@ Joel: Jika jawabannya di sini baik, itu harus digabungkan ke pertanyaan yang lebih tua , yang ini adalah penipuan, bukan sebaliknya. Dan pertanyaan ini memang merupakan penipuan dari pertanyaan lain yang diajukan di sini dan kemudian beberapa (meskipun beberapa yang diusulkan lebih cocok daripada yang lain). Perhatikan bahwa saya pikir jawaban Eric baik. (Sebenarnya, saya menandai pertanyaan ini karena menggabungkan jawaban ke dalam salah satu pertanyaan yang lebih tua untuk menyelamatkan pertanyaan yang lebih tua.)
sbi

Jawaban:

4801

Bagaimana bisa? Bukankah memori variabel lokal tidak dapat diakses di luar fungsinya?

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:

Eric Lippert
sumber
56
@ muntoo: Sayangnya ini tidak seperti sistem operasi membunyikan sirene peringatan sebelum ia menonaktifkan atau membatalkan alokasi halaman memori virtual. Jika Anda mengotak-atik memori itu ketika Anda tidak memilikinya lagi, sistem operasi berhak untuk menghapus seluruh proses ketika Anda menyentuh halaman yang tidak terisi. Ledakan!
Eric Lippert
83
@Kyle: Hanya hotel yang aman yang melakukannya. Hotel-hotel yang tidak aman mendapatkan keuntungan yang terukur dari tidak harus membuang waktu untuk kunci pemrograman.
Alexander Torstling
498
@cyberguijarro: Bahwa C ++ bukan memori yang aman hanyalah fakta. Itu bukan "bashing" apa pun. Seandainya saya berkata, misalnya, "C ++ adalah mishmash mengerikan dari fitur yang tidak ditentukan, terlalu rumit yang ditumpuk di atas model memori yang rapuh dan berbahaya dan saya bersyukur setiap hari saya tidak lagi bekerja di dalamnya untuk kewarasan saya sendiri", itu akan menjadi bashing C ++. Menunjukkan bahwa ini bukan memori aman menjelaskan mengapa poster asli melihat masalah ini; itu menjawab pertanyaan, bukan editorialisasi.
Eric Lippert
50
Tegasnya analoginya harus menyebutkan bahwa resepsionis di hotel cukup senang bagi Anda untuk mengambil kunci bersama Anda. "Oh, apakah kamu keberatan jika aku membawa kunci ini bersamaku?" "Silakan. Mengapa saya peduli? Saya hanya bekerja di sini". Itu tidak menjadi ilegal sampai Anda mencoba menggunakannya.
philsquared
140
Tolong, tolong setidaknya pertimbangkan untuk menulis buku satu hari. Saya akan membelinya bahkan jika itu hanya kumpulan posting blog yang direvisi dan diperluas, dan saya yakin begitu banyak orang. Tetapi sebuah buku dengan pemikiran orisinal Anda tentang berbagai hal yang berhubungan dengan pemrograman akan menjadi bacaan yang bagus. Saya tahu itu luar biasa sulit untuk menemukan waktu untuk itu, tapi tolong pertimbangkan untuk menulis satu.
Dyppl
276

Apa yang Anda lakukan di sini hanyalah membaca dan menulis ke memori yang dulunya alamat a. Sekarang Anda berada di luar foo, 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, 5masih 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 foodihilangkan 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 untuk adirinya sendiri di luar foo), hanya aturan akses memori, yang hanya memicu peringatan daripada kesalahan.

Singkatnya: ini biasanya tidak akan berhasil, tetapi kadang-kadang akan terjadi secara kebetulan.

Rena
sumber
152

Karena ruang penyimpanan belum diinjak. Jangan mengandalkan perilaku itu.

msw
sumber
1
Sobat, itu adalah komentar terlama sejak, "Apa itu kebenaran?" Kata Pilatus sambil bercanda. Mungkin itu adalah Alkitab Gideon di laci hotel itu. Dan apa yang terjadi pada mereka? Perhatikan mereka tidak lagi hadir, setidaknya di London. Saya kira bahwa di bawah undang-undang Kesetaraan, Anda akan membutuhkan perpustakaan saluran agama.
Rob Kent
Saya bisa bersumpah bahwa saya menulis itu sejak lama, tetapi baru-baru ini muncul dan menemukan tanggapan saya tidak ada di sana. Sekarang saya harus mencari tahu alusi Anda di atas seperti yang saya harapkan saya akan terhibur ketika saya melakukannya>. <
msw
1
Ha ha. Francis Bacon, salah satu penulis esai terbesar di Inggris, yang dicurigai oleh beberapa orang menulis naskah Shakespeare, karena mereka tidak dapat menerima bahwa anak sekolah tata bahasa dari negara itu, putra seorang glover, bisa menjadi jenius. Begitulah sistem kelas bahasa Inggris. Yesus berkata, 'Akulah Kebenaran'. oregonstate.edu/instruct/phl302/texts/bacon/bacon_essays.html
Rob Kent
84

Sedikit tambahan untuk semua jawaban:

jika Anda melakukan sesuatu seperti itu:

#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);
}

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.

Michael
sumber
2
Sederhana, tetapi contoh yang bagus untuk memahami teori stack yang mendasarinya. Hanya satu tambahan pengujian, menyatakan "int a = 5;" di foo () sebagai "static int a = 5;" dapat digunakan untuk memahami ruang lingkup dan waktu hidup dari variabel statis.
Kontrol
15
-1 "untuk mungkin 7 ". Compiler mungkin mendaftar di boo. Mungkin menghapusnya karena itu tidak perlu. Ada kemungkinan bagus bahwa * p tidak akan menjadi 5 , tetapi itu tidak berarti bahwa ada alasan yang sangat bagus mengapa itu mungkin 7 .
Mat
2
Ini disebut perilaku tidak terdefinisi!
Francis Cugler
mengapa dan bagaimana boomenggunakan kembali footumpukan? tidak fungsi tumpukan terpisah satu sama lain, juga saya mendapatkan sampah menjalankan kode ini di Visual Studio 2015
ampawd
1
@ampawd sudah hampir setahun, tapi tidak, "tumpukan fungsi" tidak terpisah satu sama lain. CONTEXT memiliki tumpukan. Konteks itu menggunakan tumpukannya untuk memasukkan main, lalu turun ke foo(), ada, lalu turun ke boo(). Foo()dan Boo()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 panggilan boo()dan foo(), mengubah isinya ...
Russ Schultz
72

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 ...

Charles Brunet
sumber
5
Anda mungkin bermaksud mencoba mengakses alamat apa pun. Karena sebagian besar sistem operasi saat ini tidak akan membiarkan program mengakses alamat apa pun; ada banyak perlindungan untuk melindungi ruang alamat. Inilah sebabnya mengapa tidak akan ada LOADLIN.EXE lain di luar sana.
v010dya
67

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:

unsigned int q = 123456;

*(double*)(q) = 1.2;

Di sini saya hanya memperlakukan 123456 sebagai alamat ganda dan menulis untuk itu. Sejumlah hal dapat terjadi:

  1. qmungkin sebenarnya benar-benar menjadi alamat ganda yang valid, misalnya double p; q = &p;.
  2. q mungkin menunjuk suatu tempat di dalam memori yang dialokasikan dan saya hanya menimpa 8 byte di sana.
  3. q menunjuk ke luar memori yang dialokasikan dan manajer memori sistem operasi mengirimkan sinyal kesalahan segmentasi ke program saya, menyebabkan runtime menghentikannya.
  4. Anda memenangkan lotre.

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 valgrindakan dengan senang hati melakukan ini, jadi Anda harus menjalankan program Anda melalui itu dan menyaksikan kesalahan.

Kerrek SB
sumber
9
Saya hanya akan menulis sebuah program sekarang yang terus menjalankan program ini sehingga 4) I win the lottery
Aidiakapi
29

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.

gastush
sumber
Itu taruhan saya. Pengoptimal membuang panggilan fungsi.
Erik Aronesty
9
Itu tidak perlu. Karena tidak ada fungsi baru yang dipanggil setelah foo (), fungsi frame stack lokal belum ditimpa. Tambahkan pemanggilan fungsi lain setelah foo (), dan 5akan diubah ...
Tomas
Saya menjalankan program dengan GCC 4.8, mengganti cout dengan printf (dan termasuk stdio). Memperingatkan dengan benar "peringatan: alamat variabel lokal 'a' dikembalikan [-Wreturn-local-addr]". Output 58 tanpa optimasi dan 08 dengan -O3. Anehnya P memang memiliki alamat, meskipun nilainya 0. Saya harapkan NULL (0) sebagai alamat.
kevinf
23

Masalah Anda tidak ada hubungannya dengan ruang lingkup . Dalam kode yang Anda tunjukkan, fungsi maintidak melihat nama-nama dalam fungsi foo, jadi Anda tidak dapat mengakses adi foo langsung dengan nama ini di luar foo.

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.

Chang Peng
sumber
Saya ingat dari salinan lama Pemrograman Turbo C untuk IBM , yang saya gunakan untuk bermain-main beberapa waktu lalu, bagaimana secara langsung memanipulasi memori grafis, dan tata letak memori video mode teks IBM, dijelaskan dengan sangat rinci. Tentu saja, sistem yang menjalankan kode itu dengan jelas mendefinisikan apa arti penulisan ke alamat-alamat itu, jadi selama Anda tidak khawatir tentang portabilitas ke sistem lain, semuanya baik-baik saja. IIRC, petunjuk untuk membatalkan adalah tema umum dalam buku itu.
CVn
@Michael Kjörling: Tentu! Orang-orang suka melakukan pekerjaan kotor sesekali;)
Chang Peng
18

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.

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
}
Brian R. Bondy
sumber
Saya tidak setuju: Ada masalah sebelum cout. *amenunjuk ke memori yang tidak terisi (dibebaskan). Bahkan jika Anda tidak menolaknya, itu masih berbahaya (dan kemungkinan palsu).
sebelum
@ereOn: Saya mengklarifikasi lebih banyak apa yang saya maksud dengan masalah, tetapi tidak, itu tidak berbahaya dalam hal kode c ++ yang valid. Tetapi berbahaya dalam hal kemungkinan pengguna melakukan kesalahan dan akan melakukan sesuatu yang buruk. Mungkin misalnya Anda mencoba untuk melihat bagaimana tumpukan tumbuh, dan Anda hanya peduli dengan nilai alamat dan tidak akan pernah mengubahnya.
Brian R. Bondy
18

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.

Kerrek SB
sumber
18

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:

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;
}

Ini mencetak "y = 123", tetapi hasil Anda mungkin bervariasi (sungguh!). Pointer Anda menghancurkan variabel lokal lain yang tidak terkait.

Letakkan
sumber
18

Perhatikan semua peringatan. Jangan hanya menyelesaikan kesalahan.
GCC menunjukkan Peringatan ini

peringatan: alamat variabel lokal 'a' dikembalikan

Ini adalah kekuatan C ++. Anda harus peduli dengan ingatan. Dengan -Werrorbendera, peringatan ini menjadi kesalahan dan sekarang Anda harus men-debug-nya.

sam
sumber
17

Ini bekerja karena tumpukan belum diubah (belum) sejak diletakkan di sana. Panggil beberapa fungsi lain (yang juga memanggil fungsi lain) sebelum mengakses alagi dan Anda mungkin tidak akan seberuntung itu lagi ... ;-)

Adrian Grigore
sumber
16

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 amelainkan lokasi memori di mana adulu. Perbedaan ini sangat mirip dengan perbedaan antara menabrak dan tidak menabrak.

Alexander Gessler
sumber
14

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 nilai a(atau alamat memori yang adigunakan 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!

larsmoa
sumber
3
"cetak nilai blok memori dengan alamat yang dulu ditempati oleh" tidak tepat. Ini membuatnya terdengar seperti kodenya memiliki beberapa makna yang jelas, yang tidak terjadi. Anda benar bahwa ini mungkin bagaimana kebanyakan kompiler akan mengimplementasikannya.
Brennan Vincent
@ BrennanVincent: Sementara penyimpanan ditempati a, pointer memegang alamat a. 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.
supercat
@ BrennanVincent: Sebagai contoh, sementara Standar mungkin tidak mengharuskan implementasi memungkinkan penunjuk yang lewat untuk reallocdibandingkan 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 diberikan realloc.
supercat
14

Itu bisa, karena amerupakan 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.

littleadv
sumber
12

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

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); 
}
Mykola
sumber
5

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 alamat adan adihancurkan 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.

Ghulam Moinul Quadir
sumber
4

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).

Ayub
sumber
1

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:

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;
}

Berbeda dengan contoh Anda, dengan contoh ini Anda adalah:

  • mengalokasikan memori untuk int ke fungsi lokal
  • alamat memori itu masih berlaku juga ketika fungsi berakhir, (itu tidak dihapus oleh siapa pun)
  • alamat memori dapat dipercaya (bahwa blok memori tidak dianggap gratis, sehingga tidak akan diganti sampai dihapus)
  • alamat memori harus dihapus ketika tidak digunakan. (lihat penghapusan di akhir program)
Nobun
sumber
Apakah Anda menambahkan sesuatu yang belum dicakup oleh jawaban yang ada? Dan tolong jangan gunakan pointer mentah / new.
Lightness Races in Orbit
1
Penanya menggunakan pointer mentah. Saya melakukan contoh yang mencerminkan persis contoh yang dia lakukan untuk memungkinkan dia melihat perbedaan antara pointer yang tidak dapat dipercaya dan yang dapat dipercaya. Sebenarnya ada jawaban lain yang mirip dengan saya, tetapi ia menggunakan strcpy yang, IMHO, bisa kurang jelas untuk pemula kode daripada contoh saya yang menggunakan yang baru.
Nobun
Mereka tidak menggunakannya new. Anda mengajar mereka untuk menggunakan new. Tetapi Anda tidak harus menggunakannya new.
Lightness Races in Orbit
Jadi menurut Anda lebih baik untuk memberikan alamat ke variabel lokal yang dihancurkan dalam suatu fungsi daripada benar-benar mengalokasikan memori? Ini tidak masuk akal. Memahami konsep pengalokasian memori secara dealokasi adalah penting, ya, terutama jika Anda bertanya tentang pointer (penanya tidak menggunakan pointer baru, tetapi digunakan).
Nobun
Kapan saya mengatakan itu? Tidak, lebih baik menggunakan smart pointer untuk menunjukkan kepemilikan sumber daya yang dirujuk dengan benar. Jangan gunakan newpada tahun 2019 (kecuali Anda sedang menulis kode perpustakaan) dan jangan mengajari pendatang baru untuk melakukannya juga! Bersulang.
Lightness Races in Orbit