Если вы всегда используете "int" для чисел в C, даже если они неотрицательны?

Я всегда использую unsigned 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 для чисел, даже если они не может быть отрицательным?

Ответ 1

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

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

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

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

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

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

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

Ответ 2

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

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

Ответ 3

В некоторых случаях, когда вы должны использовать целые типы без знака, выполните следующие действия:

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

Но для общей арифметики дело в том, что когда вы говорите, что что-то "не может быть отрицательным", это не обязательно означает, что вы должны использовать неподписанный тип. Поскольку вы можете поместить отрицательное значение в unsigned, это просто, что он станет действительно большим значением, когда вы пойдете, чтобы его получить. Итак, если вы имеете в виду, что отрицательные значения запрещены, например, для основной функции квадратного корня, тогда вы указываете предварительное условие функции, и вы должны утверждать. И вы не можете утверждать, что не может быть; вам нужен способ удерживать значения вне диапазона, чтобы вы могли проверить их (это та же логика, что и 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 очень велико, и, следовательно, для этого случая должен быть код. С другой стороны, подписанное арифметическое переполнение undefined, поэтому GCC будет идти вперед и предположить, что подписанная арифметика не переполняется, и поэтому (a + 69) не может быть меньше a. Поэтому выбор беззнаковых типов без разбора может привести к неоправданному воздействию производительности.

Ответ 4

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

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

Ответ 5

Ответ: Да. "Unsigned" int type C и С++ не является "всегда положительным целым числом", независимо от того, как выглядит имя типа. Поведение C/С++ unsigned ints не имеет смысла, если вы попытаетесь прочитать этот тип как "неотрицательный"... например:

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

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

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

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

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

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

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

Ответ 6

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

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

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

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

bitsAvailable – mandatoryDataSize >= optionalDataSize

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

bitsAvailable >= optionalDataSize + mandatoryDataSize

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

Ответ 7

if (bitsAvailable >= optionalDataSize + mandatoryDataSize) {
    // Optional data fits, so add it to the header.
}

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

Ответ 8

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

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

Ответ 9

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

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

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

Эрик Липперт

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

Ответ 10

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

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

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

MAX_INT + 1

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

Ответ 11

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

Ответ 12

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

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

Ответ 13

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

Ответ 14

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

Ответ 15

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

Ответ 16

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