Di C, malloc()
mengalokasikan wilayah memori di heap dan mengembalikan pointer ke sana. Hanya itu yang Anda dapatkan. Memori tidak diinisialisasi dan Anda tidak memiliki jaminan bahwa semuanya nol atau apa pun.
Di Jawa, panggilan new
tidak seperti alokasi heap malloc()
, tetapi Anda juga mendapatkan banyak kenyamanan tambahan (atau overhead, jika Anda mau). Misalnya, Anda tidak harus secara eksplisit menentukan jumlah byte yang akan dialokasikan. Compiler menghitungnya untuk Anda berdasarkan pada jenis objek yang Anda coba alokasikan. Selain itu, konstruktor objek dipanggil (yang dapat Anda berikan argumen jika Anda ingin mengontrol bagaimana inisialisasi terjadi). Ketika new
kembali, Anda dijamin memiliki objek yang diinisialisasi.
Tapi ya, pada akhir panggilan baik hasil malloc()
dan new
hanya petunjuk ke beberapa data berbasis heap.
Bagian kedua dari pertanyaan Anda menanyakan tentang perbedaan antara tumpukan dan tumpukan. Jawaban yang jauh lebih komprehensif dapat ditemukan dengan mengikuti kursus tentang (atau membaca buku tentang) desain kompiler. Kursus tentang sistem operasi juga akan sangat membantu. Ada juga banyak pertanyaan dan jawaban di SO tentang tumpukan dan tumpukan.
Karena itu, saya akan memberikan gambaran umum saya harap tidak terlalu bertele-tele dan bertujuan untuk menjelaskan perbedaan pada tingkat yang cukup tinggi.
Pada dasarnya, alasan utama untuk memiliki dua sistem manajemen memori, yaitu tumpukan dan tumpukan, adalah untuk efisiensi . Alasan kedua adalah bahwa masing-masing lebih baik pada jenis masalah tertentu daripada yang lain.
Tumpukan agak lebih mudah bagi saya untuk dipahami sebagai konsep, jadi saya mulai dengan tumpukan. Mari kita pertimbangkan fungsi ini di C ...
int add(int lhs, int rhs) {
int result = lhs + rhs;
return result;
}
Di atas tampaknya cukup mudah. Kami mendefinisikan fungsi bernama add()
dan meneruskan di addend kiri dan kanan. Fungsi menambahkannya dan mengembalikan hasilnya. Harap abaikan semua hal tepi-kasus seperti luapan yang mungkin terjadi, pada titik ini tidak berhubungan dengan diskusi.
Tujuan add()
fungsi ini tampaknya cukup mudah, tetapi apa yang bisa kita katakan tentang siklus hidupnya? Terutama kebutuhan pemanfaatan memorinya?
Yang paling penting, kompiler mengetahui apriori (yaitu pada waktu kompilasi) seberapa besar tipe data dan berapa banyak yang akan digunakan. The lhs
dan rhs
argumen yang sizeof(int)
, 4 byte masing-masing. Variabelnya result
juga sizeof(int)
. Kompiler dapat mengetahui bahwa add()
fungsi tersebut menggunakan 4 bytes * 3 ints
atau total 12 byte memori.
Ketika add()
fungsi dipanggil, register perangkat keras yang disebut penunjuk tumpukan akan memiliki alamat di dalamnya yang menunjuk ke bagian atas tumpukan. Untuk mengalokasikan memori yang add()
harus dijalankan fungsi, semua kode fungsi-entri perlu lakukan adalah mengeluarkan satu instruksi bahasa rakitan tunggal untuk mengurangi nilai register penunjuk tumpukan dengan 12. Dengan demikian, ia menciptakan penyimpanan pada tumpukan selama tiga ints
, masing-masing untuk lhs
, rhs
, dan result
. Mendapatkan ruang memori yang Anda butuhkan dengan mengeksekusi instruksi tunggal adalah kemenangan besar dalam hal kecepatan karena instruksi tunggal cenderung dieksekusi dalam satu clock tick (1 milyar detik, 1 CPU 1 GHz).
Selain itu, dari tampilan kompiler, ia dapat membuat peta ke variabel yang terlihat sangat buruk seperti mengindeks array:
lhs: ((int *)stack_pointer_register)[0]
rhs: ((int *)stack_pointer_register)[1]
result: ((int *)stack_pointer_register)[2]
Sekali lagi, semua ini sangat cepat.
Ketika add()
fungsi keluar harus dibersihkan. Ini dilakukan dengan mengurangi 12 byte dari register penunjuk tumpukan. Ini mirip dengan panggilan untuk free()
tetapi hanya menggunakan satu instruksi CPU dan hanya membutuhkan satu centang. Ini sangat, sangat cepat.
Sekarang pertimbangkan alokasi berbasis heap. Ini berlaku ketika kita tidak tahu apriori berapa banyak memori yang akan kita butuhkan (yaitu kita hanya akan mempelajarinya saat runtime).
Pertimbangkan fungsi ini:
int addRandom(int count) {
int numberOfBytesToAllocate = sizeof(int) * count;
int *array = malloc(numberOfBytesToAllocate);
int result = 0;
if array != NULL {
for (i = 0; i < count; ++i) {
array[i] = (int) random();
result += array[i];
}
free(array);
}
return result;
}
Perhatikan bahwa addRandom()
fungsi tidak tahu pada waktu kompilasi berapa nilai count
argumen itu. Karena itu, tidak masuk akal untuk mencoba mendefinisikan array
seperti yang kita lakukan jika kita meletakkannya di tumpukan, seperti ini:
int array[count];
Jika count
besar, ini dapat menyebabkan tumpukan kami tumbuh terlalu besar dan menimpa segmen program lainnya. Ketika stack overflow ini terjadi, program Anda mogok (atau lebih buruk).
Jadi, dalam kasus di mana kita tidak tahu berapa banyak memori yang akan kita butuhkan sampai runtime, kita gunakan malloc()
. Kemudian kita bisa menanyakan jumlah byte yang kita butuhkan saat kita membutuhkannya, dan malloc()
akan memeriksa apakah bisa byte sebanyak itu. Jika bisa, bagus, kami mendapatkannya kembali, jika tidak, kami mendapatkan pointer NULL yang memberi tahu kami bahwa panggilan malloc()
gagal. Khususnya, program ini tidak macet! Tentu saja Anda sebagai programmer dapat memutuskan bahwa program Anda tidak diizinkan berjalan jika alokasi sumber daya gagal, tetapi pemutusan yang diprogram oleh programmer berbeda dari crash palsu.
Jadi sekarang kita harus kembali untuk melihat efisiensi. Penyusun stack sangat cepat - satu instruksi untuk dialokasikan, satu instruksi untuk membatalkan alokasi, dan itu dilakukan oleh kompiler, tetapi ingat stack dimaksudkan untuk hal-hal seperti variabel lokal dari ukuran yang diketahui sehingga cenderung cukup kecil.
Pengalokasi tumpukan di sisi lain adalah beberapa pesanan lebih lambat lebih besar. Itu harus melakukan pencarian di tabel untuk melihat apakah ia memiliki cukup memori bebas untuk dapat memv jumlah memori yang diinginkan pengguna. Itu harus memperbarui tabel-tabel setelah vending memori untuk memastikan tidak ada orang lain yang dapat menggunakan blok itu (pembukuan ini mungkin memerlukan pengalokasi untuk mencadangkan memori untuk dirinya sendiri di samping apa yang ia rencanakan untuk dijual). Pengalokasi harus menggunakan strategi penguncian untuk memastikannya mengosongkan memori dengan cara yang aman. Dan ketika ingatan akhirnyafree()
d, yang terjadi pada waktu yang berbeda dan tanpa urutan yang dapat diprediksi biasanya, pengalokasi harus menemukan blok yang berdekatan dan menyatukannya kembali untuk memperbaiki fragmentasi timbunan. Jika kedengarannya seperti itu akan membutuhkan lebih dari satu instruksi CPU tunggal untuk menyelesaikan semua itu, Anda benar! Ini sangat rumit dan butuh beberapa saat.
Tapi tumpukan itu besar. Jauh lebih besar dari tumpukan. Kita bisa mendapatkan banyak memori dari mereka dan mereka hebat ketika kita tidak tahu pada waktu kompilasi berapa banyak memori yang kita butuhkan. Jadi, kami menukar kecepatan dengan sistem memori terkelola yang menolak kami dengan sopan alih-alih menabrak ketika kami mencoba mengalokasikan sesuatu yang terlalu besar.
Saya harap itu membantu menjawab beberapa pertanyaan Anda. Harap beri tahu saya jika Anda ingin klarifikasi pada salah satu di atas.