... простое уменьшение указателя за пределами выделенного диапазона мне кажется весьма схематичным. Это «разрешенное» поведение в C?
Разрешается? Да. Хорошая идея? Как правило, не.
C - сокращение от ассемблера, а на ассемблере нет указателей, только адреса памяти. Указатели Си - это адреса памяти, которые имеют побочное поведение, увеличивающееся или уменьшающееся на размер того, на что они указывают, когда подвергаются арифметике. Это делает следующее очень хорошо с точки зрения синтаксиса:
double *p = (double *)0xdeadbeef;
--p; // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];
Массивы на самом деле не вещь в C; они просто указатели на смежные области памяти, которые ведут себя как массивы. []
Оператор является обобщающим для выполнения арифметических операций над указателями и разыменования, поэтому на a[x]
самом деле означает *(a + x)
.
Существуют веские причины для выполнения вышеизложенного, например, некоторые устройства ввода-вывода, имеющие пару double
s, сопоставленных с 0xdeadbee7
и 0xdeadbeef
. Очень немногие программы должны были бы сделать это.
Когда вы создаете адрес чего-либо, например, с помощью &
оператора или вызова malloc()
, вы хотите сохранить исходный указатель без изменений, чтобы вы знали, что он указывает на что-то действительное. Уменьшение указателя означает, что некоторый фрагмент ошибочного кода может попытаться разыменовать его, получить ошибочные результаты, что-то засорять или, в зависимости от среды, совершить нарушение сегментации. Это особенно верно malloc()
, потому что вы возлагаете бремя на тех, кто звонит, free()
чтобы помнить, чтобы передать оригинальное значение, а не какую-то измененную версию, которая заставит весь хек вырваться.
Если вам нужны массивы на основе 1 в C, вы можете сделать это безопасно за счет выделения одного дополнительного элемента, который никогда не будет использоваться:
double *array_create(size_t size) {
// Wasting one element, so don't allow it to be full-sized
assert(size < SIZE_MAX);
return malloc((size+1) * sizeof(double));
}
inline double array_index(double *array, size_t index) {
assert(array != NULL);
assert(index >= 1); // This is a 1-based array
return array[index];
}
Обратите внимание, что это ничего не делает для защиты от превышения верхней границы, но с этим достаточно легко справиться.
Приложение:
Несколько глав и стихов из черновика C99 (извините, это все, на что я могу ссылаться):
§6.5.2.1.1 говорит, что второе («другое») выражение, используемое с оператором индекса, имеет целочисленный тип. -1
является целым числом, и это делает p[-1]
действительным и, следовательно, также делает указатель &(p[-1])
действительным. Это не означает, что доступ к памяти в этом месте приведет к определенному поведению, но указатель все еще является допустимым указателем.
§6.5.2.2 говорит, что оператор индекса массива оценивается как эквивалент добавления номера элемента к указателю, поэтому p[-1]
эквивалентен *(p + (-1))
. Все еще в силе, но может не дать желаемого поведения.
§6.5.6.8 говорит (выделение мое):
Когда выражение, имеющее целочисленный тип, добавляется или вычитается из указателя, результат имеет тип операнда указателя.
... если выражение P
указывает на i
-й элемент объекта массива, выражения (P)+N
(эквивалентно N+(P)
) и (P)-N
(где N
имеет значение n
) указывают соответственно на i+n
-й и
i−n
-й элементы объекта массива, если они существуют ,
Это означает, что результаты арифметики указателей должны указывать на элемент в массиве. Это не говорит о том, что арифметика должна быть сделана все сразу. Следовательно:
double a[20];
// This points to element 9 of a; behavior is defined.
double d = a[-1 + 10];
double *p = a - 1; // This is just a pointer. No dereferencing.
double e = p[0]; // Does not point at any element of a; behavior is undefined.
double f = p[1]; // Points at element 0 of a; behavior is defined.
Я рекомендую делать вещи таким образом? Я не знаю, и мой ответ объясняет почему.