Следует ли вам всегда использовать int для чисел в C, даже если они неотрицательны? - PullRequest
46 голосов
/ 15 июля 2010

Я всегда использую без знака int для значений, которые никогда не должны быть отрицательными.Но сегодня я заметил такую ​​ситуацию в моем коде:

void CreateRequestHeader( unsigned bitsAvailable, unsigned mandatoryDataSize, 
    unsigned optionalDataSize )
{
    If ( bitsAvailable – mandatoryDataSize >= optionalDataSize ) {
        // Optional data fits, so add it to the header.
    }

    // BUG! The above includes the optional part even if
    // mandatoryDataSize > bitsAvailable.
}

Должен ли я начать использовать int вместо unsigned int для чисел, даже если они не могут бытьотрицательный?

Ответы [ 16 ]

93 голосов
/ 16 июля 2010

Одна вещь, которая не была упомянута, заключается в том, что обмен номерами со знаком / без знака может привести к ошибкам безопасности . Это проблема big , поскольку многие функции в стандартной библиотеке C принимают / возвращают числа без знака (fread, memcpy, malloc и т. Д., Все принимают параметры size_t)

Например, возьмите следующий безобидный пример (из реального кода):

//Copy a user-defined structure into a buffer and process it
char* processNext(char* data, short length)
{
    char buffer[512];
    if (length <= 512) {
        memcpy(buffer, data, length);
        process(buffer);
        return data + length;
    } else {
        return -1;
    }
}

Выглядит безобидно, верно? Проблема в том, что length подписан, но при передаче в memcpy преобразуется в неподписанный. Таким образом, установка длины SHRT_MIN проверит тест <= 512, но заставит memcpy скопировать более 512 байт в буфер - это позволит злоумышленнику перезаписать адрес возврата функции в стеке и (после небольшой работы) ) возьми свой компьютер!

Вы можете наивно говорить: «Это так очевидно, что длина должна быть size_t или проверена на >= 0, я никогда не смогу совершить эту ошибку» . Кроме того, я гарантирую, что если вы когда-нибудь написали что-нибудь нетривиальное, у вас есть. Так же как и авторы Windows , Linux , BSD , Solaris , Firefox , OpenSSL , Safari , MS Paint , Internet Explorer , Google Picasa , Opera , Flash , Open Office , Subversion , Apache , Python , PHP , Pidgin , Gimp , ... включается и включается и включается ... - и это все яркие люди, чья работа зная безопасность.

Короче говоря, всегда используйте size_t для размеров.

Человек, программирование сложно .

25 голосов
/ 15 июля 2010

Должен ли я всегда ...

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

Но это очень субъективный вопрос, действительно легко испортить неподписанные:

for (unsigned int i = 10; i >= 0; i--);

приводит к бесконечному циклу.

Именно поэтому некоторые руководства по стилю, включая Руководство по стилю Google C ++ , не поощряют unsigned типы данных.

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

