Каков правильный тип для индексов массива в C?

Какой тип для индекса массива в C99 следует использовать? Он должен работать на LP32, ILP32, ILP64, LP64, LLP64 и более. Это не обязательно должен быть тип C89.

Я нашел 5 кандидатов:

  • size_t
  • ptrdiff_t
  • intptr_t / uintptr_t
  • int_fast*_t / uint_fast*_t
  • int_least*_t / uint_least*_t

Существует простой код, чтобы лучше проиллюстрировать проблему. Какой тип лучше всего подходит для i а также j в этих двух конкретных циклах. Если есть веская причина, два разных типа тоже подойдут.

for (i=0; i<imax; i++) {
        do_something(a[i]);
}
/* jmin can be less than 0 */
for (j=jmin; j<jmax; j++) {
        do_something(a[j]);
}

PS В первой версии вопроса я забыл об отрицательных показателях.

PPS Я не собираюсь писать компилятор C99. Однако любой ответ от программиста компилятора был бы очень ценным для меня.

Подобный вопрос:

8 ответов

Решение

Я почти всегда использую size_t для индексов массива / счетчиков цикла. Конечно, есть некоторые особые случаи, когда вам могут потребоваться смещения со знаком, но в целом использование типа со знаком имеет много проблем:

Наибольший риск заключается в том, что если вы передали вызывающему абоненту огромный размер / смещение, которое рассматривает вещи как неподписанные (или если вы прочитали это из файла с ошибочно доверенными данными), вы можете интерпретировать его как отрицательное число и не поймать, что оно за границами. Например if (offset<size) array[offset]=foo; else error(); напишу где-то не должно.

Другая проблема заключается в возможности неопределенного поведения с целочисленным переполнением со знаком. Независимо от того, используете ли вы неподписанную или подписанную арифметику, существуют проблемы переполнения, о которых нужно знать и проверять, но лично я считаю, что с неподписанным поведением гораздо легче справиться.

Еще одна причина использовать арифметику без знака (в общем) - иногда я использую индексы как смещения в массиве битов и хочу использовать%8 и /8 или%32 и /32. С подписанными типами это будут фактические операции деления. С unsigned можно генерировать ожидаемые побитовые и / битовые операции.

Я думаю, что вы должны использовать ptrdiff_t по следующим причинам

  • Индексы могут быть отрицательными (при этом все типы без знака, в том числе size_tне может быть и речи)
  • Тип p2 - p1 является ptrdiff_t, Тип i в обратном направлении, *(p1 + i), должен быть такого типа тоже (обратите внимание, что *(p + i) эквивалентно p[i])

Так как тип sizeof(array) (а также mallocаргумент) size_tи массив не может содержать больше элементов, чем его размер, из этого следует, что size_t может использоваться для индекса массива.

РЕДАКТИРОВАТЬ Этот анализ для массивов на основе 0, что является распространенным случаем. ptrdiff_t будет работать в любом случае, но для переменной индекса немного странно иметь тип разности указателей.

Если вы начнете с 0используйте size_t, потому что этот тип должен иметь возможность индексировать любой массив:

  • sizeof возвращает его, поэтому недопустимо, чтобы массив имел больше size_t элементы
  • malloc принимает это как аргумент, как упомянуто Амноном

Если вы начнете ниже нуля, то переключитесь на старт с нуля и используйте size_t, который гарантированно работает по причинам, изложенным выше. Так что замените:

for (j = jmin; j < jmax; j++) {
    do_something(a[j]);
}

с:

int *b = &a[jmin];
for (size_t i = 0; i < (jmax - jmin); i++) {
    do_something(b[i]);
}

