В настоящее время я читаю книгу под названием «Численные рецепты в Си». В этой книге автор подробно описывает, как определенные алгоритмы по своей природе работают лучше, если у нас были индексы, начинающиеся с 1 (я не совсем следую его аргументам, и это не является суть этого поста), но C всегда индексирует свои массивы, начиная с 0 Чтобы обойти это, он предлагает просто уменьшить указатель после выделения, например:
float *a = malloc(size);
a--;
Это, по его словам, фактически даст вам указатель с индексом, начинающимся с 1, который затем будет свободен с:
free(a + 1);
Насколько я знаю, однако, это неопределенное поведение по стандарту C. Это, очевидно, очень авторитетная книга в сообществе HPC, поэтому я не хочу просто игнорировать то, что он говорит, но простое уменьшение указателя за пределами выделенного диапазона мне кажется весьма отрывочным. Это «разрешенное» поведение в C? Я проверил это, используя и gcc, и icc, и оба эти результата указывают на то, что я ни о чем не беспокоюсь, но я хочу быть абсолютно позитивным.
Ответы:
Вы правы, что код, такой как
дает неопределенное поведение в соответствии со стандартом ANSI C, раздел 3.3.6:
Для такого кода качество кода C в книге (когда я использовал его в конце 1990-х) не считалось очень высоким.
Проблема с неопределенным поведением заключается в том, что независимо от того, какой результат выдает компилятор, этот результат по определению является правильным (даже если он очень деструктивный и непредсказуемый).
К счастью, очень немногие компиляторы прилагают усилия к тому, чтобы фактически вызвать непредвиденное поведение в таких случаях, и типичная
malloc
реализация на машинах, используемых для HPC, имеет некоторые бухгалтерские данные непосредственно перед возвращаемым адресом, поэтому уменьшение обычно дает указатель на эти бухгалтерские данные. Писать там не очень хорошая идея, но просто создание указателя в этих системах безвредно.Просто имейте в виду, что код может сломаться при изменении среды выполнения или при переносе кода в другую среду.
источник
Официально, это неопределенное поведение - указывать точку указателя вне массива (за исключением одного после конца), даже если он никогда не разыменовывается .
На практике, если ваш процессор имеет плоскую модель памяти (в отличие от странных моделей , таких как x86-16 ), и если компилятор не выдает ошибку времени выполнения или неправильную оптимизацию, если вы создаете недопустимый указатель, тогда код будет работать просто хорошо.
источник
Во-первых, это неопределенное поведение. В настоящее время некоторые оптимизирующие компиляторы очень агрессивно относятся к неопределенному поведению. Например, поскольку a-- в этом случае - неопределенное поведение, компилятор может решить сохранить инструкцию и цикл процессора, а не уменьшить a. Что официально правильно и законно.
Игнорируя это, вы можете вычесть 1, 2 или 1980. Например, если у меня есть финансовые данные за 1980–2013 годы, я могу вычесть 1980. Теперь, если мы возьмем float * a = malloc (size); несомненно, существует некоторая большая константа k такая, что a - k является нулевым указателем. В этом случае мы действительно ожидаем, что что-то пойдет не так.
Теперь возьмите большую структуру, скажем, мегабайт. Выделите указатель p, указывающий на две структуры. p - 1 может быть нулевым указателем. p-1 может обернуться (если структура имеет размер в мегабайт, а блок malloc находится в 900 КБ от начала адресного пространства). Таким образом, это может быть без какого-либо злого умысла компилятора, что p - 1> p. Вещи могут стать интересными.
источник
Разрешается? Да. Хорошая идея? Как правило, не.
C - сокращение от ассемблера, а на ассемблере нет указателей, только адреса памяти. Указатели Си - это адреса памяти, которые имеют побочное поведение, увеличивающееся или уменьшающееся на размер того, на что они указывают, когда подвергаются арифметике. Это делает следующее очень хорошо с точки зрения синтаксиса:
Массивы на самом деле не вещь в C; они просто указатели на смежные области памяти, которые ведут себя как массивы.
[]
Оператор является обобщающим для выполнения арифметических операций над указателями и разыменования, поэтому наa[x]
самом деле означает*(a + x)
.Существуют веские причины для выполнения вышеизложенного, например, некоторые устройства ввода-вывода, имеющие пару
double
s, сопоставленных с0xdeadbee7
и0xdeadbeef
. Очень немногие программы должны были бы сделать это.Когда вы создаете адрес чего-либо, например, с помощью
&
оператора или вызоваmalloc()
, вы хотите сохранить исходный указатель без изменений, чтобы вы знали, что он указывает на что-то действительное. Уменьшение указателя означает, что некоторый фрагмент ошибочного кода может попытаться разыменовать его, получить ошибочные результаты, что-то засорять или, в зависимости от среды, совершить нарушение сегментации. Это особенно верноmalloc()
, потому что вы возлагаете бремя на тех, кто звонит,free()
чтобы помнить, чтобы передать оригинальное значение, а не какую-то измененную версию, которая заставит весь хек вырваться.Если вам нужны массивы на основе 1 в C, вы можете сделать это безопасно за счет выделения одного дополнительного элемента, который никогда не будет использоваться:
Обратите внимание, что это ничего не делает для защиты от превышения верхней границы, но с этим достаточно легко справиться.
Приложение:
Несколько глав и стихов из черновика C99 (извините, это все, на что я могу ссылаться):
§6.5.2.1.1 говорит, что второе («другое») выражение, используемое с оператором индекса, имеет целочисленный тип.
-1
является целым числом, и это делаетp[-1]
действительным и, следовательно, также делает указатель&(p[-1])
действительным. Это не означает, что доступ к памяти в этом месте приведет к определенному поведению, но указатель все еще является допустимым указателем.§6.5.2.2 говорит, что оператор индекса массива оценивается как эквивалент добавления номера элемента к указателю, поэтому
p[-1]
эквивалентен*(p + (-1))
. Все еще в силе, но может не дать желаемого поведения.§6.5.6.8 говорит (выделение мое):
Это означает, что результаты арифметики указателей должны указывать на элемент в массиве. Это не говорит о том, что арифметика должна быть сделана все сразу. Следовательно:
Я рекомендую делать вещи таким образом? Я не знаю, и мой ответ объясняет почему.
источник