Один аргумент free(void *)
(представлений в Unix V7) має ще одну велику перевагу перед попереднім двоаргументом, про mfree(void *, size_t)
який я не бачив тут: один аргумент free
різко спрощує кожен інший API, який працює з кучевою пам'яттю. Наприклад, якщо free
потрібен розмір блоку пам'яті, тоді strdup
якимось чином доведеться повертати два значення (покажчик + розмір) замість одного (покажчик), а C робить багатозначні повернення набагато громіздкішими, ніж однозначні. Замість того char *strdup(char *)
, щоб писати, char *strdup(char *, size_t *)
або ще struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
. (На сьогоднішній день цей другий варіант виглядає досить спокусливо, тому що ми знаємо, що закінчувані NUL рядки - це "найбільш катастрофічна помилка дизайну в історії обчислень", але це зворотний погляд. Ще в 70-х роках здатність С обробляти рядки як просту char *
насправді вважалася визначальною перевагою перед конкурентами, такими як Паскаль та Алгол .) Крім того, strdup
ця проблема страждає не тільки - вона впливає на кожну визначену системою чи користувачем функція, яка виділяє купу пам'яті.
Ранні дизайнери Unix були дуже кмітливими людьми, і є багато причин, чому free
це краще, ніж mfree
це, в основному, я думаю, що відповідь на питання полягає в тому, що вони це помітили і відповідно розробили свою систему. Сумніваюся, ви знайдете якийсь прямий запис того, що відбувалося в їхніх головах на момент прийняття ними такого рішення. Але ми можемо уявити.
Робіть вигляд, що пишете програми на C для запуску на V6 Unix з двома аргументами mfree
. Наразі у вас все вийшло, але відстеження цих розмірів покажчиків стає дедалі більше клопотом, оскільки ваші програми стають більш амбіційними та вимагають дедалі більше використання змінних, призначених для купи. Але тоді у вас є блискуча ідея: замість того, щоб постійно копіювати навколо них size_t
, ви можете просто написати деякі утилітні функції, які зберігають розмір безпосередньо у виділеній пам'яті:
void *my_alloc(size_t size) {
void *block = malloc(sizeof(size) + size);
*(size_t *)block = size;
return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
block = (size_t *)block - 1;
mfree(block, *(size_t *)block);
}
І чим більше коду ви пишете, використовуючи ці нові функції, тим вони неймовірнішими. Вони не тільки полегшують написання вашого коду, але й роблять його швидшим - дві речі, які часто не поєднуються! До того, як ви передавали ці size_t
s повсюди, що додало накладні витрати на процесор для копіювання, і це означало, що вам доводилося частіше розливати регістри (зокрема, додаткові аргументи функції), а також марну пам'ять (оскільки вкладені виклики функцій часто призводять до у декількох копіях того, size_t
що зберігається в різних кадрах стека). У вашій новій системі вам все одно доведеться витратити пам'ять для зберіганняsize_t
, але лише один раз, і він ніколи ніде не копіюється. Це може здатися невеликою ефективністю, але майте на увазі, що мова йде про висококласні машини з 256 КБ оперативної пам'яті.
Це вас радує! Тож ви ділитеся своїм крутим трюком з бородатими чоловіками, які працюють над наступним випуском Unix, але це не радує їх, а засмучує. Розумієте, вони якраз збиралися додати купу нових функцій, таких як утиліта strdup
, і вони розуміють, що люди, які використовують ваш крутий трюк, не зможуть використовувати свої нові функції, оскільки всі їхні нові функції використовують громіздкий покажчик + розмір API. І тоді це також засмучує вас, бо ви розумієте, що вам доведеться переписувати хорошу strdup(char *)
функцію самостійно у кожній написаній вами програмі, замість того, щоб мати можливість використовувати версію системи.
Але почекай! Це 1977 рік, і зворотна сумісність не буде винайдена ще протягом 5 років! І крім того, ніхто серйозно насправді не використовує цю незрозумілу річ "Unix" з її нефарбовою назвою. Перше видання K&R вже зараз на шляху до видавця, але це не проблема - прямо на першій сторінці сказано, що "C не надає жодних операцій для безпосередньої роботи з складеними об'єктами, такими як рядки символів ... немає купи ... ". На даний момент в історії є string.h
і malloc
розширення постачальників (!). Отже, припускає Бородатий Чоловік №1, ми можемо змінювати їх як завгодно; чому ми просто не оголосимо ваш хитрий розподільник офіційним розподільником?
Кілька днів потому Бородатий Чоловік №2 бачить новий API і каже, пристосуйтесь, це краще, ніж раніше, але він все одно витрачає ціле слово на розподіл, зберігаючи розмір. Він розглядає це як наступну річ для блюзнірства. Всі інші дивляться на нього, як на божевільного, бо що ти ще можеш зробити? Тієї ночі він затримується до кінця і вигадує новий розподільник, який взагалі не зберігає розмір, а натомість виводить його на льоту, виконуючи бітову зміну чорної магії на значення покажчика, і міняє місцями, зберігаючи новий API на місці. Новий API означає, що ніхто не помічає перемикач, але вони помічають, що наступного ранку компілятор використовує на 10% менше оперативної пам'яті.
І тепер усі раді: Ви отримуєте свій простіший для написання та швидший код, Бородатий Чоловік №1 пише приємний простий, strdup
яким люди насправді користуватимуться, а Бородатий Чоловік №2 - впевнений, що трохи заробив - - повертається до базікання з quines . Відправте!
Або, принаймні, так могло статися.