Почему бы не использовать:

  • ptrdiff_t: максимальное значение, которое это представляет, может быть меньше, чем максимальное значение size_t,

    Это упоминается в cppref, и возможность неопределенного поведения, если массив слишком велик, предлагается в C99 6.5.5/9:

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

    Из любопытства, intptr_t также может быть больше, чем size_t на архитектуре сегментированной памяти: /questions/11674223/sizet-protiv-uintptrt/11674239#11674239

    GCC также накладывает дополнительные ограничения на максимальный размер объектов статического массива. Какой максимальный размер массива в C?

  • uintptr_t: я не уверен. Так что я бы просто использовал size_t потому что я более уверен:-)

Мой выбор: ptrdiff_t

Многие проголосовали за ptrdiff_t, но некоторые говорят, что странно индексировать с использованием разностного типа указателя. Для меня это имеет смысл: индекс массива - это отличие от исходного указателя.

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

Я использую unsigned int, (хотя я предпочитаю стенографию unsigned)

В С99 unsigned int гарантированно сможет индексировать любой переносимый массив. Гарантируется поддержка только массивов размером 65'535 байт или меньше, а максимальный unsigned int значение не менее 65'535.

Из открытого проекта WG14 N1256 стандарта C99:

5.2.4.1 Пределы перевода

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

(...)

  • 65535 байт в объекте (только в размещенной среде)

(...)

5.2.4.2 Численные ограничения

Реализация должна документировать все ограничения, указанные в этом подпункте, которые указаны в заголовках <limits.h> а также <float.h>, Дополнительные ограничения указаны в <stdint.h>,

5.2.4.2.1 Размеры целочисленных типов <limits.h>

Значения, приведенные ниже, должны быть заменены постоянными выражениями, подходящими для использования в #if директивы предварительной обработки. Более того, кроме CHAR_BIT а также MB_LEN_MAXследующее должно быть заменено выражениями того же типа, что и выражение, являющееся объектом соответствующего типа, преобразованным в соответствии с целочисленными преобразованиями. Их определяемые реализацией значения должны быть равны или больше по величине (абсолютное значение) показанным с тем же знаком.

(...)

  • максимальное значение для объекта типа unsigned intUINT_MAX 65535 // 2^16 - 1

В ANSI C максимальный размер переносимого массива на самом деле составляет всего 32 767 байт, поэтому даже подписанный int будет делать, который имеет максимальное значение не менее 32 767 (Приложение A.4).

Из §2.2.4 проекта C89:

2.2.4.1 Пределы перевода

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

(...)

  • 32767 байт в объекте (только в размещенной среде)

(...)

2.2.4.2 Численные ограничения

Соответствующая реализация должна документировать все ограничения, указанные в этом разделе, которые должны быть указаны в заголовках. <limits.h> а также <float.h>,

"Размеры интегральных типов <limits.h>"

Значения, приведенные ниже, должны быть заменены константными выражениями, подходящими для использования в директивах предварительной обработки #if. Их определяемые реализацией значения должны быть равны или больше по величине (абсолютное значение) показанным с тем же знаком.

(...)

  • максимальное значение для объекта типа int INT_MAX +32767

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

for(size_t i=5; i>=0; i--) {
  printf("danger, this loops forever\n);
}

Этого не произойдет, если вы используете ptrdiff_t или любой другой подходящий подписанный тип. В системах POSIX вы можете использовать ssize_t,

Лично я часто просто использую intдаже если это, возможно, не правильная вещь.

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

  • int_fast*_t / uint_fast*_t
  • int_least*_t / uint_least*_t

Во всех остальных случаях я бы рекомендовал использовать

  • size_t

или же

  • ptrdiff_t

в зависимости от погоды вы хотите разрешить отрицательные показатели.

С помощью

  • intptr_t / uintptr_t

было бы также безопасно, но иметь немного другую семантику.

Я обычно использую size_t для смещений массива, но если вам нужна отрицательная индексация массива, используйте int. Он может обращаться к массиву максимального размера, гарантированному C89 (32767 байт).

Если вы хотите получить доступ к массивам максимального размера, гарантированного C99 (65535 байт), используйте unsigned.

См. Предыдущие версии для доступа к массивам, разрешенного, но не гарантированного C.

Другие вопросы по тегам