Необходимо ли всегда использовать 'интервал' для чисел в C, даже если они являются неотрицательными?

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

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.
}

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

46
задан Steve Hanov 15 July 2010 в 19:51
поделиться

16 ответов

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

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

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

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

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

Вот почему некоторые руководства по стилю, включая Google's C ++ Style Guide , не рекомендуют беззнаковые типы данных.

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

25
ответ дан 26 November 2019 в 19:59
поделиться

Я не знаю, возможно ли это в c, но в этом случае я бы просто преобразовал X-Y в int.

0
ответ дан 26 November 2019 в 19:59
поделиться

Если ваши числа не должны быть меньше нуля, но имеют шанс быть <0, во что бы то ни стало используйте целые числа со знаком и добавляйте утверждения или другие проверки во время выполнения. Если вы действительно работаете с 32-битными (или 64, или 16, в зависимости от вашей целевой архитектуры) значениями, где старший бит означает что-то иное, кроме «-», вы должны использовать только беззнаковые переменные для их хранения. Легче обнаружить целочисленные переполнения, когда число, которое всегда должно быть положительным, очень отрицательно, чем когда оно равно нулю, поэтому, если вам не нужен этот бит, используйте подписанные.

0
ответ дан 26 November 2019 в 19:59
поделиться

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

0
ответ дан 26 November 2019 в 19:59
поделиться

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

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

Опять же, если вы используете подписанные типы, вам все равно придется иногда учитывать переполнение:

MAX_INT + 1

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

2
ответ дан 26 November 2019 в 19:59
поделиться

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

2
ответ дан 26 November 2019 в 19:59
поделиться

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

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

6
ответ дан 26 November 2019 в 19:59
поделиться
if (bitsAvailable >= optionalDataSize + mandatoryDataSize) {
    // Optional data fits, so add it to the header.
}

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

6
ответ дан 26 November 2019 в 19:59
поделиться

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

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

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

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

bitsAvailable - обязательный размер данных> = optionalDataSize

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

bitsAvailable> = optionalDataSize + обязательный размер данных

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

7
ответ дан 26 November 2019 в 19:59
поделиться

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

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

11
ответ дан 26 November 2019 в 19:59
поделиться

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

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

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

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

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

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

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

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

9
ответ дан 26 November 2019 в 19:59
поделиться

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

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

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

Кроме того, выбор signed-vs.-unsigned может иметь практические последствия для производительности. Взгляните на (надуманный) код ниже:

#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. Поэтому выбор беззнаковых типов без разбора может неоправданно повлиять на производительность.

12
ответ дан 26 November 2019 в 19:59
поделиться

Одна вещь, о которой не упоминалось, - это то, что перестановка подписанных / беззнаковых чисел может привести к ошибкам безопасности . Это большая проблема, поскольку многие функции в стандартной 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;
    }
}

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

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

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

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

95
ответ дан 26 November 2019 в 19:59
поделиться

Предположим, вам нужно посчитать от 1 до 50000. Это можно сделать с помощью двухбайтового беззнакового целого числа, но не с помощью двухбайтового знакового целого числа (если пространство имеет такое большое значение).

0
ответ дан 26 November 2019 в 19:59
поделиться

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

void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize ) 
{ 
    signed __int64 available = bitsAvailable;
    signed __int64 mandatory = mandatoryDataSize;
    signed __int64 optional = optionalDataSize;

    if ( (mandatory + optional) <= available ) { 
        // Optional data fits, so add it to the header. 
    } 
} 

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

void CreateRequestHeader( unsigned int bitsAvailable, unsigned int mandatoryDataSize, unsigned int optionalDataSize ) 
{ 
    if ( bitsAvailable < mandatoryDataSize ) { 
        return;
    } 
    bitsAvailable -= mandatoryDataSize;

    if ( bitsAvailable < optionalDataSize ) { 
        return;
    } 
    bitsAvailable -= optionalDataSize;

    // Optional data fits, so add it to the header. 
} 
1
ответ дан 26 November 2019 в 19:59
поделиться

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

Jeffrey L. Whitledge

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

Эрик Липперт

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

3
ответ дан 26 November 2019 в 19:59
поделиться
Другие вопросы по тегам:

Похожие вопросы: