C Manajemen Memori

90

Saya selalu mendengar bahwa di C Anda harus benar-benar memperhatikan bagaimana Anda mengelola memori. Dan saya masih mulai belajar C, tapi sejauh ini, saya tidak perlu melakukan pengelolaan memori sama sekali. Saya selalu membayangkan harus melepaskan variabel dan melakukan segala macam hal yang buruk. Tapi sepertinya bukan itu masalahnya.

Dapatkah seseorang menunjukkan kepada saya (dengan contoh kode) contoh kapan Anda harus melakukan "manajemen memori"?

The.Anti.9
sumber
Tempat yang baik untuk belajar G4G
EsmaeelE

Jawaban:

231

Ada dua tempat di mana variabel bisa dimasukkan ke dalam memori. Saat Anda membuat variabel seperti ini:

int  a;
char c;
char d[16];

Variabel dibuat di " tumpukan ". Variabel tumpukan secara otomatis dibebaskan saat berada di luar ruang lingkup (yaitu, saat kode tidak dapat menjangkau mereka lagi). Anda mungkin mendengarnya disebut variabel "otomatis", tetapi itu sudah ketinggalan zaman.

Banyak contoh pemula hanya akan menggunakan variabel tumpukan.

Tumpukan itu bagus karena otomatis, tetapi juga memiliki dua kelemahan: (1) Kompilator perlu mengetahui sebelumnya seberapa besar variabelnya, dan (b) ruang tumpukan agak terbatas. Misalnya: di Windows, di bawah pengaturan default untuk Microsoft linker, tumpukan diatur ke 1 MB, dan tidak semuanya tersedia untuk variabel Anda.

Jika Anda tidak tahu pada waktu kompilasi seberapa besar array Anda, atau jika Anda membutuhkan array atau struct yang besar, Anda memerlukan "plan B".

Rencana B disebut dengan " heap ". Anda biasanya dapat membuat variabel sebesar yang diizinkan Sistem Operasi, tetapi Anda harus melakukannya sendiri. Posting sebelumnya menunjukkan kepada Anda satu cara untuk melakukannya, meskipun ada cara lain:

int size;
// ...
// Set size to some value, based on information available at run-time. Then:
// ...
char *p = (char *)malloc(size);

(Perhatikan bahwa variabel di heap tidak dimanipulasi secara langsung, tetapi melalui pointer)

Setelah Anda membuat variabel heap, masalahnya adalah kompilator tidak dapat mengetahui saat Anda selesai menggunakannya, jadi Anda kehilangan pelepasan otomatis. Di situlah "pelepasan manual" yang Anda maksud masuk. Kode Anda sekarang bertanggung jawab untuk memutuskan kapan variabel tidak diperlukan lagi, dan melepaskannya sehingga memori dapat diambil untuk tujuan lain. Untuk kasus diatas, dengan:

free(p);

Apa yang membuat opsi kedua ini "bisnis yang buruk" adalah tidak selalu mudah untuk mengetahui kapan variabel tidak diperlukan lagi. Lupa melepaskan variabel saat Anda tidak membutuhkannya akan menyebabkan program Anda menghabiskan lebih banyak memori yang diperlukannya. Situasi ini disebut "kebocoran". Memori "bocor" tidak dapat digunakan untuk apa pun hingga program Anda berakhir dan OS memulihkan semua sumber dayanya. Masalah yang lebih buruk pun mungkin terjadi jika Anda melepaskan variabel heap secara tidak sengaja sebelumnya Anda benar-benar selesai menggunakannya.

Di C dan C ++, Anda bertanggung jawab untuk membersihkan variabel heap seperti yang ditunjukkan di atas. Namun, ada bahasa dan lingkungan seperti Java dan bahasa .NET seperti C # yang menggunakan pendekatan berbeda, di mana heap dibersihkan sendiri. Metode kedua ini, yang disebut "pengumpulan sampah", jauh lebih mudah bagi pengembang tetapi Anda harus membayar denda dalam biaya overhead dan kinerja. Itu keseimbangan.

(Saya telah menutupi banyak detail untuk memberikan jawaban yang lebih sederhana, tetapi mudah-mudahan lebih rata)

Euro Micelli
sumber
3
Jika Anda ingin meletakkan sesuatu di tumpukan tetapi tidak tahu seberapa besar saat kompilasi, alloca () dapat memperbesar bingkai tumpukan untuk memberi ruang. Tidak ada freea (), seluruh frame stack akan muncul saat fungsi kembali. Menggunakan alloca () untuk alokasi besar berisiko.
DGentry
1
Mungkin Anda bisa menambahkan satu atau dua kalimat tentang lokasi memori variabel global
Michael Käfer
Di C tidak pernah memberikan pengembalian malloc(), penyebabnya UB, (char *)malloc(size);lihat stackoverflow.com/questions/605845/…
EsmaeelE
17

Berikut contohnya. Misalkan Anda memiliki fungsi strdup () yang menduplikasi string:

char *strdup(char *src)
{
    char * dest;
    dest = malloc(strlen(src) + 1);
    if (dest == NULL)
        abort();
    strcpy(dest, src);
    return dest;
}

Dan Anda menyebutnya seperti ini:

main()
{
    char *s;
    s = strdup("hello");
    printf("%s\n", s);
    s = strdup("world");
    printf("%s\n", s);
}

Anda dapat melihat bahwa program tersebut berfungsi, tetapi Anda telah mengalokasikan memori (melalui malloc) tanpa membebaskannya. Anda telah kehilangan penunjuk Anda ke blok memori pertama saat Anda memanggil strdup untuk kedua kalinya.

Ini bukan masalah besar untuk jumlah memori kecil ini, tetapi pertimbangkan kasusnya:

for (i = 0; i < 1000000000; ++i)  /* billion times */
    s = strdup("hello world");    /* 11 bytes */

Anda sekarang telah menggunakan 11 gig memori (mungkin lebih, tergantung pada pengelola memori Anda) dan jika Anda belum crash proses Anda mungkin berjalan cukup lambat.

Untuk memperbaikinya, Anda perlu memanggil free () untuk semua yang diperoleh dengan malloc () setelah Anda selesai menggunakannya:

s = strdup("hello");
free(s);  /* now not leaking memory! */
s = strdup("world");
...

Semoga contoh ini membantu!

Mark Harrison
sumber
Saya lebih menyukai jawaban ini. Tapi saya mendapat sedikit pertanyaan sampingan. Saya berharap sesuatu seperti ini diselesaikan dengan perpustakaan, bukankah ada perpustakaan yang akan meniru tipe data dasar dengan dekat dan menambahkan fitur-fitur yang membebaskan memori sehingga ketika variabel digunakan, mereka juga dibebaskan secara otomatis?
Lorenzo
Tidak ada yang merupakan bagian dari standar. Jika Anda masuk ke C ++ Anda mendapatkan string dan kontainer yang melakukan manajemen memori otomatis.
Mark Harrison
Begitu, jadi ada beberapa libs pihak ketiga? Bisakah Anda memberi nama mereka?
Lorenzo
9

Anda harus melakukan "manajemen memori" saat Anda ingin menggunakan memori di heap daripada di stack. Jika Anda tidak tahu seberapa besar membuat array hingga runtime, Anda harus menggunakan heap. Misalnya, Anda mungkin ingin menyimpan sesuatu dalam string, tetapi tidak tahu seberapa besar isinya hingga program dijalankan. Dalam hal ini Anda akan menulis sesuatu seperti ini:

 char *string = malloc(stringlength); // stringlength is the number of bytes to allocate

 // Do something with the string...

 free(string); // Free the allocated memory
Jeremy Ruten
sumber
5

Saya pikir cara paling ringkas untuk menjawab pertanyaan untuk mempertimbangkan peran penunjuk di C. Penunjuk adalah mekanisme ringan namun kuat yang memberi Anda kebebasan besar dengan mengorbankan kapasitas yang sangat besar untuk menembak diri sendiri di kaki.

Di C tanggung jawab untuk memastikan petunjuk Anda menunjuk ke memori yang Anda miliki adalah milik Anda dan milik Anda sendiri. Ini membutuhkan pendekatan yang terorganisir dan disiplin, kecuali jika Anda mengabaikan petunjuk, yang membuatnya sulit untuk menulis C yang efektif.

Jawaban yang diposting hingga saat ini berkonsentrasi pada alokasi variabel tumpukan dan otomatis. Menggunakan alokasi tumpukan memang membuat memori yang dikelola secara otomatis dan nyaman, tetapi dalam beberapa keadaan (buffer besar, algoritme rekursif) dapat menyebabkan masalah tumpukan overflow yang menghebohkan. Mengetahui dengan tepat berapa banyak memori yang dapat Anda alokasikan pada stack sangat bergantung pada sistem. Dalam beberapa skenario tersemat, beberapa lusin byte mungkin menjadi batas Anda, dalam beberapa skenario desktop Anda dapat menggunakan megabyte dengan aman.

Alokasi heap kurang melekat pada bahasa. Ini pada dasarnya adalah serangkaian panggilan perpustakaan yang memberi Anda kepemilikan blok memori dengan ukuran tertentu sampai Anda siap untuk mengembalikannya ('gratis'). Kedengarannya sederhana, tetapi dikaitkan dengan kesedihan programmer yang tak terhitung. Masalahnya sederhana (membebaskan memori yang sama dua kali, atau tidak sama sekali [kebocoran memori], tidak mengalokasikan cukup memori [buffer overflow], dll) tetapi sulit untuk dihindari dan di-debug. Pendekatan yang sangat disiplin mutlak wajib dalam praktiknya, tetapi tentu saja bahasanya tidak benar-benar mengharuskannya.

Saya ingin menyebutkan jenis alokasi memori lain yang telah diabaikan oleh posting lain. Dimungkinkan untuk mengalokasikan variabel secara statis dengan mendeklarasikannya di luar fungsi apa pun. Saya pikir secara umum jenis alokasi ini mendapat reputasi buruk karena digunakan oleh variabel global. Namun tidak ada yang mengatakan satu-satunya cara untuk menggunakan memori yang dialokasikan dengan cara ini adalah sebagai variabel global yang tidak disiplin dalam kode spaghetti yang berantakan. Metode alokasi statis dapat digunakan hanya untuk menghindari beberapa perangkap metode heap dan alokasi otomatis. Beberapa pemrogram C terkejut mengetahui bahwa program tertanam dan permainan C yang besar dan canggih telah dibuat tanpa menggunakan alokasi heap sama sekali.

Bill Forster
sumber
4

Ada beberapa jawaban bagus di sini tentang cara mengalokasikan dan membebaskan memori, dan menurut saya sisi yang lebih menantang dalam menggunakan C adalah memastikan bahwa satu-satunya memori yang Anda gunakan adalah memori yang telah Anda alokasikan - jika ini tidak dilakukan dengan benar, apa yang Anda akhiri up dengan adalah sepupu situs ini - buffer overflow - dan Anda mungkin menimpa memori yang sedang digunakan oleh aplikasi lain, dengan hasil yang sangat tidak terduga.

Sebuah contoh:

int main() {
    char* myString = (char*)malloc(5*sizeof(char));
    myString = "abcd";
}

Pada titik ini Anda telah mengalokasikan 5 byte untuk myString dan mengisinya dengan "abcd \ 0" (string diakhiri dengan null - \ 0). Jika alokasi string Anda adalah

myString = "abcde";

Anda akan menetapkan "abcde" dalam 5 byte yang telah Anda alokasikan untuk program Anda, dan karakter nol di akhir akan diletakkan di akhir ini - bagian dari memori yang belum dialokasikan untuk Anda gunakan dan mungkin saja gratis, tetapi bisa juga digunakan oleh aplikasi lain - Ini adalah bagian penting dari manajemen memori, di mana kesalahan akan memiliki konsekuensi yang tidak dapat diprediksi (dan terkadang tidak dapat diulang).

