Об использовании знаковых целых чисел в C (и производных языках)

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

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

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

Итак, я спросил себя: "есть ли веская причина для этого или люди просто используют целые числа со знаком, потому что им все равно" ?

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

Я столкнулся с этими вопросами: "По умолчанию тип int: Signed or Unsigned?" и "Если вы всегда используете 'int' для чисел в C, даже если они неотрицательны?", которые оба приводят следующий пример:

for( unsigned int i = foo.Length() - 1; i >= 0; --i ) {}

Для меня это просто плохой дизайн. Конечно, это может привести к бесконечному циклу с целыми числами без знака.
Но так сложно проверить, если foo.Length() равно 0, перед циклом?

Поэтому я лично не думаю, что это хорошая причина для использования целых чисел со знаком.

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

Хорошо, что хорошо иметь определенное значение, что означает "ошибка".
Но тогда, что не так с чем-то вроде UINT_MAX, для этого конкретного значения?

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

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

В большинстве случаев люди просто не интересуются подписью и просто назначают, например, unsigned int, на signed int, не проверяя диапазон.

Я должен сказать, что я немного параноик с флагами предупреждений компилятора, поэтому с моей установкой такой неявный приведение приведет к ошибке компилятора. < br/" >

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

Мне это кажется логичным.

В качестве последнего примера, поскольку я также разработчик Objective-C (обратите внимание, что этот вопрос не связан только с Objective-C):

- ( NSInteger )tableView: ( UITableView * )tableView numberOfRowsInSection: ( NSInteger )section;

Для тех, кто не владеет текстом Objective-C, NSInteger является целым знаком.
Этот метод фактически извлекает количество строк в представлении таблицы для определенного раздела.

Результат будет никогда отрицательным (как номер раздела, кстати).

Итак, зачем использовать целое число со знаком для этого?
Я действительно не понимаю.

Это всего лишь пример, но я всегда вижу такие вещи с помощью C, С++ или Objective-C.

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

С нетерпением ждем ваших ответов:)

Ответ 1

  • a signed возвращаемое значение может дать больше информации (думаю, что числа ошибок, 0 иногда является допустимым ответом, -1 указывает на ошибку, см. man read)... что может быть особенно актуальным для разработчиков библиотек.

  • если вы беспокоитесь об одном дополнительном бите, который вы получаете при использовании unsigned вместо signed, тогда вы, вероятно, используете неправильный тип. (также как аргумент "преждевременной оптимизации" )

    Языки
  • такие как python, ruby, jscript и т.д., отлично работают без signed vs unsigned. это может быть индикатором...

Ответ 2

Существует один весовой аргумент против целых чисел без знака:

Преждевременная оптимизация - это корень всего зла.

Мы все, по крайней мере, однажды были укушены целыми числами без знака. Иногда, как в вашем цикле, иногда в других контекстах. Целые числа без знака добавляют к вашей программе опасность, хотя и небольшую. И вы вводите эту опасность для изменения значения одного бита. Один маленький, крошечный, незначительный, но для-его-знаковый бит. С другой стороны, целые числа, с которыми мы работаем в приложениях с хлебом и маслом, часто намного ниже диапазона целых чисел, больше порядка 10 ^ 1, чем 10 ^ 7. Таким образом, различный диапазон целых чисел без знака в подавляющем большинстве случаев не нужен. И когда это необходимо, вполне вероятно, что этот дополнительный бит не будет его обрезать (когда 31 слишком мало, 32 редко бывает достаточно), и в любом случае вам понадобится более широкое или произвольное целое число. Прагматический подход в этих случаях заключается в том, чтобы просто использовать целое число со знаком и избавить себя от случайного сбоя. Ваше время как программиста может быть значительно лучше использовано.

Ответ 3

Из C FAQ:

первый вопрос в часто задаваемых вопросах C - какой тип целого мы должны использовать?

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

Другой вопрос касается преобразований типов:

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

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

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

< < оператор сдвигает свой первый операнд, оставленный несколькими битами заданный его вторым операндом, заполняя новые 0 бит справа. Аналогичным образом оператор → сдвигает свое первое право операнда. Если первый операнд без знака, → заполняет 0 бит слева, но если первый операнд подписан, → может заполнить 1 бит, если высокий порядок бит был уже 1. (Неопределенность, подобная этому, является одной из причин, почему обычно рекомендуется использовать все неподписанные операнды при работе с побитовые операторы.)

взято из здесь И я видел это где-то:

Если было бы лучше использовать целые числа без знака для значений, которые никогда не являются отрицательными, мы бы начали с использования unsigned int в основной функции int main(int argc, char* argv[]). Одно можно сказать наверняка, argc никогда не бывает отрицательным.

EDIT:

Как упоминалось в комментариях, подпись main объясняется историческими причинами и, по-видимому, предшествует существованию неподписанного ключевого слова.

