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

Является ли законным вызывать memcpy с нулевой длиной на указателе только за концом массива?

Как ответил в другом месте, вызов таких функций, как memcpy с недопустимыми или NULL указателями, - это поведение undefined, даже если аргумент length равен нулю. В контексте такой функции, особенно memcpy и memmove, является указателем за конец массива действительным указателем?

Я задаю этот вопрос, потому что указатель, который находится за концом массива, является законным для получения (в отличие от, например, указателя на два элемента за конец массива), но вам не разрешено разыгрывать его сноска 106 стандарта ISO 9899: 2011 указывает, что такой указатель указывает на адресное пространство программы, критерий, необходимый для того, чтобы указатель был действительным в соответствии с §7.1.4.

Такое использование происходит в коде, где я хочу вставить элемент в середину массива, требуя, чтобы я переместил все элементы после точки вставки:

void make_space(type *array, size_t old_length, size_t index)
{
    memmove(array + index + 1, array + index, (old_length - index) * sizeof *array);
}

Если мы хотим вставить в конец массива, index равно length и array + index + 1 указывает только конец конца массива, но число скопированных элементов равно нулю.

4b9b3361

Ответ 1

3.15 объект

  • объект область хранения данных в среде исполнения, содержимое которой может представлять значения

Указатель памяти, указатель на один из последних элементов последнего объекта объекта объекта или объекта, не может представлять значения, поскольку он не может быть разыменован (6.5.6 Аддитивные операторы, пункт 8).

7.24.2.1 Функция memcpy

  1. Функция memcpy копирует n символов из объекта, на который указывает s2, в объект, на который указывает s1. Если копирование происходит между перекрывающимися объектами, поведение undefined.

Указатели, переданные memcpy, должны указывать на объект.

6.5.3.4 Операторы sizeof и _Alignof

  1. Когда sizeof применяется к операнду с типом char, unsigned char или подписанный char (или его квалифицированная версия), результат равен 1. При применении к операнд с типом массива, результатом будет общее количество байтов в массиве. когда применяется к операнду, который имеет структуру или тип объединения, результатом является общее количество байтов в таком объекте, включая внутреннее и конечное заполнение.

Оператор sizeof не учитывает элемент "один за прошлый" как объект, поскольку он не учитывается в размере объекта. Тем не менее он явно дает размер всего объекта.

6.3.2.1 Lvalues, массивы и обозначения функций

  • Значение lvalue является выражением (с типом объекта, отличным от void), которое потенциально обозначает объект; 64), если lvalue не обозначает объект при его оценке, поведение undefined.

Я утверждаю, что один предыдущий указатель на объект массива или объект, обоим из которых в противном случае разрешено указывать, не представляет объект.

int a ;
int* p = a+1 ; 

p определен, но он не указывает на объект, поскольку он не может быть разыменован, память, на которую указывает точка, не может представлять значение, а sizeof не учитывает эту память как часть объекта. Memcpy требует указателя на объект.

Следовательно, передача одного предыдущего указателя на memcpy вызывает поведение undefined.

Update:

Эта часть также подтверждает вывод:

6.5.9 Операторы равенства

  1. Два указателя сравнивают одинаковые, если и только если оба являются нулевыми указателями, оба являются указателями на тот же объект (включая указатель на объект и подобъект в начале) или функцию, оба являются указателями на один последний элемент одного и того же объекта массива, или один - указатель к одному концу конца одного объекта массива, а другой - указателю на начало другого массив, который происходит сразу после первого объекта массива в адресе пространство.

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

Ответ 2

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

(К сожалению, в стандарте не так много информации о "прошлом последнем элементе" ).

Примечание: Извините, что теперь есть другое направление...

Вопрос в основном состоит в том, является ли "один за конечным указателем" допустимым аргументом первой функции для memmove, если перемещено 0 байтов:

T array[length];
memmove(array + length, array + length - 1u, 0u);

Необходимым требованием является справедливость первого аргумента.

N1570, 7.1.4, 1

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

Если аргумент функции имеет недопустимое значение (например, значение вне домена функции или указатель вне адресного пространства программы или нулевой указатель или указатель на немодифицируемое хранилище, когда соответствующий параметр не является константным) или тип (после продвижения), не ожидаемый функцией с переменным числом аргументов, поведение undefined.

Сделать аргумент действительным, если указатель

  • не находится за пределами адресного пространства,
  • не является нулевым указателем,
  • не является указателем на константную память

