Почему у нас есть неподписанный и подписанный тип int в C?
Я новичок в C. Недавно я узнал о 2 Complement
и других способах представления отрицательного числа и почему 2 Complement
был наиболее подходящим.
Что я хочу спросить, например,
int a = -3;
unsigned int b = -3; //This is the interesting Part.
Теперь для преобразования типа int
В стандарте говорится:
6.3.1.3 Целочисленные и беззнаковые целые числа
Когда значение с целым типом преобразуется в другой целочисленный тип, отличный от _Bool, if значение может быть представлено новым типом, оно не изменяется.
В противном случае, если новый тип без знака, значение преобразуется путем многократного добавления или вычитая одно больше максимального значения, которое может быть представлено в новом типе пока значение не будет в диапазоне нового типа.
Первый абзац нельзя использовать, поскольку -3
не может быть представлен unsigned int
.
Поэтому параграф 2 подходит для игры, и нам нужно знать максимальное значение для unsigned int. Его можно найти как UINT_MAX в limits.h. Максимальное значение в этом случае равно 4294967295
, поэтому вычисление:
-3 + UINT_MAX + 1 = -3 + 4294967295 + 1 = 4294967293
Теперь 4294967293
в двоичном формате 11111111 11111111 11111111 11111101
и -3
в 2 форме дополнения 11111111 11111111 11111111 11111101
, поэтому они являются по существу одним и тем же представлением бит, это всегда было бы одинаково независимо от того, какое отрицательное целое я пытаюсь назначить без знака int.So не является неподписанным типом избыточным.
Теперь я знаю, что printf("%d" , b)
- это поведение undefined в соответствии со стандартом, но не то, что разумный и интуитивный способ делать что-то. Поскольку представление будет таким же, если отрицательные представлены как 2 Complement
, и это то, что мы имеем сейчас, а другие способы используются редко и, скорее всего, не будут в будущих разработках.
Итак, если бы мы могли использовать только один тип int, теперь, если int x = -1
, то %d
проверяет знаковый бит и печатает отрицательное число, если знак бит 1
и %u
всегда интерпретирует двоичную цифру ( бит), как есть. Сложение и вычитание уже решены из-за использования 2 Complement
. Так что это не более интуитивный и менее сложный способ делать что-то.
Ответы
Ответ 1
Это удобно для ввода, вывода и вычисления. Например, сравнение и деление поступают в подписанные и неподписанные разновидности (кстати, при умножении на битовый уровень одинаково для беззнакового и 2-символьных типов подписей, как сложение и вычитание, и оба могут компилироваться в одну и ту же инструкцию умножения CPU). Кроме того, неподписанные операции не вызывают поведение undefined в случае переполнения (за исключением деления на ноль), в то время как операции с подписью выполняются. В целом, беззнаковая арифметика хорошо определена, а неподписанные типы имеют одно представление (в отличие от трех разных для подписанных типов, хотя в наши дни на практике есть только один).
Там интересный поворот. Современные компиляторы C/С++ используют тот факт, что подписанные переполнения приводят к поведению undefined. Логика заключается в том, что ее никогда не бывает, и поэтому можно сделать некоторые дополнительные оптимизации. Если это действительно так, стандарт говорит, что это поведение undefined, и ваша багги-программа юридически завинчена. Это означает, что вам следует избегать подписанных переполнений и всех других форм UB. Однако иногда вы можете тщательно писать код, который никогда не приводит к UB, но немного эффективнее с подписанной арифметикой, чем с unsigned.
Изучите undefined, неопределенное и поведение, определяемое реализацией. Все они перечислены в конце стандарта в одном из приложений (J?).
Ответ 2
Мой ответ более абстрактный, на мой взгляд, на C вам не нужно заботиться о представлении целого в памяти. C абстрактно это вам, и это очень хорошо.
Объявить целое число как unsigned
очень полезно. Это предполагает, что значение никогда не будет отрицательным. Подобно действительному числу дескрипторов с плавающим числом, signed
целочисленный дескриптор... integer и unsigned
целочисленный дескриптор натурального числа.
Когда вы создаете алгоритм, где отрицательное целое число приведет к поведению undefined. Вы можете быть уверены, что ваше беззнаковое целочисленное значение никогда не будет отрицательным. Например, когда вы перебираете индекс массива. Отрицательный индекс приведет к поведению undefined.
Другое дело, когда вы создаете публичный API, когда для одной из ваших функций требуются размер, длина, вес или что-то еще, что не имеет смысла в негативе. Это помогает пользователю понять цель этого значения.
С другой стороны, некоторые люди не согласны с тем, что арифметика unsigned
не работает, как люди ожидают. Потому что, когда a unsigned
уменьшается, когда он равен нулю, он переходит к очень большому значению. Некоторые люди ожидают, что он будет равен -1
. Например:
// wrong
for (size_t i = n - 1; i >= 0; i--) {
// important stuff
}
Это создает бесконечный цикл или даже хуже, если n равно нулю, компилятор, вероятно, обнаружит его, но не все время:
// wrong
size_t min = 0;
for (size_t i = n - 1; i >= min; i--) {
// important stuff
}
Выполнение этого с помощью целых чисел без знака требует небольшого трюка:
size_t i = n;
while (i-- > 0) {
// important stuff
}
На мой взгляд, очень важно иметь unsigned
целое число в языке и C не будет полным без.
Ответ 3
Я думаю, что основная причина заключается в том, что операторы и операции зависят от подписанности.
Вы заметили, что add/subtract ведет себя одинаково для подписанных и неподписанных типов, если подписанные типы используют 2 комплимента (и вы игнорировали тот факт, что это "если" иногда не так).
Существует множество случаев, когда компилятору нужна информация о подписке, чтобы понять цель программы.
1. Целое продвижение.
Когда более узкий тип преобразуется в более широкий тип, компилятор будет генерировать код в зависимости от типов операндов.
например. если вы конвертируете signed short
в signed int
и int
шире, чем short
, компилятор будет генерировать код, который выполняет преобразование, и это преобразование отличается от "unsigned short" до "signed int" (расширение знака или нет).
2. Арифметический сдвиг вправо
-1>>1
может быть еще -1
, если реализация выбрала, но 0xffffffffu>>1
должна быть 0x7fffffffu
3. Целочисленное разделение
Аналогично, -1/2
является 0
, 0xffffffffu/2
является 0x7fffffffu
4. 32 бит умножить на 32 бит, с результатом 64 бит:
Это немного сложно объяснить, поэтому позвольте мне использовать код.
#include <stdio.h>
#include <stdint.h>
#include <inttypes.h>
int main(void) {
// your code goes here
int32_t a=-1;
int32_t b=-1;
int64_t c = (int64_t)a * b;
printf("signed: 0x%016"PRIx64"\n", (uint64_t)c);
uint32_t d=(uint32_t)-1;
uint32_t e=(uint32_t)-1;
uint64_t f = (uint64_t)d * e;
printf("unsigned: 0x%016"PRIx64"\n", f);
return 0;
}
Демо: http://ideone.com/k30nZ9
5. И, конечно, сравнение.
Можно создать язык без знака, но тогда многие операторы должны разбить на две или более версии, чтобы программист мог выразить цель программы, например. оператор /
должен быть разбит на udiv
и sdiv
, оператор *
должен быть разбит на umul
и smul
, целая продвижение должно быть явным, оператор >
должен быть scmpgt
> /ucmpgt
.........
Это был бы ужасный язык для использования, не так ли?
Бонус: все указатели обычно имеют одно и то же представление битов, но имеют разные операторы []
, ->
, *
, ++
, --
, +
, -
.
Ответ 4
Ну, самый простой и общий ответ - обслуживание памяти, каждая переменная на языке C резервирует некоторое пространство памяти в основной памяти (ОЗУ), когда мы объявляем его, например:
unsigned int var;
зарезервирует байты 2 or 4
и будет варьироваться от 0 to 65,535
или 0 to 4,294,967,295
.
Пока подписанный int
будет иметь диапазон от -32,768 to 32,767
или -2,147,483,648 to 2,147,483,647
.
Точка - это когда-то просто положительные числа, которые не могут быть отрицательными, например, ваш возраст, очевидно, он не может быть отрицательным, поэтому вы использовали бы "unsigned int". Аналогично, имея дело с числами, они могут содержать отрицательные числа того же диапазона, что и signed int
, чем использовать его. Короче говоря, хорошей практикой программирования является использование соответствующих типов данных в соответствии с нашей потребностью, поэтому мы можем эффективно использовать компьютерную память, и наши программы будут более компактными.
Насколько я знаю, 2 дополняют его все о конкретном типе данных или более конкретной правой базе. Мы просто не можем определить либо это 2 дополнения определенного числа, либо нет. Но поскольку компьютер имеет дело с двоичным кодом, у нас все еще есть количество байтов на нашем пути, например, 2 дополнения 7 в 8 бит будут отличаться от 32-битного и 64-битного.