Dalam C, apakah kawat gigi bertindak sebagai bingkai tumpukan?

153

Jika saya membuat variabel dalam set baru kurung kurawal, apakah variabel itu muncul dari tumpukan pada kurung kurawal, atau apakah itu hang out sampai akhir fungsi? Sebagai contoh:

void foo() {
   int c[100];
   {
       int d[200];
   }
   //code that takes a while
   return;
}

Akan dmengambil memori selama code that takes a whilebagian?

Claudiu
sumber
8
Apakah maksud Anda (1) sesuai dengan Standar, (2) praktik universal di antara implementasi, atau (3) praktik umum di antara implementasi?
David Thornley

Jawaban:

83

Tidak, kawat gigi tidak bertindak sebagai bingkai tumpukan. Dalam C, kawat gigi hanya menunjukkan ruang lingkup penamaan, tetapi tidak ada yang dihancurkan juga tidak ada yang muncul dari tumpukan ketika kontrol keluar dari itu.

Sebagai seorang programmer yang menulis kode, Anda sering dapat menganggapnya seolah-olah itu adalah bingkai tumpukan. Pengidentifikasi yang dideklarasikan dalam kurung kurawal hanya dapat diakses dalam kurung kurawal, jadi dari sudut pandang seorang programmer, sepertinya mereka didorong ke tumpukan seperti yang dideklarasikan dan kemudian muncul ketika cakupan keluar. Namun, kompiler tidak harus membuat kode yang mendorong / muncul apa pun saat masuk / keluar (dan umumnya, mereka tidak).

Perhatikan juga bahwa variabel lokal mungkin tidak menggunakan ruang stack sama sekali: mereka dapat disimpan dalam register CPU atau di beberapa lokasi penyimpanan tambahan lainnya, atau dioptimalkan sepenuhnya.

Jadi, darray, secara teori, dapat mengkonsumsi memori untuk seluruh fungsi. Namun, kompiler dapat mengoptimalkannya, atau berbagi memorinya dengan variabel lokal lainnya yang masa pakainya tidak tumpang tindih.

Kristopher Johnson
sumber
9
Bukankah itu spesifik implementasi?
avakar
54
Dalam C ++, destruktor objek dipanggil pada akhir ruang lingkupnya. Apakah memori akan direklamasi adalah masalah khusus implementasi.
Kristopher Johnson
8
@ pm100: Destruktor akan dipanggil. Itu tidak mengatakan apa-apa tentang memori yang ditempati benda-benda itu.
Donal Fellows
9
Standar C menentukan bahwa masa pakai variabel otomatis yang dideklarasikan di blok hanya meluas sampai eksekusi blok berakhir. Jadi pada dasarnya variabel - variabel otomatis itu "dihancurkan" di akhir blok.
caf
3
@KristopherJohnson: Jika suatu metode memiliki dua blok terpisah, masing-masing menyatakan array 1Kbyte, dan blok ketiga yang disebut metode bersarang, kompiler akan bebas menggunakan memori yang sama untuk kedua array, dan / atau untuk menempatkan array di bagian dangkal tumpukan dan pindahkan penunjuk tumpukan di atasnya memanggil metode bersarang. Perilaku seperti itu dapat mengurangi 2K kedalaman tumpukan yang diperlukan untuk pemanggilan fungsi.
supercat
39

Waktu di mana variabel benar - benar mengambil memori jelas tergantung pada kompiler (dan banyak kompiler tidak menyesuaikan penumpukan tumpukan ketika blok bagian dalam dimasukkan dan keluar dalam fungsi).

Namun, pertanyaan yang erat terkait tetapi mungkin lebih menarik adalah apakah program diizinkan untuk mengakses objek dalam itu di luar lingkup dalam (tetapi dalam fungsi yang mengandung), yaitu:

void foo() {
   int c[100];
   int *p;

   {
       int d[200];
       p = d;
   }

   /* Can I access p[0] here? */

   return;
}

