Указатель индексации

11

В настоящее время я читаю книгу под названием «Численные рецепты в Си». В этой книге автор подробно описывает, как определенные алгоритмы по своей природе работают лучше, если у нас были индексы, начинающиеся с 1 (я не совсем следую его аргументам, и это не является суть этого поста), но C всегда индексирует свои массивы, начиная с 0 Чтобы обойти это, он предлагает просто уменьшить указатель после выделения, например:

float *a = malloc(size);
a--;

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

free(a + 1);

Насколько я знаю, однако, это неопределенное поведение по стандарту C. Это, очевидно, очень авторитетная книга в сообществе HPC, поэтому я не хочу просто игнорировать то, что он говорит, но простое уменьшение указателя за пределами выделенного диапазона мне кажется весьма отрывочным. Это «разрешенное» поведение в C? Я проверил это, используя и gcc, и icc, и оба эти результата указывают на то, что я ни о чем не беспокоюсь, но я хочу быть абсолютно позитивным.

wolfPack88
источник
3
какой стандарт C ты имеешь в виду? Я спрашиваю, потому что, по моим воспоминаниям, «Числовые рецепты на Си» были опубликованы в 1990-х годах, в древние времена K & R и, возможно, ANSI C
комнат
2
Соответствующий вопрос SO: stackoverflow.com/questions/10473573/…
dan04
3
«Я проверил это, используя и gcc, и icc, и оба эти результата указывают на то, что я ни о чем не беспокоюсь, но хочу быть абсолютно позитивным». Никогда не предполагайте, что, поскольку ваш компилятор это позволяет, язык C позволяет это. Если, конечно, у вас все в порядке с взломом кода в будущем.
Довал
5
Не желая быть хитрым, «Числовые рецепты» обычно считаются полезной, быстрой и грязной книгой, а не парадигмой разработки программного обеспечения или численного анализа. Проверьте статью Wikipedia на «Числовые рецепты» для краткого изложения некоторых критических замечаний.
Чарльз И. Грант
1
Кроме того, вот почему мы индексируем с нуля: cs.utexas.edu/~EWD/ewd08xx/EWD831.PDF
Рассел Борогове

Ответы:

16

Вы правы, что код, такой как

float a = malloc(size);
a--;

дает неопределенное поведение в соответствии со стандартом ANSI C, раздел 3.3.6:

Если операнд-указатель и результат не указывают на член одного и того же объекта массива или один после последнего члена объекта массива, поведение не определено

Для такого кода качество кода C в книге (когда я использовал его в конце 1990-х) не считалось очень высоким.

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

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

Барт ван Инген Шенау
источник
4
Точно, в мультибанковской архитектуре возможно, что malloc может дать вам 0-й адрес в банке, а уменьшение его может вызвать ловушку ЦП с недостаточным значением для одного.
Vality
1
Я не согласен, что это "повезло". Я думаю, что было бы намного лучше, если бы компиляторы генерировали код, который сразу же падал всякий раз, когда вы вызывали неопределенное поведение.
Дэвид Конрад,
4
@DavidConrad: Тогда C не для тебя. Большая часть неопределенного поведения в C не может быть легко обнаружена или только с серьезным ударом по производительности.
Барт ван Инген Шенау
Я думал о добавлении "с переключателем компилятора". Очевидно, вы не захотите этого для оптимизированного кода. Но вы правы, и именно поэтому я бросил писать C десять лет назад.
Дэвид Конрад
@BartvanIngenSchenau, в зависимости от того, что вы подразумеваете под «серьезным ударом по производительности», есть символическое выполнение для C (например, clang + klee), а также для средств для обработки (asan, tsan, ubsan, valgrind и т. Д.), Которые, как правило, очень полезны для отладки.
Мацей Печотка
10

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

На практике, если ваш процессор имеет плоскую модель памяти (в отличие от странных моделей , таких как x86-16 ), и если компилятор не выдает ошибку времени выполнения или неправильную оптимизацию, если вы создаете недопустимый указатель, тогда код будет работать просто хорошо.

dan04
источник
1
Это имеет смысл. К сожалению, это на два больше, если мне нравится.
wolfPack88
3
Последний пункт ИМХО самый проблемный. Поскольку компиляторы в наше время не просто позволяют происходить тому, что платформа «естественно» делает в случае UB, но оптимизаторы активно ее используют , я бы не стал так легко играть с ней.
Matteo Italia
3

Во-первых, это неопределенное поведение. В настоящее время некоторые оптимизирующие компиляторы очень агрессивно относятся к неопределенному поведению. Например, поскольку 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. Вещи могут стать интересными.

gnasher729
источник
1

... простое уменьшение указателя за пределами выделенного диапазона мне кажется весьма схематичным. Это «разрешенное» поведение в C?

Разрешается? Да. Хорошая идея? Как правило, не.

C - сокращение от ассемблера, а на ассемблере нет указателей, только адреса памяти. Указатели Си - это адреса памяти, которые имеют побочное поведение, увеличивающееся или уменьшающееся на размер того, на что они указывают, когда подвергаются арифметике. Это делает следующее очень хорошо с точки зрения синтаксиса:

double *p = (double *)0xdeadbeef;
--p;  // p == 0xdeadbee7, assuming sizeof(double) == 8.
double d = p[0];

Массивы на самом деле не вещь в C; они просто указатели на смежные области памяти, которые ведут себя как массивы. []Оператор является обобщающим для выполнения арифметических операций над указателями и разыменования, поэтому на a[x]самом деле означает *(a + x).

Существуют веские причины для выполнения вышеизложенного, например, некоторые устройства ввода-вывода, имеющие пару doubles, сопоставленных с 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.

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

Blrfl
источник
8
-1 Определение «позволенного», которое включает код, который стандарт C объявляет как генерирующий неопределенные результаты, не является полезным.
Пит Киркхэм
Другие отметили, что это неопределенное поведение, поэтому не стоит говорить, что оно «разрешено». Однако предложение выделить дополнительный неиспользуемый элемент 0 является хорошим.
200_успех
Это действительно не правильно, пожалуйста, по крайней мере, обратите внимание, что это запрещено стандартом C.
Vality
@PeteKirkham: я не согласен. Смотрите приложение к моему ответу.
Blrfl
4
@Blrfl 6.5.6 стандарта ISO C11 заявляет в случае добавления целого числа к указателю: «Если и операнд указателя, и результат указывают на элементы одного и того же объекта массива или один после последнего элемента объекта массива оценка не должна приводить к переполнению, иначе поведение не определено ".
Vality