Что ((size_t *) ptr) [- 1] означает в C? - PullRequest
0 голосов
/ 14 декабря 2018

Я хочу знать размер, выделенный указателю.

Итак, я нашел ответ: как узнать размер выделенной памяти переменной указателя в c

И он имеет следующий код.

#include <stdlib.h>
#include <stdio.h>

void * my_malloc(size_t s) 
{
  size_t * ret = malloc(sizeof(size_t) + s);
  *ret = s;
  return &ret[1];
}

void my_free(void * ptr) 
{
  free( (size_t*)ptr - 1);
}

size_t allocated_size(void * ptr) 
{
  return ((size_t*)ptr)[-1];
}

int main(int argc, const char ** argv) 
{
  int * array = my_malloc(sizeof(int) * 3);
  printf("%u\n", allocated_size(array));
  my_free(array);
  return 0;
}

Линия (((size_t*)ptr)[-1]) работает отлично, но я не понимаю, почему ...

Может кто-нибудь помочь мне понять эту магическую линию?Спасибо!

Ответы [ 5 ]

0 голосов
/ 14 декабря 2018

Сначала давайте объясним, что делает (((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.

0 голосов
/ 14 декабря 2018

Во-первых, давайте начнем с того, что означает ((size_t*)ptr)[-1].

Когда вы используете оператор индекса массива как (например) A[B], это в точности эквивалентно *(A + B).То, что здесь действительно происходит, это арифметика с указателями, за которой следует разыменование.Это означает, что наличие отрицательного индекса массива допустимо, при условии, что рассматриваемый указатель не указывает на первый элемент массива.

Например:

int a[5] = { 1, 2, 3, 4, 5 };
int *p = a + 2;
printf("p[0] = %d\n", p[0]);      // prints 3
printf("p[-1] = %d\n", p[-1]);    // prints 2
printf("p[-2] = %d\n", p[-2]);    // prints 1

Таким образом, применяя это к ((size_t*)ptr)[-1], это говорит о том, что ptr указывает на элемент массива из одного или нескольких объектов типа size_t (или на один элемент после конца массива), ииндекс -1 получает объект непосредственно перед , на который указывает ptr.

Теперь, что это означает в контексте примера программы?

Функция my_malloc - это обертка вокруг malloc, которая выделяет s байтов плюс байтов, достаточных для size_t.Он записывает значение s в начале буфера malloc'а как size_t, затем возвращает указатель на память после объекта size_t.

Таким образом, фактически выделенная память и возвращенный указатель выглядят примерно так (при условии sizeof(size_t) is 8):

        -----
0x80    | s |
0x81    | s |
0x82    | s |
0x83    | s |
0x84    | s |
0x85    | s |
0x86    | s |
0x87    | s |
0x88    |   |   <--- ptr
0x89    |   |
0x8A    |   |
...

Когда указатель, возвращенный из my_malloc, передается в allocated_size,Функция может прочитать запрошенный размер буфера с помощью ((size_t*)ptr)[-1]:

        -----
0x80    | s |   <--- ptr[-1]
0x81    | s |
0x82    | s |
0x83    | s |
0x84    | s |
0x85    | s |
0x86    | s |
0x87    | s |
0x88    |   |   <--- ptr[0]
0x89    |   |
0x8A    |   |

Приведенный ptr указывает на один элемент после массива size_t размера 1, поэтому сам указатель действителен ивпоследствии получение объекта с индексом массива -1 также допустимо. Это не неопределенное поведение, как другие предлагали, так как указатель конвертируется в / из void * и указывает на действительный объектуказанный тип.

В этой реализации перед возвращаемым указателем сохраняется только размер запрошенного буфера, однако вы можете хранить там больше метаданных при условии, что для него выделено достаточно дополнительного пространства.

единственное, что не учитывается, это то, что память, возвращаемая malloc, соответствующим образом выравнивается для любых целей, а указатель возвращается bу my_malloc может не соответствовать этому требованию.Таким образом, объект, помещенный по возвращенному адресу, может иметь проблему с выравниванием и вызвать сбой.Чтобы учесть это, дополнительные байты должны быть выделены для соответствия этому требованию, и allocated_size и my_free также должны быть скорректированы для учета этого.

0 голосов
/ 14 декабря 2018

Если ptr указывает на блок памяти, выделенный malloc, calloc, realloc и т. Д., То (((size_t*)ptr)[-1] вызывает неопределенное поведение .Я предполагаю, что это зависит от поведения стандартной библиотеки, реализованной неким случайным поставщиком, которая хранит размер блока памяти в местоположении непосредственно перед местоположением, возвращаемым malloc и т. Д.

DOНЕ ИСПОЛЬЗУЙТЕ ТАКИЕ ХАКИ!Если программа выделяет память динамически, она должна иметь возможность отслеживать размеры выделяемой памяти, не полагаясь на неопределенное поведение.

Размер блока памяти, фактически выделенного с помощью malloc и т. Д., Может бытьбольше, чем запрашиваемый размер, поэтому, возможно, вам интересно узнать фактический размер выделенного блока, включая избыточную память в конце блока.Переносимый код не должен знать об этом, поскольку доступ к местоположениям, превышающим запрошенный размер, также неопределенное поведение , но, возможно, вы хотите знать этот размер для любопытства или для целей отладки.

0 голосов
/ 14 декабря 2018

Это на самом деле очень плохой код, который вызывает UB.

Если он хочет сохранить размер выделенного пространства, он должен использовать структуру, где первое поле - это размер, а второй массив нулевого размера (или vla) для фактических данных

0 голосов
/ 14 декабря 2018

Похоже, что реализация вашего компилятора C malloc сохраняет выделенный размер (в байтах) в 4 байтах непосредственно перед возвращаемым адресом.

Путем преобразования возвращенного адреса (ptr) в указатель-to- size_t (это ((size_t*)ptr)), а затем взять выровненный адрес прямо перед ним (это '[-1]', который на самом деле является просто арифметикой указателя - так же, как и запись *(((size_t*)ptr) - 1)) - вы можете получить доступвыделенный размер (типа size_t).

Это объясняет, что означает ((size_t*)ptr)[-1] и почему он работает, но это ни в коем случае не является рекомендацией по его использованию.Получение размера, выделенного для указателя, было величиной, запрашиваемой кодом приложения, и оно должно управляться им при необходимости, не полагаясь на реализации компилятора.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...