12 голосов
/ 16 июля 2010

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

  • Вам необходимо обрабатывать данные как чистое двоичное представление.
  • Вам нужна семантика арифметики по модулю, которую вы получаетес числами без знака.
  • Вы должны взаимодействовать с кодом, который использует типы без знака (например, стандартные библиотечные процедуры, которые принимают / возвращают size_t значения.

Но для общей арифметики, вещьто есть, когда вы говорите, что что-то «не может быть отрицательным», это не обязательно означает, что вы должны использовать неподписанный тип, поскольку вы можете поместить отрицательное значение в беззнаковое, простодействительно большое значение, когда вы идете, чтобы получить его. Итак, если вы имеете в виду, что отрицательные значения запрещены, например, для базовой функции квадратного корня, то вы указываете предварительное условие функции, и вы должны утверждать. И вы можетене утверждайте, что не может быть, вам нужен способ хранения внеполосных значений, чтобы вы могли проверить их (это та же логика,nd getchar() возвращает int, а не char.)

Кроме того, выбор типа «подпись против неподписанного» также может иметь практические последствия для производительности.Посмотрите на (надуманный) код ниже:

#include <stdbool.h>

bool foo_i(int a) {
    return (a + 69) > a;
}

bool foo_u(unsigned int a)
{
    return (a + 69u) > a;
}

Оба foo одинаковы, за исключением типа их параметра.Но при компиляции с c99 -fomit-frame-pointer -O2 -S вы получите:

        .file   "try.c"
        .text
        .p2align 4,,15
.globl foo_i
        .type   foo_i, @function
foo_i:
        movl    $1, %eax
        ret
        .size   foo_i, .-foo_i
        .p2align 4,,15
.globl foo_u
        .type   foo_u, @function
foo_u:
        movl    4(%esp), %eax
        leal    69(%eax), %edx
        cmpl    %eax, %edx
        seta    %al
        ret
        .size   foo_u, .-foo_u
        .ident  "GCC: (Debian 4.4.4-7) 4.4.4"
        .section        .note.GNU-stack,"",@progbits

Вы видите, что foo_i() более эффективен, чем foo_u().Это связано с тем, что арифметическое переполнение без знака определяется стандартом для «обтекания», поэтому (a + 69u) вполне может быть меньше, чем a, если a очень велико, и, следовательно, для этого случая должен быть код.С другой стороны, арифметическое переполнение со знаком не определено, поэтому GCC продолжит и примет арифметику со знаком , не переполняет , поэтому (a + 69) не может быть меньше a.Поэтому выбор неподписанных типов без разбора может излишне повлиять на производительность.

11 голосов
/ 16 июля 2010

Бьярн Страуструп, создатель C ++, предупреждает об использовании неподписанных типов в своей книге Язык программирования C ++:

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

9 голосов
/ 16 июля 2010

Ответ - да.Тип unsigned int в C и C ++ не является «всегда положительным целым числом», независимо от того, как выглядит имя типа.Поведение C / C ++ беззнаковых целочисленных значений не имеет смысла, если вы пытаетесь прочитать тип как «неотрицательный» ... например:

  • Разница между двумя беззнаковыми числами является числом без знака (делаетне имеет смысла, если вы читаете это как «Разница между двумя неотрицательными числами неотрицательна»)
  • Добавление целого и беззнакового целого является беззнаковым
  • Существует неявное преобразованиеот int до unsigned int (если вы читаете unsigned как «неотрицательное», это имеет смысл противоположное преобразование, которое имеет смысл)
  • Если вы объявляете функцию, принимающую параметр без знака, когда кто-то передаетОтрицательный int вы просто неявно преобразуете в огромное положительное значение;другими словами, использование типа параметра без знака не помогает вам находить ошибки ни во время компиляции, ни во время выполнения.

Действительно, числа без знака очень полезны для определенных случаев, потому что они являются элементами целых чисел кольца-модуло-N "с N, являющимся степенью двойки.Целые числа без знака полезны, когда вы хотите использовать эту арифметику по модулю или как битовые маски;они НЕ полезны в качестве величин.

К сожалению, в C и C ++ без знака также использовались для представления неотрицательных величин, чтобы иметь возможность использовать все 16 битов, когда целые числа были такими маленькими ... в то времяиспользовать 32k или 64k считалось большой разницей.Я бы классифицировал это в основном как историческую случайность ... вы не должны пытаться читать логику, потому что там не было логики.

Кстати, по моему мнению, это было ошибкой ... если32 КБ недостаточно, тогда довольно скоро 64 КБ тоже будет недостаточно;злоупотребление целым числом по модулю только из-за одного дополнительного бита, по моему мнению, было слишком дорого, чтобы заплатить.Конечно, было бы разумно сделать это, если бы присутствовал или был определен надлежащий неотрицательный тип ... но беззнаковая семантика просто неверна для использования его как неотрицательного.

Иногда вы можете найти, кто говоритэтот unsigned хорош, потому что он «документирует», что вам нужны только неотрицательные значения ... однако эта документация имеет какое-либо значение только для людей, которые на самом деле не знают, как unsigned работает для C или C ++.Для меня видение беззнакового типа, используемого для неотрицательных значений, просто означает, что тот, кто написал код, не понимал язык этой части.

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

Означает ли это, что возвращаемый тип C ++ std::vector<>::size() является плохим выбором?Да ... это ошибкаНо если вы так говорите, будьте готовы к тому, что вас будут называть дурными именами, которые не понимают, что «беззнаковое» имя - это просто имя ... то, что оно считает, - это поведение, а это - «по модулю n» (и неможно было бы считать «по модулю n» тип контейнера разумным выбором.

7 голосов
/ 16 июля 2010

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

Если вы, следовательно, придерживаетесь семантики, которую представляет для вас тип, тогда проблем не должно быть: используйте size_t (без знака) для индексов массива, смещения данных и т. Д. off_t (со знаком) для смещения файла. Используйте ptrdiff_t (подпись) для различий указателей. Используйте uint8_t для маленьких целых без знака и int8_t для знаковых. И вы избежите как минимум 80% проблем с переносимостью.

И не используйте int, long, unsigned, char, если вы не должны. Они принадлежат в учебниках истории. (Иногда вы должны, ошибки возвращаются, битовые поля, например)

И вернемся к вашему примеру:

bitsAvailable – mandatoryDataSize >= optionalDataSize

можно легко переписать как

bitsAvailable >= optionalDataSize + mandatoryDataSize

, которая не устраняет проблему потенциального переполнения (assert - ваш друг), но, кажется, приближает вас к идее того, что вы хотите проверить.

6 голосов
/ 16 июля 2010

Вы не можете полностью избежать неподписанных типов в переносимом коде, потому что многие определения типов в стандартной библиотеке являются беззнаковыми (особенно size_t) и многие функции возвращают их (например, std::vector<>::size()).

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

6 голосов
/ 15 июля 2010
if (bitsAvailable >= optionalDataSize + mandatoryDataSize) {
    // Optional data fits, so add it to the header.
}

Без ошибок, так как обязательный DataSize + optionDataSize не может переполнить целочисленный тип без знака - присвоение имен этим переменным приводит меня к мысли, что это, вероятно, так.

3 голосов
/ 16 июля 2010

Из комментариев к одному из сообщений в блоге Эрика Липперта (см. здесь ):

Джеффри Л. Уитледж

Однажды я разработал систему, в которой отрицательные значения не имели смысла как параметр, так что вместо проверки что значения параметров были не отрицательно, я думал, что это будет отличная идея просто использовать Uint вместо. я быстро обнаружил, что всякий раз, когда я использовал эти значения для чего-либо (как вызывая методы BCL), они должны были конвертируется в целые числа со знаком. это означало, что я должен был подтвердить, что значения не превышали подписанные целочисленный диапазон на верхнем торце, поэтому я ничего не получил Кроме того, каждый раз, когда код был назван, ints, которые были используется (часто получаемый от BCL функции) должны были быть преобразованы в uints. Это не заняло много времени, прежде чем я изменил все эти Uints обратно в Ints и взял все это ненужное литье из. Я все еще должен подтвердить, что цифры не отрицательные, а код намного чище!

Эрик Липперт

Сам не мог бы сказать это лучше. Вам почти никогда не нужен диапазон и они не соответствуют CLS. Стандартный способ представить маленький целое число с "int", даже если есть значения там, которые находятся вне спектр. Хорошее правило: используйте только «uint» для ситуаций, в которых вы находитесь взаимодействие с неуправляемым кодом что ожидает Uints, или где рассматриваемое целое число явно используется как набор битов, а не число. Всегда старайтесь избегать этого в публичных интерфейсах. - Эрик

2 голосов
/ 16 июля 2010

Нет, вы должны использовать тип, который подходит для вашего приложения.Там нет золотого правила.Иногда на небольших микроконтроллерах, например, более быстро и эффективно использовать память, скажем, 8 или 16-битные переменные, где это возможно, так как это часто является собственным размером пути данных, но это очень особый случай.Я также рекомендую использовать stdint.h, где это возможно.Если вы используете visual studio, вы можете найти лицензионные версии BSD.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...