(Dengan kata lain: apakah kompiler diperbolehkan untuk melakukan deallocate d, walaupun dalam praktiknya kebanyakan tidak?).

Jawabannya adalah bahwa compiler yang diperbolehkan untuk deallocate d, dan mengakses p[0]di mana komentar menunjukkan adalah perilaku undefined (program ini tidak diperbolehkan untuk akses di luar objek dalam dari ruang lingkup batin). Bagian yang relevan dari standar C adalah 6.2.4p5:

Untuk objek semacam itu [objek yang memiliki durasi penyimpanan otomatis] yang tidak memiliki tipe array panjang variabel, masa pakainya akan meluas dari entri ke dalam blok yang terkait dengannya sampai eksekusi blok itu berakhir dengan cara apa pun . (Memasukkan blok terlampir atau memanggil fungsi tertunda, tetapi tidak berakhir, eksekusi blok saat ini.) Jika blok dimasukkan secara rekursif, instance objek baru dibuat setiap kali. Nilai awal objek tidak pasti. Jika inisialisasi ditentukan untuk objek, itu dilakukan setiap kali deklarasi tercapai dalam pelaksanaan blok; jika tidak, nilai menjadi tak tentu setiap kali deklarasi tercapai.

kaf
sumber
Ketika seseorang mempelajari bagaimana ruang lingkup dan memori bekerja dalam C dan C ++ setelah bertahun-tahun menggunakan bahasa tingkat yang lebih tinggi, saya menemukan jawaban ini lebih tepat dan bermanfaat daripada yang diterima.
Chris
20

Pertanyaan Anda tidak cukup jelas untuk dijawab dengan jelas.

Di satu sisi, kompiler biasanya tidak melakukan alokasi alokasi-memori lokal untuk ruang lingkup blok bersarang. Memori lokal biasanya dialokasikan hanya sekali pada entri fungsi dan dirilis pada keluar fungsi.

Di sisi lain, ketika masa hidup objek lokal berakhir, memori yang ditempati oleh objek itu dapat digunakan kembali untuk objek lokal lain nanti. Misalnya, dalam kode ini

void foo()
{
  {
    int d[100];
  }
  {
    double e[20];
  }
}

kedua array biasanya akan menempati area memori yang sama, artinya jumlah total penyimpanan lokal yang dibutuhkan oleh fungsi fooadalah apa pun yang diperlukan untuk yang terbesar dari dua array, bukan untuk keduanya sekaligus.

Apakah yang terakhir memenuhi syarat sebagai dterus menempati ingatan sampai akhir fungsi dalam konteks pertanyaan Anda, apakah Anda yang memutuskan.

Semut
sumber
6

Tergantung implementasi. Saya menulis sebuah program pendek untuk menguji apa yang dilakukan gcc 4.3.4, dan ia mengalokasikan semua ruang stack sekaligus pada awal fungsi. Anda dapat memeriksa perakitan yang diproduksi gcc menggunakan flag -S.

Daniel Stutzbach
sumber
3

Tidak, d [] tidak akan ada di tumpukan selama sisa rutin. Tetapi pengalokasian () berbeda.

Sunting: Kristopher Johnson (dan simon dan Daniel) benar , dan tanggapan awal saya salah . Dengan gcc 4.3.4.pada CYGWIN, kode:

void foo(int[]);
void bar(void);
void foobar(int); 

void foobar(int flag) {
    if (flag) {
        int big[100000000];
        foo(big);
    }
    bar();
}

memberi:

_foobar:
    pushl   %ebp
    movl    %esp, %ebp
    movl    $400000008, %eax
    call    __alloca
    cmpl    $0, 8(%ebp)
    je      L2
    leal    -400000000(%ebp), %eax
    movl    %eax, (%esp)
    call    _foo
L2:
    call    _bar
    leave
    ret