и если тип аргумента

  1. не относится к типу массива.

1. Адресное пространство

N1570, 6.5.6, 8

Кроме того, если выражение P указывает на последний элемент объекта массива, выражение (P) +1 указывает один за последним элементом объекта массива, и если выражение Q указывает один за последним элементом массив, выражение (Q) -1 указывает на последний элемент объекта массива.

N1570, 6.5.6, 9

Кроме того, если выражение P указывает либо на элемент объекта массива, либо мимо последнего элемента объекта массива, а выражение Q указывает на последний элемент одного и того же объекта массива, выражение ((Q) +1) - (P) имеет то же значение, что и ((Q) - (P)) + 1 и as - ((P) - ((Q) +1)) и имеет значение 0, если выражение P указывает один за последним элементом объекта массива, хотя выражение (Q) +1 не указывает на элемент объекта массива. 106

106 Другой способ приблизиться к арифметике указателя - сначала преобразовать указатель (указатели) в указатель символов: В этой схеме сначала умножается целочисленное выражение, добавленное или вычитаемое из преобразованного указателя по размеру объекта, на который был направлен указатель, и полученный указатель преобразуется обратно в исходный тип. Для вычитания указателя результат разницы между указателями символов аналогичным образом делится на размер объекта, на который первоначально указывалось.

При просмотре таким образом реализации необходимо предоставить только один дополнительный байт (который может перекрывать другой объект в программе) сразу после конца объекта, чтобы удовлетворить требованиям "один за последним элементом".

Несмотря на то, что сноска не является нормативной, - отметил Лундин, - у нас есть объяснение, что "для реализации необходимо предоставить только один дополнительный байт". Хотя я не могу процитировать, цитируя, я подозреваю, что это намек на то, что стандарт означает, что реализация должна включать в себя память внутри адресного пространства программ в месте, на которое указывает предыдущий конечный указатель.

2. Null Pointer

Прошедший конечный указатель не является нулевым указателем.

3. Указание на константную память

Стандарт не налагает никаких дополнительных требований на конец конечного указателя, кроме предоставления некоторой информации о результате нескольких операций, и примечание (опять же не нормальное;)) поясняет, что оно может пересекаться с другим объектом. Таким образом, нет гарантии, что память, на которую указывает конечный указатель, не постоянна. Так как первый аргумент memove является указателем на непостоянную память, то прохождение последнего указателя конца не гарантируется как допустимое и потенциально поведение undefined.

4. Действительность аргументов массива

В главе 7.21.1 описывается заголовок обработки строки <string.h>, а первое условие:

Заголовок объявляет один тип и несколько функций и определяет один макрос, полезный для манипулирования массивами типа символов и других объектов, рассматриваемых как массивы типа символа.

Я не думаю, что стандарт здесь очень ясен, относятся ли "объекты, рассматриваемые как массивы типа символа", к функциям или только к макросу. Если это предложение на самом деле означает, что memove рассматривает первый аргумент как массив символов, то поведение передачи прошлого в конце указателя на memmove равно undefined по 7.1.4 (для чего требуется указатель на действительный объект).

Ответ 3

Если мы посмотрим на C99 standard, то это:

7.21.1.p2

Если аргумент, объявленный как size_t n, указывает длину массив для функции, n может иметь нулевое значение при вызове функция. Если явно не указано иначе в описании определенную функцию в этом подпункте, аргументы указателя на такой вызов должен иметь действительные значения, как описано в 7.1.4. На таком вызов, функция, которая находит символ, не обнаруживает появления, функция, которая сравнивает две последовательности символов, возвращает ноль, а функция, которая копирует символы, копирует нулевые символы....

В описании memcpy нет явного выражения в 7.21.2.1

7.1.4.p1

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

Добавлен акцент. Кажется, указатели должны указывать на допустимые местоположения (в смысле разыменования), а абзацы о арифметике указателя, позволяющие указывать на конец + 1, здесь не применяются.

Возникает вопрос, являются ли аргументы memcpy массивами или нет. Конечно, они не объявлены как массивы, но

7.21.1.p1 говорит

Заголовок string.h объявляет один тип и несколько функций, и определяет один макрос, полезный для манипулирования массивами типа символа и другие объекты, обработанные как массивы типа символа.

и memcpy находится в string.h.
Поэтому я бы предположил, что memcpy обрабатывает аргументы как массивы символов. Поскольку упомянутый макрос NULL, часть предложения "полезно для..." явно относится к функциям.