Ответ 4

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

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

Чтобы тщательно рассмотреть, какой тип наиболее подходит каждый раз, когда вы объявляете переменную, очень хорошая практика! Это означает, что вы осторожны и профессиональны. Вы должны не только учитывать подпись, но и потенциальное максимальное значение, которое вы ожидаете от этого типа.

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

  • Различные формы неявных рекламных акций, которые существуют в C, могут привести к тому, что ваш тип изменит подпись на неожиданных и, возможно, опасных способах. Целевое правило продвижения, которое является частью обычных арифметических преобразований, преобразование lvalue при присваивании, рекламные объявления по умолчанию, используемые, например, списками VA, и так далее.

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

Объявляя целые числа без знака, вы автоматически пропускаете много из вышеперечисленных опасностей. Точно так же, объявив их размером unsigned int или больше, вы избавитесь от множества опасностей, вызванных целыми рекламными акциями.

Оба размера и подписи важны, когда речь идет о написании надежного, портативного и безопасного кода. Именно поэтому вы всегда должны использовать типы из stdint.h, а не нативные, так называемые "примитивные типы данных" C.


Поэтому я спросил себя: "Есть ли веская причина для этого, или люди просто используют знаковые целые числа, потому что им все равно"?

Я действительно не думаю, что это потому, что им все равно, или потому, что они ленивы, хотя объявление всего int иногда называют "неряшливым типом" - это означает, что sloppily выбрал тип больше, чем это означает слишком ленив, чтобы напечатать.

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

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


for (unsigned int я = foo.Length() - 1; я >= 0; --i) {}

Для меня это просто плохая конструкция

В самом деле.

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

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

for(unsigned int i=0; i<foo.Length(); i++)
{
  unsigned int index = foo.Length() - i - 1;
  thing[index] = something;
}

Этот код не должен влиять на производительность, но сам цикл стал намного проще читать, в то же время исправляя ошибку, которую имел ваш пример.

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


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

Это плохой аргумент. Хороший API-интерфейс использует специальный тип ошибки для сообщений об ошибках, таких как перечисление.

Вместо того, чтобы иметь некоторый API-интерфейс на уровне хобби, например

int do_stuff (int a, int b); // returns -1 if a or b were invalid, otherwise the result

у вас должно получиться что-то вроде:

err_t do_stuff (int32_t a, int32_t b, int32_t* result);

// returns ERR_A is a is invalid, ERR_B if b is invalid, ERR_XXX if... and so on
// the result is stored in [result], which is allocated by the caller
// upon errors the contents of [result] remain untouched

Затем API будет последовательно резервировать возврат каждой функции для этого типа ошибки.

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


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

Например, руководство по стилю Google является сомнительным. Аналогично можно сказать о множестве других таких стандартов кодирования, которые используют "доказательство по авторитету". Просто потому, что в нем говорится о Google, NASA или Linux, люди слепо проглатывают их независимо от качества фактического содержимого. В этих стандартах есть хорошие вещи, но они также содержат субъективные мнения, спекуляции или вопиющие ошибки.

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

Существует также CERT C, который не так подробно и осторожен, как MISRA, но, по крайней мере, звуковой, профессиональный документ (и более сфокусированный на настольных/размещенных разработках).

Ответ 5

Неподписанные интриги - это артефакт из прошлого. Это происходит с того времени, когда процессоры могут делать арифметику без знака немного быстрее.

Это случай преждевременной оптимизации, которая считается злой.

На самом деле, в 2005 году, когда AMD представила x86_64 (или AMD64, как ее тогда называли), 64-битную архитектуру для x86, они принесли призраки прошлого: если знаковое целое используется как индекс, а компилятор не может доказать, что он никогда не является отрицательным, должен вставить инструкцию расширения расширений от 32 до 64 бит, потому что расширение с 32 по 64 бит по умолчанию не указано (верхняя половина 64-разрядного регистра становится чистым, если вы перемещаете 32-битное значение в него).

Но я бы рекомендовал против использования unsigned в любой арифметике вообще, будучи арифметикой указателя или просто простыми числами.

for( unsigned int i = foo.Length() - 1; i >= 0; --i ) {}

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

Проблемой может быть библиотека С++, она часто использует unsigned type для size_t, что требуется из-за некоторых редких угловых случаев с очень большими размерами (между 2 ^ 31 и 2 ^ 32) на 32-битных системах с некоторые загрузочные переключатели (/3GB окна).

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

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

Результат никогда не будет отрицательным (как номер раздела, кстати). Итак, зачем использовать для этого целое число со знаком?

Потому что вы можете сравнить возвращаемое значение с подписанным значением, что на самом деле отрицательно. В этом случае сравнение должно возвращать значение true, но в стандарте C указано, что подписанный get будет повышен до unsigned в этом случае, и вместо этого вы получите false. Я не знаю об ObjectiveC, хотя.