Как компиляторы C/С++ обрабатывают литье типов между типами с разными диапазонами значений?

Как кастинг типов происходит без потери данных внутри компилятора?

Например:

 int i = 10;
 UINT k = (UINT) k;

 float fl = 10.123;
 UINT  ufl = (UINT) fl; // data loss here?

 char *p = "Stackoverflow Rocks";
 unsigned char *up = (unsigned char *) p;

Как компилятор обрабатывает этот тип приведения типов? Низкоуровневый пример, показывающий бит, будет высоко оценен.

Ответ 1

Хорошо, сначала обратите внимание, что приведение - это явный запрос на преобразование значения одного типа в значение другого типа. Приведение также всегда будет создавать новый объект, который временно возвращается оператором литья. Однако приведение к ссылочному типу не создаст новый объект. Объект, на который ссылается значение, переинтерпретируется как ссылка другого типа.

Теперь на ваш вопрос. Обратите внимание, что есть два основных типа преобразований:

  • Акции: этот тип можно подумать о том, чтобы лить от более узкого типа к более широкому типу. Кастинг от char до int, от short до int, float to double - все рекламные акции.
  • Конверсии: они позволяют использовать листинг от long до int, int до unsigned int и т.д. Они могут в принципе вызывать потерю информации. Существуют правила для того, что произойдет, если вы присвойте -1 неподписанному типизированному объекту, например. В некоторых случаях неправильное преобразование может привести к поведению undefined. Если вы присваиваете двойной размер, превышающий то, что float может хранить для float, поведение не определяется.

Посмотрите на свои роли:

int i = 10; 
unsigned int k = (unsigned int) i; // :1

float fl = 10.123;
unsigned int  ufl = (unsigned int) fl; // :2

char *p = "Stackoverflow Rocks"; 
unsigned char *up = (unsigned char *) p; // :3
  • Этот приведение вызывает преобразование. Потеря данных не происходит, так как 10 гарантируется сохранение unsigned int. Если целое число было отрицательным, значение будет в основном обтекать максимальное значение unsigned int (см. 4.7/2).
  • Значение 10.123 усечено до 10. Здесь оно, очевидно, вызывает потерю информации. Поскольку 10 вписывается в unsigned int, поведение определяется.
  • На самом деле это требует большего внимания. Во-первых, устаревшее преобразование из строкового литерала в char*. Но не будем игнорировать это здесь. (см. здесь). Что еще более важно, что происходит, если вы применяете неподписанный тип? Фактически, результат этого не указан в 5.2.10/7 (обратите внимание, что семантика этого акта такая же, как использование reinterpret_cast в этом случае, поскольку это единственный С++-транслятор, способный сделать это ):

Указатель на объект может быть явно преобразован в указатель на объект разного типа. За исключением того, что преобразование rvalue типа "указатель на T1" в тип "указатель на T2" (где T1 и T2 являются типами объектов и где требования к выравниванию T2 не более строгие, чем требования T1) и обратно к исходному типу исходное значение указателя, результат такого преобразования указателя не указан.

Таким образом, вы можете использовать указатель только после того, как снова вернетесь к char *.

Ответ 2

Два примера C-стиля в вашем примере - это разные виды приведения. В С++ вы обычно пишете их

unsigned int uf1 = static_cast<unsigned int>(fl);

и

unsigned char* up = reinterpret_cast<unsigned char*>(p);

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

Второй не производит изменений в данных - он просто инструктирует компилятор обрабатывать указатель как другой тип. Необходимо проявлять осторожность при таком типе: это может быть очень опасно.

Ответ 3

"Тип" в C и С++ - это свойство, назначенное переменным, когда они обрабатываются в компиляторе. Свойство больше не существует во время выполнения, за исключением виртуальных функций /RTTI в С++.

Компилятор использует тип переменных для определения многих вещей. Например, при назначении float в int он будет знать, что ему нужно преобразовать. Оба типа, вероятно, 32 бита, но с разными значениями. Вероятно, процессор имеет инструкцию, но в противном случае компилятор мог бы назвать функцию преобразования. То есть & __stack[4] = float_to_int_bits(& __stack[0])

Преобразование из char * в unsigned char * равномерное. Это просто другой ярлык. На уровне бит p и up идентичны. Компилятор просто должен помнить, что * p требует расширения знака, а не вверх.

Ответ 4

Броски означают разные вещи в зависимости от того, что они собой представляют. Они могут просто быть переименованием типа данных, без изменения представленных битов (большинство отливок между целыми типами и указателями похожи на это) или преобразования, которые даже не сохраняют длину (например, между double и int в большинстве компиляторов), Во многих случаях смысл приведения просто неопределен, что означает, что компилятор должен сделать что-то разумное, но ему не нужно точно документировать.

Приведение не требует даже полезного значения. Что-то вроде char * cp; float * fp; cp = malloc(100); fp = (float *)(cp + 1); почти наверняка приведет к неправильному указателю на float, что приведет к сбою программы в некоторых системах, если программа попытается ее использовать.