Об использовании целых чисел со знаком в семействе языков C
Используя целочисленные значения в моем собственном коде, я всегда стараюсь учитывать подписанность, спрашивая себя, должно ли целое число быть подписанным или беззнаковым.
Когда я уверен, что значение никогда не будет отрицательным, я использую целое число без знака.
И я должен сказать, что это происходит в большинстве случаев.
Читая код других людей, я редко вижу целые числа без знака, даже если представленное значение не может быть отрицательным.
Поэтому я спросил себя: "есть ли для этого веские причины, или люди просто используют целые числа со знаком, потому что им все равно"?
Я провел поиск по теме, здесь и в других местах, и должен сказать, что не могу найти вескую причину не использовать целые числа без знака, когда это применимо.
Я сталкивался с такими вопросами: "Тип int по умолчанию: Подписано или Без знака?" И " Следует ли вам всегда использовать int для чисел в C, даже если они неотрицательные? "Которые оба представляют следующий пример:
for( unsigned int i = foo.Length() - 1; i >= 0; --i ) {}
Для меня это просто плохой дизайн. Конечно, это может привести к бесконечному циклу с целыми числами без знака.
Но так ли сложно проверить, foo.Length()
0, до цикла?
Поэтому я лично не думаю, что это хорошая причина для использования целых чисел со знаком.
Некоторые люди могут также сказать, что целые числа со знаком могут быть полезны, даже для неотрицательных значений, для предоставления флага ошибки, обычно -1
,
Хорошо, хорошо иметь конкретное значение, которое означает "ошибка".
Но тогда, что не так с чем-то вроде UINT_MAX
для этого конкретного значения?
Я на самом деле задаю этот вопрос, потому что это может привести к огромным проблемам, обычно при использовании сторонних библиотек.
В таком случае вам часто приходится иметь дело со значениями со знаком и без знака.
Большую часть времени люди просто не заботятся о подписи, и просто назначают, например, unsigned int
к signed int
без проверки дальности.
Я должен сказать, что я немного параноидален с флагами предупреждения компилятора, поэтому при моей установке такое неявное приведение приведет к ошибке компилятора.
Для такого рода вещей я обычно использую функцию или макрос для проверки диапазона, а затем присваиваю, используя явное приведение, при необходимости вызывая ошибку.
Это просто кажется мне логичным.
В качестве последнего примера, поскольку я также являюсь разработчиком Objective-C (обратите внимание, что этот вопрос относится не только к Objective-C):
- ( NSInteger )tableView: ( UITableView * )tableView numberOfRowsInSection: ( NSInteger )section;
Для тех, кто не владеет Objective-C, NSInteger
целое число со знаком
Этот метод фактически извлекает количество строк в табличном представлении для определенного раздела.
Результат никогда не будет отрицательным значением (кстати, как номер раздела).
Так зачем использовать для этого целое число со знаком?
Я действительно не понимаю.
Это всего лишь пример, но я всегда вижу подобные вещи с C, C++ или Objective-C.
Итак, еще раз, мне просто интересно, если люди просто не заботятся о таких проблемах, или, наконец, есть веская и веская причина не использовать целые числа без знака для таких случаев.
Будем рады услышать ваши ответы:)
5 ответов
signed
возвращаемое значение может дать больше информации (например, число ошибок,0
иногда правильный ответ,-1
указывает на ошибку, см.man read
) ... что может быть актуально особенно для разработчиков библиотек.если вы беспокоитесь об одном дополнительном бите, который вы получаете при использовании
unsigned
вместоsigned
тогда вы, вероятно, в любом случае используете неправильный тип. (также своего рода аргумент "преждевременной оптимизации")такие языки, как python, ruby, jscript и т. д., прекрасно работают без
signed
противunsigned
, это может быть индикатором...
Используя целочисленные значения в моем собственном коде, я всегда стараюсь учитывать подписанность, спрашивая себя, должно ли целое число быть подписанным или беззнаковым.
Когда я уверен, что значение никогда не будет отрицательным, я использую целое число без знака. И я должен сказать, что это происходит в большинстве случаев.
Тщательно продумывать, какой тип наиболее подходит каждый раз, когда вы объявляете переменную, это очень хорошая практика! Это означает, что вы осторожны и профессиональны. Вы должны учитывать не только подпись, но и потенциальное максимальное значение, которое, как вы ожидаете, будет иметь этот тип.
Причина, по которой вы не должны использовать подписанные типы, когда они не нужны, не имеет ничего общего с производительностью, а с безопасностью типов. Есть много потенциальных, тонких ошибок, которые могут быть вызваны подписанными типами:
Различные формы неявного продвижения, которые существуют в C, могут заставить ваш тип изменить подпись неожиданным и, возможно, опасным образом. Целочисленное правило продвижения, являющееся частью обычных арифметических преобразований, преобразование lvalue при присваивании, продвижения аргументов по умолчанию, используемые, например, списками VA, и так далее.
При использовании любой формы побитовых операторов или аналогичного аппаратного программирования подписанные типы опасны и могут легко вызывать различные формы неопределенного поведения.
Объявляя ваши целые числа без знака, вы автоматически пропускаете множество вышеупомянутых опасностей. Точно так же, объявив их как unsigned int
или больше, вы избавляетесь от многих опасностей, вызванных целочисленными акциями.
Как размер, так и подпись важны, когда речь идет о написании надежного, переносимого и безопасного кода. Это причина, почему вы всегда должны использовать типы из stdint.h
а не родные, так называемые "примитивные типы данных" C.
Поэтому я спросил себя: "есть ли для этого веские причины, или люди просто используют целые числа со знаком, потому что им все равно"?
Я действительно не думаю, что это потому, что им все равно, или потому что они ленивы, хотя объявляют все int
иногда упоминается как "небрежная типизация" - что означает небрежно выбранный тип, а не слишком ленивый для ввода.
Я скорее верю, что это потому, что им не хватает более глубокого знания различных вещей, которые я упомянул выше. Есть пугающее количество опытных программистов на C, которые не знают, как неявные продвижения типов работают в C, и как подписанные типы могут вызывать плохо определенное поведение при использовании вместе с определенными операторами.
Это на самом деле очень частый источник тонких ошибок. Многие программисты смотрят на предупреждение компилятора или на специфическую ошибку, которую они могут устранить, добавив приведение. Но они не понимают почему, они просто добавляют актерский состав и идут дальше.
for (без знака int i = foo.Length() - 1; i >= 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, но, по крайней мере, надежный, профессиональный документ (и в большей степени ориентированный на разработку для настольных компьютеров и хостинга).
Есть один весомый аргумент против целых чисел без знака:
Преждевременная оптимизация - корень всего зла.
Мы все по крайней мере в одном случае были укушены целыми числами без знака. Иногда как в вашей петле, иногда в других контекстах. Целые числа без знака добавляют опасность, пусть даже небольшую, вашей программе. И вы вводите эту опасность, чтобы изменить значение одного бита. Один маленький, крошечный, незначительный бит, но для его значения знака. С другой стороны, целые числа, с которыми мы работаем в приложениях типа "хлеб-масло", часто намного ниже диапазона целых чисел, порядка 10^1, а не 10^7. Таким образом, другой диапазон целых чисел без знака в подавляющем большинстве случаев не требуется. И когда это необходимо, вполне вероятно, что этот дополнительный бит не обрежет его (когда 31 слишком мало, 32 достаточно редко), и вам все равно понадобится более широкое или произвольное целое число. Прагматичный подход в этих случаях состоит в том, чтобы просто использовать целое число со знаком и избавить себя от случайной ошибки при переполнении. Ваше время как программиста может быть использовано намного лучше.
Из C FAQ:
Первый вопрос в C FAQ: какой целочисленный тип мы должны использовать?
Если вам могут понадобиться большие значения (выше 32 767 или ниже -32 767), используйте long. В противном случае, если пространство очень важно (например, если есть большие массивы или много структур), используйте short. В противном случае используйте int. Если четко определенные характеристики переполнения важны, а отрицательные значения - нет, или если вы хотите избежать проблем с расширением знака при манипулировании битами или байтами, используйте один из соответствующих типов без знака.
Другой вопрос касается преобразования типов:
Если операция включает в себя целые числа со знаком и без знака, ситуация немного сложнее. Если операнд без знака меньше (возможно, мы работаем с unsigned int и long int), так что больший тип со знаком может представлять все значения меньшего типа без знака, тогда значение без знака преобразуется в больший тип со знаком. и результат имеет больший тип со знаком. В противном случае (то есть, если подписанный тип не может представлять все значения типа без знака), оба значения преобразуются в общий тип без знака, и результат имеет этот тип без знака.
Вы можете найти это здесь. Таким образом, использование целых чисел без знака, в основном для арифметических преобразований, может усложнить ситуацию, поскольку вам придется либо сделать все ваши целые числа без знака, либо рискнуть перепутать компилятор и себя, но если вы знаете, что делаете, это на самом деле не риск как таковой. Тем не менее, это может привести к простым ошибкам.
И когда хорошо использовать целые числа без знака? одна ситуация при использовании побитовых операций:
Оператор << сдвигает свой первый операнд влево на число битов, заданных его вторым операндом, заполняя новые 0 битов справа. Аналогично, оператор >> смещает свой первый операнд вправо. Если первый операнд не подписан, >> заполняет 0 бит слева, но если первый операнд подписан, >> может заполнить 1 бит, если старший бит уже равен 1. (Подобная неопределенность является одной из причин, почему Обычно рекомендуется использовать все беззнаковые операнды при работе с побитовыми операторами.)
взято отсюда И я где-то видел это:
Если бы было лучше использовать целые числа без знака для значений, которые никогда не бывают отрицательными, мы бы начали с использования unsigned int в основной функции
int main(int argc, char* argv[])
, Одно можно сказать наверняка, argc никогда не бывает отрицательным.
РЕДАКТИРОВАТЬ:
Как уже упоминалось в комментариях, подпись main
это связано с историческими причинами и, по-видимому, предшествует существованию беззнакового ключевого слова.
Неподписанные целые числа являются артефактом из прошлого. Это из того времени, когда процессоры могли делать арифметику без знака немного быстрее.
Это случай преждевременной оптимизации, которая считается злой.
Фактически, в 2005 году, когда AMD представила 64-разрядную архитектуру для x86 x86_64 (или AMD64, как ее тогда называли), они вернули призраки прошлого: если в качестве индекса используется целое число со знаком, а компилятор не может доказать что он никогда не бывает отрицательным, он должен вставить инструкцию расширения 32-64-разрядного знака - потому что 32-разрядное расширение по умолчанию является беззнаковым (верхняя половина 64-разрядного регистра очищается, если в него переместить 32-разрядное значение),
Но я бы рекомендовал не использовать unsigned в любой арифметике, будь то указатель арифметики или просто простые числа.
for( unsigned int i = foo.Length() - 1; i >= 0; --i ) {}
Любой недавний компилятор будет предупреждать о такой конструкции с условием ist всегда true или подобным. Используя переменную со знаком, вы вообще избегаете таких ловушек. Вместо этого используйте ptrdiff_t
,
Проблемой может быть библиотека C++, она часто использует тип без знака для size_t
, что требуется из-за некоторых редких угловых случаев с очень большими размерами (между 2^31 и 2^32) в 32-битных системах с определенными загрузочными переключателями (окна /3GB).
На мой взгляд, есть еще много сравнений между подписанным и неподписанным, где значение со знаком автоматически переводится в число без знака и, таким образом, становится огромным положительным числом, если раньше оно было небольшим отрицательным.
Одно исключение для использования unsigned
существует: для битовых полей, флагов, масок это довольно часто. Обычно вообще не имеет смысла интерпретировать значение этих переменных как величину, и читатель может сделать вывод из типа, что эта переменная должна интерпретироваться в битах.
Результат никогда не будет отрицательным значением (кстати, как номер раздела). Так зачем использовать для этого целое число со знаком?
Потому что вы можете сравнить возвращаемое значение со значением со знаком, которое на самом деле является отрицательным. Сравнение должно вернуть true в этом случае, но стандарт C указывает, что подписанный get будет повышен до unsigned в этом случае, и вместо этого вы получите false. Я не знаю о ObjectiveC, хотя.