Hidup dan belajar! Dan tes cepat tampaknya menunjukkan bahwa AndreyT juga benar tentang alokasi ganda.

Ditambahkan jauh kemudian : Tes di atas menunjukkan dokumentasi gcc tidak tepat. Selama bertahun-tahun dikatakan (penekanan ditambahkan):

"Ruang untuk array panjang variabel dideallocated segera setelah ruang lingkup nama array berakhir ."

Joseph Quinsey
sumber
Kompilasi dengan optimisasi dinonaktifkan tidak selalu menunjukkan kepada Anda apa yang akan Anda dapatkan dalam kode yang dioptimalkan. Dalam hal ini, tingkah lakunya sama (mengalokasikan di awal fungsi, dan hanya gratis ketika meninggalkan fungsi): godbolt.org/g/M112AQ . Tapi gcc non-cygwin tidak memanggil allocafungsi. Saya sangat terkejut bahwa cygwin gcc akan melakukan itu. Itu bahkan bukan array panjang variabel, jadi IDK mengapa Anda memunculkannya.
Peter Cordes
2

Mereka mungkin. Mungkin tidak. Jawaban yang menurut saya sangat Anda butuhkan adalah: Jangan pernah berasumsi apa pun. Kompiler modern mengerjakan semua jenis arsitektur dan implementasi khusus sihir. Tulis kode Anda secara sederhana dan jelas kepada manusia dan biarkan kompiler melakukan hal-hal yang baik. Jika Anda mencoba kode di sekitar kompiler Anda meminta masalah - dan masalah yang biasanya Anda dapatkan dalam situasi ini biasanya sangat halus dan sulit untuk didiagnosis.

pengguna19666
sumber
1

Variabel Anda dbiasanya tidak muncul dari tumpukan. Kurung kurawal tidak menunjukkan bingkai tumpukan. Jika tidak, Anda tidak akan dapat melakukan hal seperti ini:

char var = getch();
    {
        char next_var = var + 1;
        use_variable(next_char);
    }

Jika kurung kurawal menyebabkan stack push / pop yang sebenarnya (seperti panggilan fungsi akan), maka kode di atas tidak akan dikompilasi karena kode di dalam kurung kurawal tidak akan dapat mengakses variabel varyang hidup di luar kurung kurawal (sama seperti sub- fungsi tidak dapat langsung mengakses variabel dalam fungsi panggilan). Kita tahu ini bukan masalahnya.

Kurung kurawal hanya digunakan untuk pelingkupan. Kompiler akan memperlakukan setiap akses ke variabel "dalam" dari luar kurung kurawal sebagai tidak sah, dan mungkin menggunakan kembali memori itu untuk sesuatu yang lain (ini tergantung pada implementasi). Namun, itu mungkin tidak muncul dari tumpukan sampai fungsi penutup kembali.

Update: Inilah yang C spek katakan. Mengenai benda-benda dengan durasi penyimpanan otomatis (bagian 6.4.2):

Untuk objek yang tidak memiliki tipe array panjang variabel, masa pakainya diperpanjang dari entri ke dalam blok yang terkait dengan itu sampai pelaksanaan blok itu berakhir pula.

Bagian yang sama mendefinisikan istilah "seumur hidup" sebagai (penekanan milikku):

The seumur hidup dari sebuah objek adalah bagian dari pelaksanaan program selama penyimpanan dijamin akan disediakan untuk itu. Objek ada, memiliki alamat konstan, dan mempertahankan nilai yang terakhir disimpan sepanjang masa pakainya. Jika suatu benda disebut di luar masa hidupnya, perilaku itu tidak terdefinisi.

Kata kuncinya di sini adalah, tentu saja, 'dijamin'. Setelah Anda meninggalkan ruang lingkup set kawat gigi dalam, masa pakai array sudah berakhir. Penyimpanan mungkin atau mungkin masih belum dialokasikan untuk itu (kompiler Anda mungkin menggunakan kembali ruang untuk sesuatu yang lain), tetapi setiap upaya untuk mengakses array memicu perilaku yang tidak terdefinisi dan membawa hasil yang tidak terduga.

C spec tidak memiliki gagasan tentang stack frame. Itu hanya berbicara tentang bagaimana program yang dihasilkan akan berperilaku, dan meninggalkan detail implementasi ke kompiler (setelah semua, implementasi akan terlihat sangat berbeda pada CPU tanpa tumpukan dibandingkan pada CPU dengan tumpukan perangkat keras). Tidak ada dalam spesifikasi C yang mengamanatkan di mana frame stack akan atau tidak akan berakhir. Satu-satunya cara nyata untuk mengetahui adalah mengkompilasi kode pada kompiler / platform khusus Anda dan memeriksa rakitan yang dihasilkan. Pilihan optimisasi kompiler Anda saat ini kemungkinan juga akan berperan dalam hal ini.

Jika Anda ingin memastikan bahwa array dtidak lagi menghabiskan memori saat kode Anda sedang berjalan, Anda dapat mengonversi kode dalam kurung kurawal menjadi fungsi terpisah atau secara eksplisit mallocdan freememori alih-alih menggunakan penyimpanan otomatis.

bta
sumber
1
"Jika kawat gigi keriting menyebabkan penumpukan push / pop, maka kode di atas tidak akan dikompilasi karena kode di dalam kawat gigi tidak akan dapat mengakses variabel var yang hidup di luar kawat gigi" - ini tidak benar. Compiler selalu dapat mengingat jarak dari stack / frame pointer, dan menggunakannya untuk referensi variabel luar. Juga, lihat jawaban Joseph untuk contoh kurung kurawal yang memang menyebabkan penumpukan push / pop.
george
@ george- Perilaku yang Anda gambarkan, serta contoh Joseph, bergantung pada kompiler dan platform yang Anda gunakan. Misalnya, kompilasi kode yang sama untuk target MIPS menghasilkan hasil yang sama sekali berbeda. Saya berbicara murni dari sudut pandang spesifikasi C (karena OP tidak menentukan kompiler atau target). Saya akan mengedit jawabannya dan menambahkan lebih spesifik.
bta
0

Saya percaya bahwa itu keluar dari ruang lingkup, tetapi tidak muncul dari tumpukan sampai fungsi kembali. Jadi masih akan mengambil memori pada stack sampai fungsi selesai, tetapi tidak dapat diakses di hilir kurung kurawal penutupan pertama.

simon
sumber
3
Tidak ada jaminan Setelah cakupan ditutup, kompiler tidak lagi melacak memori itu (atau setidaknya tidak diperlukan untuk ...) dan mungkin menggunakannya kembali. Inilah sebabnya mengapa menyentuh memori yang sebelumnya ditempati oleh variabel out of scope adalah perilaku yang tidak terdefinisi. Waspadalah terhadap setan hidung dan peringatan serupa.
dmckee --- ex-moderator kitten
0

Sudah ada banyak informasi mengenai standar yang mengindikasikan bahwa ini memang implementasi spesifik.

Jadi, satu percobaan mungkin menarik. Jika kami mencoba kode berikut:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
        printf("%p\n", (void*) x);
    }
    {
        int b;
        y = &b;
        printf("%p\n", (void*) y);
    }
}

Menggunakan gcc yang kami dapatkan di sini dua kali alamat yang sama: Coliro

Tetapi jika kita mencoba kode berikut:

#include <stdio.h>
int main() {
    int* x;
    int* y;
    {
        int a;
        x = &a;
    }
    {
        int b;
        y = &b;
    }
    printf("%p\n", (void*) x);
    printf("%p\n", (void*) y);
}

Dengan menggunakan gcc, kami memperoleh dua alamat berbeda di sini: Coliro

Jadi, Anda tidak dapat benar-benar yakin apa yang sedang terjadi.

Mi-He
sumber