Chris BC
sumber
Di sini Anda mengalokasikan 5 byte. Lepaskan dengan menetapkan penunjuk. Setiap upaya untuk membebaskan penunjuk ini mengarah ke perilaku yang tidak ditentukan. Catatan C-String tidak membebani = operator tidak ada salinannya.
Martin York
Padahal, itu sangat tergantung pada malloc yang Anda gunakan. Banyak operator malloc sejajar dengan 8 byte. Jadi jika malloc ini menggunakan sistem header / footer, malloc akan memesan 5 + 4 * 2 (4 byte untuk header dan footer). Itu akan menjadi 13 byte, dan malloc hanya akan memberi Anda 3 byte tambahan untuk penyelarasan. Saya tidak mengatakan bahwa itu ide yang baik untuk menggunakan ini, karena itu hanya akan sistem yang malloc-nya bekerja seperti ini, tapi paling tidak penting untuk mengetahui mengapa melakukan sesuatu yang salah mungkin berhasil.
kodai
Loki: Saya telah mengedit jawaban untuk digunakan, strcpy()bukan =; Saya berasumsi bahwa itulah niat Chris BC.
echristopherson
Saya percaya dalam platform modern perlindungan memori perangkat keras mencegah proses ruang pengguna menimpa ruang alamat proses lain; Anda akan mendapatkan kesalahan segmentasi. Tapi itu bukan bagian dari C.
echristopherson
4

Satu hal yang perlu diingat adalah selalu menginisialisasi pointer Anda ke NULL, karena pointer yang tidak diinisialisasi mungkin berisi alamat memori valid pseudorandom yang dapat membuat kesalahan pointer terus berlanjut secara diam-diam. Dengan memaksakan sebuah pointer untuk diinisialisasi dengan NULL, Anda selalu dapat menangkap jika Anda menggunakan pointer ini tanpa memulainya. Alasannya adalah bahwa sistem operasi "menghubungkan" alamat virtual 0x00000000 ke pengecualian perlindungan umum untuk menjebak penggunaan pointer nol.

Hernán
sumber
2

Anda juga mungkin ingin menggunakan alokasi memori dinamis ketika Anda perlu mendefinisikan array yang besar, katakanlah int [10000]. Anda tidak bisa begitu saja meletakkannya di tumpukan karena kemudian, hm ... Anda akan mendapatkan tumpukan melimpah.

Contoh bagus lainnya adalah implementasi struktur data, misalnya daftar tertaut atau pohon biner. Saya tidak memiliki kode contoh untuk ditempel di sini, tetapi Anda dapat menggunakan Google dengan mudah.

Serge
sumber
2

(Saya menulis karena saya merasa jawabannya sejauh ini belum tepat.)

Alasan Anda memiliki manajemen memori yang layak disebut adalah ketika Anda memiliki masalah / solusi yang mengharuskan Anda membuat struktur yang kompleks. (Jika program Anda macet jika Anda mengalokasikan ke banyak ruang pada tumpukan sekaligus, itu bug.) Biasanya, struktur data pertama yang perlu Anda pelajari adalah semacam daftar . Inilah satu yang terhubung, di luar kepala saya:

typedef struct listelem { struct listelem *next; void *data;} listelem;

listelem * create(void * data)
{
   listelem *p = calloc(1, sizeof(listelem));
   if(p) p->data = data;
   return p;
}

listelem * delete(listelem * p)
{
   listelem next = p->next;
   free(p);
   return next;
}

void deleteall(listelem * p)
{
  while(p) p = delete(p);
}

void foreach(listelem * p, void (*fun)(void *data) )
{
  for( ; p != NULL; p = p->next) fun(p->data);
}

listelem * merge(listelem *p, listelem *q)
{
  while(p != NULL && p->next != NULL) p = p->next;
  if(p) {
    p->next = q;
    return p;
  } else
    return q;
}

