В C malloc()
виділяє область пам’яті в купу і повертає на неї вказівник. Це все, що ви отримуєте. Пам'ять неініціалізована, і ви не маєте гарантії, що це все нулі чи щось інше.
У Java виклик new
робить розподіл на основі купи так само malloc()
, але ви також отримуєте тону додаткової зручності (або накладні витрати, якщо хочете). Наприклад, не потрібно чітко вказувати кількість байтів, які потрібно виділити. Компілятор розраховує це для вас на основі типу об'єкта, який ви намагаєтеся виділити. Крім того, викликаються конструктори об'єктів (яким ви можете передавати аргументи, якщо ви хочете контролювати, як відбувається ініціалізація). Після new
повернення вам гарантовано буде об’єкт, який ініціалізується.
Але так, наприкінці дзвінка і результат, malloc()
і new
просто вказівки на деякий фрагмент даних на основі купи.
Друга частина вашого запитання задає питання про відмінності між стеком і групою. Набагато вичерпніші відповіді можна знайти, взявши курс (або прочитавши книгу про) проекту компілятора. Курс з операційних систем також буде корисним. Також є численні запитання та відповіді на SO про стеки та купи.
Сказавши це, я дам загальний огляд, сподіваюся, не надто багатослівний і спрямований на пояснення відмінностей на досить високому рівні.
По суті, основна причина наявності двох систем управління пам'яттю, тобто купа та стека, - це ефективність . Вторинна причина полягає в тому, що кожен краще вирішує певні типи проблем, ніж інші.
Стеки мені дещо простіше зрозуміти як поняття, тому я починаю з стеків. Розглянемо цю функцію в C ...
int add(int lhs, int rhs) {
int result = lhs + rhs;
return result;
}
Сказане здається досить відвертим. Визначимо функцію з ім'ям add()
і передаємо в лівому і правому додаваннях. Функція додає їх і повертає результат. Будь ласка, ігноруйте всі кращі регістри, такі як переповнення, які можуть виникнути, і на даний момент це не є предметом обговорення.
Призначення add()
функції здається досить простим, але що ми можемо сказати про його життєвий цикл? Особливо потребує використання пам'яті?
Найголовніше, що компілятор апріорі знає (тобто під час компіляції), наскільки великі типи даних і скільки буде використано. lhs
І rhs
аргументи sizeof(int)
, 4 байта кожен. Змінна result
також sizeof(int)
. Компілятор може сказати, що add()
функція використовує 4 bytes * 3 ints
або загалом 12 байт пам'яті.
Коли add()
функція викликається, апаратний регістр під назвою покажчик стека матиме в ній адресу, яка вказує на верхню частину стека. Для виділення пам'яті, яку add()
функція повинна запускати, все, що потрібно ввести код функції, - це видати одну єдину інструкцію мови збірки, щоб зменшити значення регістру вказівника стека на 12. При цьому він створює сховище в стеку на три ints
, по одному для кожного lhs
, rhs
і result
. Отримати необхідний простір пам’яті, виконавши одну інструкцію, - це величезна виграш у швидкості, тому що одні інструкції, як правило, виконуються за один тактовий годинник (1 мільярд секунди 1 процесор 1 ГГц).
Також, з точки зору компілятора, він може створити карту змінних, яка виглядає жахливо, як індексація масиву:
lhs: ((int *)stack_pointer_register)[0]
rhs: ((int *)stack_pointer_register)[1]
result: ((int *)stack_pointer_register)[2]
Знову ж таки, все це дуже швидко.
Коли add()
функція закінчується, вона повинна очищатись. Це робиться шляхом віднімання 12 байтів з регістра вказівника стека. Це схоже на дзвінок, free()
але він використовує лише одну інструкцію процесора, і він займає лише одну галочку. Це дуже, дуже швидко.
Тепер розглянемо розподіл на основі купи. Це грає, коли ми апріорі не знаємо, скільки пам'яті нам знадобиться (тобто ми дізнаємося про це лише під час виконання).
Розглянемо цю функцію:
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;
}
Зауважте, що addRandom()
функція не знає під час компіляції, яким буде значення count
аргументу. Через це не має сенсу намагатися визначитись так, array
як ми, якби ставити його на стек, як це:
int array[count];
Якщо count
він величезний, це може призвести до того, що наш стек стає занадто великим і замінить інші сегменти програми. Коли ця переповнення стека трапиться, програма завершиться (або гірше).
Тож у тих випадках, коли ми не знаємо, скільки пам'яті нам знадобиться до часу виконання, ми використовуємо malloc()
. Тоді ми можемо просто запитати кількість потрібних нам байтів, коли нам це потрібно, і malloc()
перевіримо, чи може він продати стільки байтів. Якщо це можливо, чудово, ми повертаємо його назад, якщо ні, то отримуємо вказівник NULL, який повідомляє нам, що виклик malloc()
не вдався. Примітно, що програма не виходить з ладу! Звичайно, ви як програміст можете вирішити, що ваша програма не може працювати, якщо розподіл ресурсів не вдається, але ініційоване програмуванням припинення відрізняється від помилкової аварії.
Тож тепер ми повинні повернутися, щоб подивитися на ефективність. Розподільник стеків дуже швидкий - одна інструкція розподілити, одна інструкція розібрати, і це робиться компілятором, але пам’ятайте, що стек призначений для таких речей, як локальні змінні відомого розміру, тому він, як правило, досить малий.
З іншого боку, розподільник купи на кілька порядків повільніше. Він повинен здійснити пошук у таблицях, щоб побачити, чи є у нього достатньо вільної пам'яті, щоб можна було продати об'єм пам'яті, який хоче користувач. Після оновлення пам'яті він повинен оновити ці таблиці, щоб переконатися, що ніхто більше не може використовувати цей блок (ця бухгалтерія може зажадати від розподільника резервувати пам'ять для себе на додаток до того, що планує продавати). Алокатор повинен використовувати стратегії блокування, щоб переконатися, що він поширює пам'ять безпечним способом. І коли нарешті пам’ятьfree()
d, що відбувається в різний час і в непередбачуваному порядку, як правило, алокатор повинен знаходити суміжні блоки і зшивати їх назад, щоб виправити фрагмент купи. Якщо це здається, що для виконання всього цього знадобиться більше однієї інструкції процесора, ви маєте рацію! Це дуже складно і потребує певного часу.
Але купи великі. Набагато більше, ніж стеки. Ми можемо отримати від них багато пам’яті, і вони чудові, коли ми не знаємо, коли компілювати, скільки пам’яті нам знадобиться. Таким чином, ми торгуємо швидкістю для керованої системи пам’яті, яка ввічливо відхиляє нас, а не дає збоїв, коли ми намагаємося виділити щось занадто велике.
Я сподіваюся, що це допоможе відповісти на деякі ваші запитання. Будь ласка, дайте мені знати, чи хочете ви пояснити щось із вищезазначеного.