Что происходит с побитовыми операторами и целым продвижением?

У меня простая программа. Обратите внимание, что я использую целое число без знака с фиксированной шириной 1 байт.

#include <cstdint>
#include <iostream>
#include <limits>

int main()
{
    uint8_t x = 12;
    std::cout << (x << 1) << '\n';
    std::cout << ~x;

    std::cin.clear();
    std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
    std::cin.get();

    return 0;
}

Мой вывод следующий.

24
-13

Я тестировал большие числа, а оператор << всегда дает мне положительные числа, а оператор ~ всегда дает отрицательные числа. Затем я использовал sizeof() и нашел...

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

Когда я использую побитовый оператор (~), я получаю подписанное 4-байтовое целое.

Кажется, что побитовое не оператор (~) выполняет подписанную интегральную продвижение, как это делают арифметические операторы. Однако левый оператор сдвига (<<), по-видимому, способствует беззнаковому интегралу.

Я чувствую себя обязанным знать, когда компилятор что-то меняет за моей спиной. Если я правильно в своем анализе, все битовые операторы продвигают до 4 байтовых целых чисел? И почему некоторые подписываются и некоторые без знака? Я так смущен!

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

Ответ 1

[expr.unary.op]

Операнд ~ должен иметь целочисленный или неперечисленный тип перечисления; результат является дополнением к его операнду. Интегральные рекламные акции выполняется.

[expr.shift]

Операторы сдвига << и >> группируются слева направо. [...] Операнды должны быть целочисленного или не подлежащего регистрации типа перечисления и интегральных рекламных акций выполняются.

Какова интегральная поддержка uint8_t (которая обычно будет unsigned_char за кулисами)?

[conv.prom]

Значение целочисленного типа, отличного от bool, char16_t, char32_t или wchar_t, чей целочисленный ранг преобразования (4.13) меньше ранга int может быть преобразован в prvalue типа int, если int может представлять все значения типа источника; в противном случае исходное значение может быть преобразуется в prvalue типа unsigned int.

So int, потому что все значения a uint8_t могут быть представлены int.

Что такое int(12) << 1? int(24).

Что такое ~int(12)? int(-13).

Ответ 2

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

Когда вы используете тип хранилища в выражении, он автоматически преобразуется в int или в unsigned int неявно. Например:

// Assume a char is 8 bit
unsigned char x = 255;
unsigned char one = 1;

int y = x + one; // result will be 256 (too large for a byte!)
++x;             // x is now 0

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

Аналогично, если в выражении есть значение float, первое, что сделает компилятор, это продвигать его на double (другими словами float - это тип хранилища, а double - это естественный размер для чисел с плавающей запятой). Именно по этой причине, если вы используете printf для печати поплавков, вам не нужно указывать %lf int строки формата и %f достаточно (%lf требуется для scanf, однако, поскольку эта функция сохраняет результат, а float может быть меньше double).

С++ затруднил проблему совсем немного, потому что при передаче параметров в функции вы можете различать int и более мелкие типы. Таким образом, не ВСЕГДА верно, что преобразование выполняется в каждом выражении... например, вы можете иметь:

void foo(unsigned char x);
void foo(int x);

где

unsigned char x = 255, one = 1;
foo(x);       // Calls foo(unsigned char), no promotion
foo(x + one); // Calls foo(int), promotion of both x and one to int

Ответ 3

Я тестировал большие числа и оператор < всегда дает мне положительный числа, а оператор ~ всегда дает отрицательные числа. Затем я использовал sizeof() и нашел...

Неправильно, протестируйте его:

uint8_t v = 1;
for (int i=0; i<32; i++) cout << (v<<i) << endl;

дает:

1
2
4
8
16
32
64
128
256
512
1024
2048
4096
8192
16384
32768
65536
131072
262144
524288
1048576
2097152
4194304
8388608
16777216
33554432
67108864
134217728
268435456
536870912
1073741824
-2147483648

uint8_t - это 8-разрядный длинный беззнаковый целочисленный тип, который может представлять значения в диапазоне [0,255], так как этот диапазон включен в диапазон int, он продвигается до int (не ). Продвижение int имеет приоритет над продвижением до unsigned.

Ответ 4

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

#include <cstdint>
#include <iostream>
#include <limits>
int main()
{
uint8_t x = 1;
int shiftby=0;
shiftby=8*sizeof(int)-1;
std::cout << (x << shiftby) << '\n'; // or std::cout << (x << 31) << '\n';

std::cout << ~x;

std::cin.clear();
std::cin.ignore(std::numeric_limits<std::streamsize>::max(), '\n');
std::cin.get();
return 0;
}

Выходной сигнал -2147483648

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

** ИЗМЕНИТЬ **
Хорошо, я могу думать о причине, по которой операторы сдвига будут использовать unsigned int. Рассмотрим операцию правого сдвига >>, если вы сдвигаете вправо -12, вы получите 122 вместо -6. Это связано с тем, что он добавляет нуль в начале без учета знака