Secara alami, Anda menginginkan beberapa fungsi lain, tetapi pada dasarnya, inilah yang Anda butuhkan untuk manajemen memori. Saya harus menunjukkan bahwa ada sejumlah trik yang mungkin dilakukan dengan manajemen memori "manual", misalnya,

  • Menggunakan fakta malloc itu dijamin (oleh standar bahasa) untuk mengembalikan pointer yang habis dibagi 4,
  • mengalokasikan ruang ekstra untuk tujuan jahat Anda sendiri,
  • membuat kumpulan memori s ..

Dapatkan debugger yang bagus ... Semoga berhasil!

Anders Eurenius
sumber
Mempelajari struktur data adalah langkah kunci berikutnya dalam memahami manajemen memori. Mempelajari algoritme untuk menjalankan struktur ini dengan tepat akan menunjukkan kepada Anda metode yang tepat untuk mengatasi hambatan ini. Inilah sebabnya mengapa Anda akan menemukan struktur Data dan Algoritma yang diajarkan di kursus yang sama.
aj.toulan
0

@ Euro Micelli

Satu negatif untuk ditambahkan adalah bahwa pointer ke tumpukan tidak lagi valid ketika fungsi tersebut kembali, jadi Anda tidak dapat mengembalikan pointer ke variabel tumpukan dari suatu fungsi. Ini adalah kesalahan umum dan alasan utama mengapa Anda tidak dapat bertahan hanya dengan menumpuk variabel. Jika fungsi Anda perlu mengembalikan pointer, maka Anda harus malloc dan berurusan dengan manajemen memori.

Jonathan Branam
sumber
0

@ Ted Percival :
... Anda tidak perlu menggunakan nilai pengembalian malloc ().

Anda benar, tentu saja. Saya percaya itu selalu benar, meskipun saya tidak memiliki salinan K&R untuk diperiksa.

Saya tidak suka banyak konversi implisit di C, jadi saya cenderung menggunakan gips untuk membuat "sihir" lebih terlihat. Terkadang membantu keterbacaan, terkadang tidak, dan terkadang menyebabkan bug diam ditangkap oleh kompilator. Tetap saja, saya tidak memiliki pendapat yang kuat tentang ini, dengan satu atau lain cara.

Ini sangat mungkin terjadi jika kompiler Anda memahami komentar bergaya C ++.

Ya ... Anda menangkap saya di sana. Saya menghabiskan lebih banyak waktu di C ++ daripada C. Terima kasih telah memperhatikannya.

Euro Micelli
sumber
@echristopherson, terima kasih. Anda benar - tapi harap dicatat bahwa Tanya Jawab ini dari Agustus 2008, sebelum Stack Overflow bahkan dalam versi Beta publik. Saat itu, kami masih memikirkan cara kerja situs tersebut. Format Tanya / Jawab ini tidak harus dilihat sebagai model bagaimana menggunakan SO. Terima kasih!
Euro Micelli
Ah, terima kasih telah menunjukkan hal itu - saya tidak menyadari bahwa aspek situs masih berubah saat itu.
echristopherson
0

Di C, Anda sebenarnya memiliki dua pilihan berbeda. Pertama, Anda dapat membiarkan sistem mengelola memori untuk Anda. Atau, Anda bisa melakukannya sendiri. Umumnya, Anda ingin tetap berpegang pada yang pertama selama mungkin. Namun, memori yang dikelola secara otomatis di C sangat terbatas dan Anda perlu mengelola memori secara manual dalam banyak kasus, seperti:

Sebuah. Anda ingin variabel hidup lebih lama dari fungsinya, dan Anda tidak ingin memiliki variabel global. ex:

pasangan struct {
   int val;
   pasangan struct * berikutnya;
}

pasangan struct * pasangan_baru (int val) {
   pasangan struct * np = malloc (sizeof (pasangan struct));
   np-> val = val;
   np-> berikutnya = NULL;
   kembali np;
}

b. Anda ingin memiliki memori yang dialokasikan secara dinamis. Contoh paling umum adalah array tanpa panjang tetap:

