Следует ли вам всегда использовать 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 для чисел, даже если они не могут быть отрицательными?
16 ответов
Должен ли я всегда...
Ответ на вопрос "Должен ли я всегда..." почти наверняка "нет", есть много факторов, которые определяют, следует ли использовать тип данных, поэтому важна согласованность.
Но это очень субъективный вопрос, действительно легко испортить неподписанные:
for (unsigned int i = 10; i >= 0; i--);
приводит к бесконечному циклу.
Вот почему некоторые руководства по стилю, включая Google C++ Style Guide, не поощряют unsigned
типы данных.
По моему личному мнению, я не сталкивался со многими ошибками, вызванными этими проблемами с неподписанными типами данных - я бы сказал, используйте утверждения для проверки вашего кода и используйте их разумно (и меньше, когда вы выполняете арифметику).
Одна вещь, которая не была упомянута, - то, что обмен номерами со знаком / без знака может привести к ошибкам безопасности. Это большая проблема, поскольку многие функции в стандартной 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
подписан, но преобразуется в неподписанный при передаче 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
для размеров.
Человек, программирование сложно.
Некоторые случаи, когда вы должны использовать целочисленные типы без знака:
- Вы должны рассматривать данные как чистое двоичное представление.
- Вам нужна семантика арифметики по модулю, которую вы получаете с числами без знака.
- Вы должны взаимодействовать с кодом, который использует неподписанные типы (например, стандартные библиотечные процедуры, которые принимают / возвращают
size_t
ценности.
Но для общей арифметики дело в том, что когда вы говорите, что что-то "не может быть отрицательным", это не обязательно означает, что вы должны использовать тип без знака. Поскольку вы можете поместить отрицательное значение в неподписанное, просто оно станет действительно большим значением, когда вы его получите. Итак, если вы имеете в виду, что отрицательные значения запрещены, например, для базовой функции квадратного корня, то вы указываете предварительное условие функции, и вам следует утверждать. И вы не можете утверждать, что не может быть, есть; вам нужен способ хранения внеполосных значений, чтобы вы могли проверить их (это та же самая логика getchar()
возвращая int
и не char
.)
Кроме того, выбор подписи против неподписания также может иметь практические последствия для производительности. Взгляните на (надуманный) код ниже:
#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".текст.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
, Поэтому выбор неподписанных типов без разбора может излишне повлиять на производительность.
Ответ - да. Тип unsigned int в C и C++ не является "всегда положительным целым числом", независимо от того, как выглядит имя типа. Поведение C/C++ беззнаковых целочисленных значений не имеет смысла, если вы пытаетесь считать тип "неотрицательным"... например:
- Разница между двумя беззнаковыми числами является числом без знака (не имеет смысла, если вы читаете его как "Разница между двумя неотрицательными числами неотрицательна")
- Добавление int и неподписанного int является неподписанным
- Существует неявное преобразование из int в unsigned int (если вы читаете unsigned как "неотрицательное", это имеет смысл противоположное преобразование)
- Если вы объявляете функцию, принимающую параметр без знака, когда кто-то передает отрицательное значение типа int, вы просто неявно преобразуете его в огромное положительное значение; другими словами, использование беззнакового типа параметра не помогает вам находить ошибки ни во время компиляции, ни во время выполнения.
Действительно, числа без знака очень полезны для определенных случаев, потому что они являются элементами кольца "целые числа по модулю N", где N - степень двойки. Целые числа без знака полезны, когда вы хотите использовать эту арифметику по модулю или как битовые маски; они НЕ полезны как количества.
К сожалению, в C и C++ unsigned также использовались для представления неотрицательных величин, чтобы иметь возможность использовать все 16 битов, когда целые числа были такими маленькими... в то время возможность использовать 32k или 64k считалась большой разницей. Я бы классифицировал это в основном как историческую случайность... вы не должны пытаться читать логику в этом, потому что не было логики.
Кстати, на мой взгляд, это было ошибкой... если 32 КБ недостаточно, то довольно скоро 64 КБ тоже будет недостаточно; злоупотребление целым числом по модулю только из-за одного дополнительного бита, по моему мнению, было слишком дорого, чтобы заплатить. Конечно, было бы разумно сделать это, если бы присутствовал или был определен надлежащий неотрицательный тип... но беззнаковая семантика просто неверна для использования его как неотрицательного.
Иногда вы можете обнаружить, кто говорит, что unsigned хорош, потому что он "документирует", что вам нужны только неотрицательные значения... однако эта документация имеет значение только для людей, которые на самом деле не знают, как unsigned работает для C или C++. Для меня видение типа без знака, используемого для неотрицательных значений, просто означает, что тот, кто написал код, не понимал язык этой части.
Если вы действительно понимаете и хотите "обернуть" поведение беззнаковых целочисленных значений, то это правильный выбор (например, я почти всегда использую "беззнаковое целое" при обработке байтов); если вы не собираетесь использовать поведение обтекания (и это поведение будет для вас проблемой, как в случае разницы, которую вы показали), то это явный показатель того, что неподписанный тип является плохим выбором, и вы следует придерживаться простых вставок.
Означает ли это, что C++ std::vector<>::size()
тип возврата плохой выбор? Да... это ошибка Но если вы так говорите, будьте готовы к тому, что вас будут называть дурными именами, которые не понимают, что "беззнаковое" имя - это просто имя... то, что оно считает, - это поведение, а это - "по модулю n" (и не можно было бы подумать, что тип "по модулю-n" для размера контейнера - разумный выбор).
Бьярн Страуструп, создатель C++, предупреждает об использовании неподписанных типов в своей книге "Язык программирования C++":
Целочисленные типы без знака идеальны для применений, которые рассматривают хранилище как битовый массив. Использование unsigned вместо int для получения еще одного бита для представления положительных целых чисел почти никогда не является хорошей идеей. Попытки обеспечить положительные значения некоторых значений, объявив переменные без знака, будут, как правило, отвергнуты правилами неявного преобразования.
Я, кажется, не согласен с большинством людей здесь, но я нахожу unsigned
Типы довольно полезны, но не в их необработанном историческом виде.
Следовательно, если вы придерживаетесь семантики, которую представляет для вас тип, тогда проблем быть не должно: size_t
(без знака) для индексов массивов, смещений данных и т. д. off_t
(подписано) для смещения файлов. использование ptrdiff_t
(подписано) для различий указателей. использование uint8_t
для небольших целых чисел без знака и int8_t
для подписанных. И вы избежите не менее 80% проблем с переносимостью.
И не используйте int
, long
, unsigned
, char
если ты не должен Они принадлежат в учебниках истории. (Иногда необходимо, ошибки возвращаются, битовые поля, например)
И вернемся к вашему примеру:
bitsAvailable – mandatoryDataSize >= optionalDataSize
может быть легко переписан как
bitsAvailable >= optionalDataSize + mandatoryDataSize
которая не устраняет проблему потенциального переполнения (assert
это твой друг) но, я думаю, немного приблизит тебя к идее того, что ты хочешь проверить.
Вы не можете полностью избежать неподписанных типов в переносимом коде, потому что многие определения типов в стандартной библиотеке являются неподписанными (особенно size_t
), и многие функции возвращают их (например, std::vector<>::size()
).
Тем не менее, я обычно предпочитаю придерживаться подписанных типов, где это возможно, по причинам, которые вы изложили. Это не просто случай, который вы затронули - в случае смешанной арифметики со знаком / без знака, аргумент со знаком незаметно переводится в беззнаковое.
if (bitsAvailable >= optionalDataSize + mandatoryDataSize) {
// Optional data fits, so add it to the header.
}
Безошибочная, до тех пор, пока requiredDataSize + optionDataSize не может переполнить целочисленный тип без знака - именование этих переменных заставляет меня полагать, что это, вероятно, имеет место.
Из комментариев к одному из сообщений в блоге Эрика Липперта (см. Здесь):
Джеффри Л. Уитледж
Однажды я разработал систему, в которой отрицательные значения не имеют смысла в качестве параметра, поэтому вместо того, чтобы проверять, чтобы значения параметров были неотрицательными, я подумал, что будет хорошей идеей просто использовать вместо этого uint. Я быстро обнаружил, что всякий раз, когда я использовал эти значения для чего-либо (например, для вызова методов BCL), они преобразовывались в целые числа со знаком. Это означало, что мне пришлось проверить, что значения не превышают диапазон целых чисел со знаком в верхней части, поэтому я ничего не получил. Кроме того, каждый раз, когда вызывался код, используемые целые числа (часто полученные из функций BCL) приходилось преобразовывать в uints. Прошло совсем немного времени, прежде чем я сменил все эти уинты обратно на инты и удалил все ненужное изгнание. Я все еще должен подтвердить, что числа не отрицательны, но код намного чище!
Эрик Липперт
Сам не мог бы сказать это лучше. Вы почти никогда не нуждаетесь в диапазоне значений uint, и они не соответствуют CLS. Стандартный способ представления небольшого целого числа - это int, даже если там есть значения, выходящие за пределы диапазона. Хорошее практическое правило: используйте "uint" только для ситуаций, когда вы взаимодействуете с неуправляемым кодом, который ожидает uints, или когда целое число явно используется как набор битов, а не число. Всегда старайтесь избегать этого в общедоступных интерфейсах. - Эрик
Ситуация, когда (bitsAvailable – mandatoryDataSize)
выдает "неожиданный" результат, когда типы не подписаны и bitsAvailable < mandatoryDataSize
является причиной того, что иногда используются подписанные типы, даже если ожидается, что данные никогда не будут отрицательными.
Я думаю, что нет жесткого и быстрого правила - я обычно "по умолчанию" использую неподписанные типы для данных, у которых нет причин быть отрицательными, но тогда вам нужно принять меры, чтобы арифметическая упаковка не выявляла ошибок.
Опять же, если вы используете подписанные типы, вам все равно придется иногда учитывать переполнение:
MAX_INT + 1
Ключ в том, что вы должны позаботиться, выполняя арифметику для подобных ошибок.
Нет, вы должны использовать тип, который подходит для вашего приложения. Там нет золотого правила. Иногда на небольших микроконтроллерах, например, более быстро и эффективно использовать память, скажем, 8 или 16-битные переменные, где это возможно, так как это часто является собственным размером пути данных, но это очень особый случай. Я также рекомендую использовать stdint.h, где это возможно. Если вы используете visual studio, вы можете найти лицензионные версии BSD.
Если существует вероятность переполнения, присвойте значения следующему наивысшему типу данных во время вычисления, а именно:
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.
}
Если ваши числа никогда не должны быть меньше нуля, но имеют шанс быть < 0, во что бы то ни стало используйте целые числа со знаком и посыпьте утверждения или другие проверки во время выполнения. Если вы на самом деле работаете с 32-битными (или 64, или 16, в зависимости от вашей целевой архитектуры) значениями, где старший значащий бит означает что-то отличное от "-", вы должны использовать только переменные без знака для их хранения. Целочисленные переполнения легче обнаружить, когда число, которое всегда должно быть положительным, очень отрицательно, чем когда оно равно нулю, поэтому, если вам не нужен этот бит, используйте со знаком.
Предположим, вам нужно сосчитать от 1 до 50000. Вы можете сделать это с помощью двухбайтового целого числа без знака, но не с помощью двухбайтового целого числа со знаком (если пробел так важен).
Я не знаю, возможно ли это в c, но в этом случае я просто приведу XY к int.
Вам нужно будет посмотреть на результаты операций, которые вы выполняете над переменными, чтобы проверить, можете ли вы получить переполнение / переполнение - в вашем случае результат может быть отрицательным. В этом случае вам лучше использовать подписанные эквиваленты.