Почему у нас есть неподписанный и подписанный тип 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-битного.