int * my_special_array;
my_special_array = malloc (sizeof (int) * number_of_element);
untuk (i = 0; i

c. Anda ingin melakukan sesuatu yang BENAR-BENAR kotor. Misalnya, saya ingin sebuah struct untuk mewakili berbagai jenis data dan saya tidak suka union (union terlihat sangat berantakan):

data struct { int data_type; data_in_mem panjang; }; struct hewan {/ * sesuatu * /}; struct person {/ * beberapa hal lain * /}; struct hewan * read_animal (); struct person * read_person (); / * Di utama * / contoh data struct; sampe.data_type = input_type; switch (input_type) { kasus DATA_PERSON: sample.data_in_mem = read_person (); istirahat; kasus DATA_ANIMAL: sample.data_in_mem = read_animal (); default: printf ("Oh hoh! Saya peringatkan Anda, itu lagi dan saya akan seg kesalahan OS Anda"); }

Lihat, nilai panjang sudah cukup untuk menampung APA SAJA. Ingatlah untuk membebaskannya, atau Anda AKAN menyesal. Ini adalah salah satu trik favorit saya untuk bersenang-senang di C: D.

Namun, secara umum, Anda ingin menghindari trik favorit Anda (T___T). Anda AKAN merusak OS Anda, cepat atau lambat, jika Anda menggunakannya terlalu sering. Selama Anda tidak menggunakan * alok dan gratis, dapat dikatakan bahwa Anda masih perawan, dan kodenya masih terlihat bagus.

sihir
sumber
"Lihat, nilai panjang cukup untuk menampung APA SAJA" -: / apa yang Anda bicarakan, pada kebanyakan sistem, nilai panjang adalah 4 byte, persis sama dengan int. Satu-satunya alasan itu cocok dengan pointer di sini adalah karena ukuran panjang kebetulan sama dengan ukuran pointer. Anda seharusnya benar-benar menggunakan void *.
Skor_di bawah
-2

Tentu. Jika Anda membuat objek yang ada di luar cakupan tempat Anda menggunakannya. Berikut adalah contoh yang dibuat-buat (ingat sintaks saya akan mati; C saya berkarat, tetapi contoh ini masih akan menggambarkan konsepnya):

class MyClass
{
   SomeOtherClass *myObject;

   public MyClass()
   {
      //The object is created when the class is constructed
      myObject = (SomeOtherClass*)malloc(sizeof(myObject));
   }

   public ~MyClass()
   {
      //The class is destructed
      //If you don't free the object here, you leak memory
      free(myObject);
   }

   public void SomeMemberFunction()
   {
      //Some use of the object
      myObject->SomeOperation();
   }


};

Dalam contoh ini, saya menggunakan objek berjenis SomeOtherClass selama masa pakai MyClass. Objek SomeOtherClass digunakan dalam beberapa fungsi, jadi saya telah mengalokasikan memori secara dinamis: objek SomeOtherClass dibuat saat MyClass dibuat, digunakan beberapa kali selama masa pakai objek, dan kemudian dibebaskan setelah MyClass dibebaskan.

Jelas jika ini adalah kode nyata, tidak akan ada alasan (selain dari kemungkinan konsumsi memori tumpukan) untuk membuat myObject dengan cara ini, tetapi jenis pembuatan / penghancuran objek ini menjadi berguna ketika Anda memiliki banyak objek, dan ingin mengontrol dengan cermat ketika dibuat dan dihancurkan (sehingga aplikasi Anda tidak menyedot 1GB RAM untuk seluruh masa pakainya, misalnya), dan dalam lingkungan Windowed, ini cukup wajib, sebagai objek yang Anda buat (tombol, katakanlah) , harus ada dengan baik di luar cakupan fungsi tertentu (atau bahkan kelas ').

TheSmurf
sumber
1
Heh, ya, itu C ++ bukan? Luar biasa bahwa butuh lima bulan bagi siapa pun untuk menelepon saya.
TheSmurf