Сначала давайте объясним, что делает (((size_t*)ptr)[-1])
, предполагая, что оно действительно:
(size_t*)ptr
преобразует ptr
в тип «указатель на size_t
». ((size_t *)ptr)[-1]
по определению 1 эквивалентно *((size_t *) ptr - 1)
. 2 То есть он вычитает 1 из (size_t *) ptr
и «выводит» из результирующего указателя. - Арифметика указателей определяется в терминах элементов массива и рассматривает отдельный объект как массив из одного элемента. 2 Если
(size_t *) ptr
указывает «чуть выше» объекта size_t
, то *((size_t *) ptr - 1)
указывает на объект size_t
. - Таким образом,
(((size_t*)ptr)[-1])
является объектом size_t
, который находится непосредственно перед ptr
.
Теперь давайте обсудим,выражение допустимо.ptr
получается с помощью этого кода:
void * my_malloc(size_t s)
{
size_t * ret = malloc(sizeof(size_t) + s);
*ret = s;
return &ret[1];
}
Если malloc
завершается успешно, он выделяет пространство для любого объекта запрошенного размера. 4 Таким образом, мы, безусловно, можем хранить size_t
Там 5 , за исключением того, что этот код должен проверять возвращаемое значение для защиты от сбоя выделения.Кроме того, мы можем вернуть &ret[1]
:
&ret[1]
эквивалентно &*(ret + 1)
, что эквивалентно ret + 1
.Это указывает на единицу за size_t
, который мы сохранили в ret
, что является действительной арифметикой указателя. - Указатель преобразуется в тип возвращаемого значения функции,
void *
, который действителен. 5
Код, показанный в вопросе, делает только две вещи со значением, возвращаемым из my_malloc
: извлечь сохраненный размер с помощью ((size_t*)ptr)[-1]
и освободить пространство с помощью (size_t*)ptr - 1
.Оба они действительны, так как преобразование указателя является подходящим, и они работают в пределах арифметики указателя.
Однако, есть вопрос о том, к какому дальнейшему использованию может быть возвращено возвращаемое значение.Как уже отмечали другие, в то время как указатель, возвращаемый из malloc
, соответствующим образом выровнен для любого объекта, добавление size_t
создает указатель, который соответствующим образом выровнен только для объекта, требование выравнивания которого не является более строгим, чем size_t
.Например, во многих реализациях C это означало бы, что указатель не может использоваться для double
, который часто требует восьмибайтового выравнивания, в то время как size_t
составляет всего четыре байта.
Таким образом, мы сразу видим, чтоmy_malloc
не является полной заменой malloc
.Тем не менее, возможно, его можно использовать только для объектов с удовлетворительными требованиями к выравниванию.Давайте рассмотрим это.
Я думаю, что многие реализации C не будут иметь проблем с этим, но технически здесь есть проблема: malloc
указано для возврата пространства для одного объекта запрошенного размера.Этот объект может быть массивом, поэтому пространство можно использовать для нескольких объектов одного типа.Однако не указано, что пространство может использоваться для нескольких объектов разных типов.Таким образом, если какой-то объект, отличный от size_t
, хранится в пространстве, возвращаемом my_malloc
, я не вижу, чтобы стандарт C определял поведение.Как я уже отметил, это педантичное различие;Я не ожидаю, что реализация C будет иметь проблему с этим, хотя все более агрессивные оптимизации удивили меня на протяжении многих лет.
Один из способов сохранить несколько различных объектов в пространстве, возвращаемом malloc
, - это использоватьсостав.Тогда мы могли бы поставить int
или float
или char *
в пробел после size_t
.Однако мы не можем сделать это с помощью арифметики с указателями - использование арифметики с указателями для навигации по элементам структуры не полностью определено.Адресация членов структуры выполняется по имени, а не по манипуляциям с указателями.Поэтому возврат &ret[1]
из my_malloc
является недопустимым способом (определенным стандартом C) для предоставления указателя на пространство, которое можно использовать для любого объекта (даже если требование выравнивания удовлетворено).
Другие примечания
Этот код неправильно использует %u
для форматирования значения типа size_t
:
printf("%u\n", allocated_size(array));
Конкретный целочисленный тип size_t
определяется реализацией и может не соответствоватьunsigned
.Результирующее поведение не может быть определено стандартом C.Правильный спецификатор формата: %zu
.
Сноски
1 C 2018 6.5.2.1 2.
2 Точнее, это *((((size_t *) ptr)) + (-1))
, но они эквивалентны.
3 C 2018 6.5.6 8 и 9.
4 C 2018 7.22.3.4.
5 Очень педантичный читатель C 2018 7.22.3.4 может возразитьчто size_t
не является объектом запрошенного размера, но является объектом меньшего размера.Я не верю, что это подразумеваемое значение.
6 C 2018 6.3.2.3 1.