Об использовании целых чисел со знаком в семействе языков C - PullRequest
10 голосов
/ 08 декабря 2011

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

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

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

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

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

Я сталкивался с этими вопросами: «Тип int по умолчанию: Signed или Unsigned?» и « Следует ли вам всегда использовать 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.

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

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

Ответы [ 5 ]

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

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

  • языки, такие как python, ruby, jscript и т. Д., Прекрасно работают без signed против unsigned. это может быть индикатором ...

2 голосов
/ 02 февраля 2017

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

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

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

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

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

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

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

И размер, и подпись важны, когда речь идет о написании надежного, переносимого и безопасного кода. По этой причине вы всегда должны использовать типы из stdint.h, а не нативные, так называемые «примитивные типы данных» языка C.


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

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

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

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


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

2 голосов
/ 08 декабря 2011

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

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

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

1 голос
/ 08 декабря 2011

Из 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 никогда не бывает отрицательным.

EDIT:

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

0 голосов
/ 08 декабря 2011

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

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

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

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

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

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

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

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

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

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

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

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