Один аргумент 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-х годах способность C обрабатывать строки как простуюchar *
фактически считалось определяющим преимуществом по сравнению с такими конкурентами, как Pascal и Algol .) Кроме того, strdup
эта проблема не только в нем , но и во всех системных или пользовательских функциях, которые выделяют динамическую память.
Первые разработчики Unix были очень умными людьми, и есть много причин, почему free
это лучше, чем mfree
это, в основном, я думаю, что ответ на вопрос состоит в том, что они заметили это и соответствующим образом спроектировали свою систему. Я сомневаюсь, что вы найдете какие-либо прямые записи о том, что происходило в их головах в тот момент, когда они приняли это решение. Но мы можем представить.
Представьте, что вы пишете приложения на C для работы в V6 Unix с его двумя аргументами mfree
. Пока у вас все хорошо, но отслеживание размеров этих указателей становится все труднее, поскольку ваши программы становятся все более амбициозными и требуют все большего и большего использования переменных, размещенных в куче. Но тогда у вас есть блестящая идея: вместо того, чтобы постоянно копировать эти size_t
s, вы можете просто написать несколько служебных функций, которые сохраняют размер непосредственно в выделенной памяти:
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
повсюду, что увеличивало нагрузку на ЦП для копирования и означало, что вам приходилось чаще проливать регистры (особенно для дополнительных аргументов функции) и тратить впустую память (поскольку вызовы вложенных функций часто приводят к в нескольких копиях 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 - уверен, что он немного заработал на себе - - возвращается к возням с лебедой . Отправим его!
По крайней мере, так могло случиться.