Почему такое неоднозначное поведение printf ()? - PullRequest
4 голосов
/ 09 мая 2020

Я намеренно заставил printf() печатать celsius как int (используя для него спецификатор формата %8d). Я знаю, что это причина печати 0 (под заголовком Celsius Scale).

Но я просто хочу знать, почему fahr будет печатать 0.0 во всей таблице.

Я использовал компилятор G CC.

Это код для преобразования Цельсия в Фаренгейта :

#define LOWER 0.0F
#define UPPER 300.0F
#define STEP 20.0F

#include <stdio.h>

void main() {
    float celsius, fahr;
    printf("*Conversion from Celsius to Fahrenheit*\n");
    printf("Celsius Scale \t   Fahrenheit Scale\n");
    for (celsius = LOWER; celsius <= UPPER; celsius = celsius + STEP) {
        fahr = (9.0f * celsius / 5.0f) + 32.0f; 
        printf("%8d\t\t%5.1f\n", celsius, fahr);
    }
}

В следующей таблице вывод приведенного выше кода:

*Conversion from Celsius to Fahrenheit*
Celsius Scale      Fahrenheit Scale
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0
       0                  0.0

Ответы [ 3 ]

6 голосов
/ 09 мая 2020

У вас есть неопределенное поведение (что означает, что результаты операции не «предсказуемы»), поскольку вы передаете аргумент float в printf, когда он ожидает int аргумент (для формата %8d).

Из этого C11 Draft Standard :

7.21.6 Форматированные функции ввода / вывода ... 9 Если спецификация преобразования недействительна, поведение не определено. Если какой-либо аргумент не является правильным типом для соответствующей спецификации преобразования, поведение не определено.

Точно почему значения fahr отображаются неправильно, трудно сказать с помощью любого определенность, но это возможно связано с тем фактом, что аргументы float повышаются до double, размер которого отличается от ожидаемого типа int, и, таким образом, стек вызовов (или вызов 'frame', если аргументы передаются в регистры) поврежден.

Возможно, стоит отметить, что в моей системе (MSV C, 64-разрядная версия) при выполнении вашего кода отображаются нули для celsius, но правильные значения для fahr. (Но при компиляции для 32-битной цели воспроизводится ваша проблема!)

Чтобы устранить проблему, явно приведите аргумент celsius к int:

void main()
{
    float celsius, fahr;
    printf("*Conversion from Celsius to Fahrenheit*\n");
    printf("Celsius Scale \t   Fahrenheit Scale\n");
    for (celsius = LOWER; celsius <= UPPER; celsius = celsius + STEP) {
        fahr = (9.0f * celsius / 5.0f) + 32.0f;
        printf("%8d\t\t%5.1f\n", (int)celsius, fahr);
    }
}
4 голосов
/ 09 мая 2020

Ваш вопрос точен: Я просто хочу знать, почему fahr будет печатать 0,0 во всей таблице.

Как вы уже понимаете, у вас неопределенное поведение, потому что вы проходите float, который фактически преобразуется в double, в printf в качестве аргумента, для которого printf ожидает int. Все может случиться, вы получите 0 и 0.0 в качестве выходных данных для всех строк, вы могли бы получить что-нибудь еще или хрен sh ...

Чтобы попытаться объяснить свои наблюдения, вы должен изучить, что на самом деле происходит в вашей системе для этого кода. Такой анализ требует глубоких знаний о вашей системе, ABI, компиляторе, параметрах компилятора и т. Д. c.

Я изменил ваш код и скомпилировал его с помощью Godbolt's Compiler Explorer , и вот мои наблюдения для 2 конфигураций:

g cc версия 9.3 для 64-битных Intel, с отключенной оптимизацией.

Код ошибочного printf в 64-битном виде:

    cvtss2sd        xmm1, DWORD PTR [rbp-8]
    cvtss2sd        xmm0, DWORD PTR [rbp-4]
    mov     edi, OFFSET FLAT:.LC6
    mov     eax, 2
    call    printf

Код для измененного аргумента (int)celcius, который ожидает printf:

    cvtss2sd  xmm0, DWORD PTR [rbp-8]
    movss     xmm1, DWORD PTR [rbp-4]
    cvttss2si eax, xmm1
    mov       esi, eax
    mov       edi, OFFSET FLAT:.LC6
    mov       eax, 1
    call      printf

В 64-битной версии ошибочный код передает celcius и fahr как double значения в регистрах с плавающей запятой %xmm0 и %xmm1 соответственно и передает значение 2 в %eax, тогда как правильный код будет передавать fahr как double в %xmm0 и celcius преобразовано в int в регистре %esi, а значение 1 в %eax.

Значение в %eax, точнее, содержимое %al - это число векторных регистров, используемых для передачи аргументов. Чтобы реализовать vararg api в printf, компилятор генерирует пролог, который использует это значение для сохранения аргументов регистра в стек:

