Подтвердить что ты не робот

Правильно ли используется указатель, указывающий на one-past-malloc?

В C отлично сделать указатель, указывающий на один последний элемент массива и использовать его в арифметике указателя, если вы не разыщите его:

int a[5], *p = a+5, diff = p-a; // Well-defined

Однако это UB:

p = a+6;
int b = *(a+5), diff = p-a; // Dereferencing and pointer arithmetic

Теперь у меня есть вопрос: относится ли это к динамически распределенной памяти? Предположим, что я использую указатель, указывающий на одно из последних в арифметике указателя, без разыменования его, и malloc() преуспевает.

int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");
// Question:
int *p = a+5;
int diff = p-a; // Use in pointer arithmetic?
4b9b3361

Ответ 1

Хорошо ли используется указатель, указывающий на one-past-malloc?

Хорошо определено, если p указывает на одно прошлое выделенной памяти, и оно не разыменовывается.

n1570 - §6.5.6 (p8):

[...] Если результат указывает один за последний элемент объекта массива, он не должен использоваться как операнд унарного оператора *, который оценивается.

Вычитание двух указателей допустимо только тогда, когда они указывают на элементы одного и того же объекта массива или один за последним элементом объекта массива, иначе это приведет к поведению undefined.

(p9):

Когда два указателя вычитаются, оба должны указывать на элементы одного и того же объекта массива или один за последним элементом объекта массива [...]

Вышеприведенные цитаты хорошо применимы как для динамически, так и для статически распределенной памяти.

int a[5];
ptrdiff_t diff = &a[5] - &a[0]; // Well-defined

int *d = malloc(5 * sizeof(*d));
assert(d != NULL, "Memory allocation failed");
diff = &d[5] - &d[0];        // Well-defined

Другая причина, по которой это справедливо для динамически распределенной памяти, как указано Джонатаном Леффлером в comment есть:

§7.22.3 (p1):

Порядок и смежность хранилища, выделенные последовательными вызовами функций aligned_alloc, calloc, malloc и realloc, не определены. Указатель возвращается, если выделение успешно выполняется соответствующим образом, чтобы его можно было назначить указателю на любой тип объекта с фундаментальным требованием к выравниванию, а затем использовать для доступа к такому объекту или массив таких объектов в выделенном пространстве (пока пространство явно не освобождено).

Указатель, возвращенный malloc в приведенном выше фрагменте, присваивается d, а выделенная память представляет собой массив из 5 int объектов.

Ответ 2

Проект n4296 для C11 является явным, что определение одного из прошедших массивов четко определено: 6.5.6 Язык/выражения/Аддитивные операторы:

§ 8 Когда выражение, которое имеет целочисленный тип, добавляется или вычитается из указателя, result имеет тип операнда указателя.... Кроме того, если выражение P указывает на последнее элемент объекта массива, выражение (P) +1 указывает один за последним элементом массив, и если выражение Q указывает один за последним элементом объекта массива, выражение (Q) -1 указывает на последний элемент объекта массива... Если результат указывает один за последний элемент объекта массива, он не должен использоваться в качестве операнда унарного * оператора, который оценивается.

Поскольку тип памяти никогда не уточняется в подпункте, он применяется к любому типу памяти, включая выделенную.

Это ясно означает, что после:

int *a = malloc(5 * sizeof(*a));
assert(a != NULL, "Memory allocation failed");

оба

int *p = a+5;
int diff = p-a;

отлично определены и, как применяются обычные правила арифметики указателя, diff получает значение 5.

Ответ 3

Да, те же правила применяются к переменным с динамической и автоматической продолжительностью хранения. Это относится даже к запросу malloc для одного элемента (скаляр эквивалентен одноэлементному массиву в этом отношении).

Арифметика указателя действительна только в массивах, включая один конец конца массива.

При разыменовании важно отметить одно соображение: относительно инициализации int a[5] = {0}; компилятор не должен пытаться разыменовать a[5] в выражении int* p = &a[5]; он должен скомпилировать это как int* p = a + 5;. То же самое относится и к динамическому хранилищу.

Ответ 4

Хорошо ли используется указатель, указывающий на one-past-malloc?

Да, но есть угловой случай, когда это не четко определено:

void foo(size_t n) {
  int *a = malloc(n * sizeof *a);
  assert(a != NULL || n == 0, "Memory allocation failed");
  int *p = a+n;
  intptr_t diff = p-a;
  ...
}

Функции управления памятью. Если размер запрашиваемого пространства равен нулю, поведение определяется реализацией: возвращается нулевой указатель или поведение такое, как если бы размер был некоторым ненулевое значение, за исключением того, что возвращаемый указатель не должен использоваться для доступа к объекту. C11dr §7.22.3 1

foo(0)malloc(0) может возвращать NULL или non-NULL. В первой реализации возвращение NULL не является "Ошибка выделения памяти". Это означает, что код пытается int *p = NULL + 0; с int *p = a+n;, который не дает гарантий относительно математики указателя - или, по крайней мере, приводит к возникновению такого кода.

Преимущества портативного кода, избегая 0 распределений по размеру.

void bar(size_t n) {
  intptr_t diff;
  int *a;
  int *p;
  if (n > 0) {
    a = malloc(n * sizeof *a);
    assert(a != NULL, "Memory allocation failed");
    p = a+n;
    diff = p-a;
  } else {
    a = p = NULL;
    diff = 0;
  }
  ...
}

Ответ 5

Я обнаружил, что в C в Windows на чипе Intel X86 я могу иметь недопустимое значение в указателе и не вызывать GPF, если не разыскиваю указатель. В C в Unix на процессоре HP 68000 в программе будет дамп ядра, если указатель имел недопустимое значение, даже если оно не было разыменовано. (Хотя я признаю, что это было в 1990-х годах). С тех пор у меня была привычка не позволять указателю идти за конец массива. Кроме того, я часто делаю массивы немного большими по нескольким элементам, чтобы избежать того, чтобы моя программа взорвалась или что-то еще не привело к тому, что отверстие для защиты от переполнения буфера. Легко отключиться одним элементом. Не допускайте, чтобы ошибка была ошибкой, которая разбивает вашу программу (или получает вашу компанию в новостях) ради сохранения нескольких байтов. Поскольку ответ несколько зависит от процессора, я рекомендую не делать этого.

Обновление:. Прочитав несколько комментариев, я провел несколько исследований и два механизма, которые приводят к сбою программы с недопустимым указателем, выпавшим из края массива, даже если вы не разыменовали Недопустимый указатель умозрительное выполнение и предсказание ветвей. В принципе, процессор (даже современный процессор) иногда пытается обрабатывать сразу две команды, иногда отбрасывая результаты второй команды из-за результатов первой команды. В случае цикла первая команда цикла выполняется снова, даже если последняя команда цикла была ветвью, которая решила не зацикливаться снова, не считая результатов первой инструкции цикла. Я не знаю, если это произойдет больше, потому что я перестал программировать так же много лет назад. Если вы намерены запрограммировать этот путь, вы можете также прочитать Eager Evaluation и некоторые оптимизация переупорядочения команды компилятора. Если вы ищете что-то в стандартах для этого, я надеюсь, что компилятор не добавит кучу лишних байтов в конце каждого выделенного мной массива. Если вы пытаетесь найти для себя некоторые стандарты стандарта C, то это CPU, который даст вашей программе ускоряющий билет.