myprintf:
    push    rbp
    mov     rbp, rsp
    sub     rsp, 104
    mov     QWORD PTR [rbp-216], rdi
    mov     QWORD PTR [rbp-168], rsi
    mov     QWORD PTR [rbp-160], rdx
    mov     QWORD PTR [rbp-152], rcx
    mov     QWORD PTR [rbp-144], r8
    mov     QWORD PTR [rbp-136], r9
    test    al, al
    je      .L12
    movaps  XMMWORD PTR [rbp-128], xmm0
    movaps  XMMWORD PTR [rbp-112], xmm1
    movaps  XMMWORD PTR [rbp-96], xmm2
    movaps  XMMWORD PTR [rbp-80], xmm3
    movaps  XMMWORD PTR [rbp-64], xmm4
    movaps  XMMWORD PTR [rbp-48], xmm5
    movaps  XMMWORD PTR [rbp-32], xmm6
    movaps  XMMWORD PTR [rbp-16], xmm7
.L12:

Итак, printf будет читать из [rbp-216] int ожидаемое значение для формата %8d и из [rbp-128] значение double для fahr. Значение int будет соответствовать тому, что %rsi содержится при вызове printf, 0 по вашим наблюдениям. Значение double должно быть тем, что было передано в xmm0, поэтому вы ожидаете увидеть значение celcius, и это действительно то, что я наблюдаю в своей системе. Поскольку вы наблюдаете совсем другое, есть большая вероятность, что ваша система не использует этот 64-битный ABI.

g cc версия 9.3 для 32-битных Intel с отключенной оптимизацией.

В 32-битном формате все аргументы передаются в стек. При передаче 2 float значений мы имеем:

    fld     DWORD PTR [ebp-12]
    fld     DWORD PTR [ebp-16]
    sub     esp, 12
    lea     esp, [esp-8]
    fstp    QWORD PTR [esp]
    lea     esp, [esp-8]
    fstp    QWORD PTR [esp]
    push    OFFSET FLAT:.LC6
    call    printf
    add     esp, 32

, а при передаче int и float:

    fld     DWORD PTR [ebp-16]
    movss   xmm0, DWORD PTR [ebp-12]
    cvttss2si       eax, xmm0
    lea     esp, [esp-8]
    fstp    QWORD PTR [esp]
    push    eax
    push    OFFSET FLAT:.LC6
    call    printf
    add     esp, 16

Итак, printf ожидает int в [ebp+12] и double в [ebp+16], но вместо этого celcius был выдвинут как doouble в [ebp+12] и fahr в [ebp+20].

Считывание int from [ebp+12] фактически являются 4 младшими байтами значения celcius. Поскольку значения celcius являются небольшими целыми числами, 32 младших бита их 64-битного представления с плавающей запятой все являются нулями, следовательно, целочисленное чтение будет 0. И наоборот, значение double, считываемое для fahr, смещено: первые 4 байта - это старшие 32 бита значения double из celcius, а последние 4 байта - младшие 32 бита double значение fahr, которые равны 0, потому что fahr также является небольшим целым значением. Следовательно, в экспоненциальной части этого значения double все биты равны нулю, поэтому это либо значение 0.0, либо очень маленькое денормальное значение, которое преобразуется в 0.0 с форматом преобразования %5.1f. Действительно, я получаю тот же результат, что и вы в 32-битном режиме.

Вы можете поэкспериментировать с другим форматом, например %g для fahr, и проверить, верен ли мой прогноз для очень маленького значения.

Конечно, это судебное исследование c имеет отношение только к определенной архитектуре c и никоим образом не оправдывается стандартом C.

4 голосов
/ 09 мая 2020

Что касается проблемы с fahr, я пытался воспроизвести в нескольких версиях G CC, но мне не удалось, что не является неожиданным, поскольку поведение undefined - это именно то, что, undefined, нельзя ожидать согласованных результатов. Я могу попробовать использовать ту версию и ту команду компиляции, которую вы использовали, но я не ожидаю, что это будет иметь значение.

Например, в этом примере кода результаты полностью отличаются от ваш.

Для переменной double или float, как вы знаете, вам нужен спецификатор "%f", вы можете преобразовать celsius в int, как уже упоминалось, или удалить 0 s с помощью сам спецификатор:

printf("%8.0f\t\t%5.1f\n", celsius, fahr);
         ^^^

Demo

Обратите внимание, что при использовании этого метода значения округляются, а не усекаются.

Вывод:

*Conversion from Celsius to Fahrenheit*
Celsius Scale      Fahrenheit Scale
       0                 32.0
      20                 68.0
      40                104.0
      60                140.0
      80                176.0
     100                212.0
     120                248.0
     140                284.0
     160                320.0
     180                356.0
     200                392.0
     220                428.0
     240                464.0
     260                500.0
     280                536.0
     300                572.0
...