Неожиданное поведение при печати 4-байтового целого числа байт за байтом

У меня есть этот пример кода для преобразования 32-битных целых чисел в IP-адреса.


#include <stdio.h>
int main()
{
 unsigned int c ;
 unsigned char* cptr  = (unsigned char*)&c ;
 while(1)
 {
  scanf("%d",&c) ;
  printf("Integer value: %u\n",c);
  printf("%u.%u.%u.%u \n",*cptr, *(cptr+1), *(cptr+2), *(cptr+3) );
 }
}

Этот код дает неправильный вывод для ввода 2249459722 . Но когда я заменяю

scanf("%d",&c) ;
на
scanf("%u",&c) ;
, результат становится правильным.

P.S. Я знаю о inet_ntop и inet_pton.
Я ожидаю ответов, а не предложений.


person sud03r    schedule 09.01.2010    source источник
comment
Какой результат вы получили? Какой результат вы ожидали?   -  person Jonathan Leffler    schedule 09.01.2010
comment
Гнилое название. Он ничего не сообщает читателю ничего о проблеме, с которой вы столкнулись.   -  person dmckee --- ex-moderator kitten    schedule 10.01.2010
comment
2249459722 не обязательно вписываться в int или unsigned int в этом отношении. Используйте unsigned long или, если он у вас есть, uint32_t. В этом случае формат для scanf() становится "%lu"/"%" SCNu32.   -  person Alok Singhal    schedule 10.01.2010


Ответы (4)


Вы кодируете 'греховно' (создавая ряд ошибки, которые рано или поздно навредят вам — чаще всего раньше). Во-первых, вы предполагаете, что целое число имеет правильный порядок следования байтов. На некоторых машинах вы ошибетесь — либо на машинах Intel, либо на машинах PowerPC или SPARC.

В общем, вы должны показывать фактические результаты, которые вы получаете, а не просто говорить, что вы получили неправильный результат; вы также должны показать ожидаемый результат. Это помогает людям отладить ваши ожидания.


Вот моя модифицированная версия вашего кода - вместо того, чтобы запрашивать ввод, он просто принимает указанное вами значение.

#include <stdio.h>
int main(void)
{
    unsigned int c = 2249459722;
    unsigned char* cptr  = (unsigned char*)&c;
    printf("Integer value:  %10u\n", c);
    printf("Integer value:  0x%08X\n", c);
    printf("Dotted decimal: %u.%u.%u.%u \n", *cptr, *(cptr+1), *(cptr+2), *(cptr+3));
    return(0);
}

При компиляции на моем Mac (Intel, обратный порядок байтов) вывод:

Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 

При компиляции на моем Sun (SPARC, с обратным порядком байтов) вывод:

Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 134.20.8.10 

(Используя GCC 4.4.2 на SPARC, я получаю предупреждение:

xx.c:4: warning: this decimal constant is unsigned only in ISO C90

Используя GCC 4.2.1 на Mac — с множеством включенных предупреждений (gcc -std=c99 -pedantic -Wall -Wshadow -Wpointer-arith -Wstrict-prototypes -Wmissing-prototypes -Werror) — я не получаю этого предупреждения, что интересно.) Я могу удалить это, добавив суффикс U к целочисленной константе.


Другой способ взглянуть на проблемы иллюстрируется следующим кодом и показанными выше чрезвычайно суетливыми настройками компилятора:

#include <stdio.h>

static void print_value(unsigned int c)
{
    unsigned char* cptr  = (unsigned char*)&c;
    printf("Integer value:  %10u\n", c);
    printf("Integer value:  0x%08X\n", c);
    printf("Dotted decimal: %u.%u.%u.%u \n", *cptr, *(cptr+1), *(cptr+2), *(cptr+3));
}

int main(void)
{
    const char str[] = "2249459722";
    unsigned int c = 2249459722;

    printf("Direct operations:\n");
    print_value(c);

    printf("Indirect operations:\n");
    if (sscanf("2249559722", "%d", &c) != 0)
        printf("Conversion failed for %s\n", str);
    else
        print_value(c);
    return(0);
}

Это не удается скомпилировать (из-за настройки -Werror) с сообщением:

cc1: warnings being treated as errors
xx.c: In function ‘main’:
xx.c:20: warning: format ‘%d’ expects type ‘int *’, but argument 3 has type ‘unsigned int *’

Удалите параметр -Werror, и он скомпилируется, но затем покажет следующую проблему, которая у вас есть, — отсутствие проверки индикации ошибок из функций, которые могут дать сбой:

Direct operations:
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 
Indirect operations:
Conversion failed for 2249459722

По сути, функция sscanf() сообщает, что ей не удалось преобразовать строку в целое число со знаком (поскольку значение слишком велико, чтобы уместиться — см. предупреждение от GCC 4.4.2), но ваш код не проверял возврат ошибки из sscanf(), поэтому вы использовали любое значение, оставшееся в c в то время.

Итак, в вашем коде есть несколько проблем:

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

Комментарий Алока

Да, тест на sscanf() неверен. Вот почему у вас есть обзоры кода, а также почему это помогает публиковать код, который вы тестируете.

Теперь я немного озадачен - постоянное поведение, которое я не могу сразу объяснить. При очевидной доработке (тестирование на MacOS X 10.6.2, GCC 4.2.1, 32-битные и 64-битные компиляции) получаю один не очень вменяемый ответ. Когда переписываю более модульно, получаю вменяемый ответ.

+ cat yy.c
#include <stdio.h>

static void print_value(unsigned int c)
{
    unsigned char* cptr  = (unsigned char*)&c;
    printf("Integer value:  %10u\n", c);
    printf("Integer value:  0x%08X\n", c);
    printf("Dotted decimal: %u.%u.%u.%u \n", *cptr, *(cptr+1), *(cptr+2), *(cptr+3));
}

int main(void)
{
    const char str[] = "2249459722";
    unsigned int c = 2249459722;

    printf("Direct operations:\n");
    print_value(c);

    printf("Indirect operations:\n");
    if (sscanf("2249559722", "%d", &c) != 1)
        printf("Conversion failed for %s\n", str);
    else
        print_value(c);
    return(0);
}


+ gcc -o yy.32 -m32 -std=c99 -pedantic -Wall -Wshadow -Wpointer-arith -Wstrict-prototypes -Wmissing-prototypes yy.c
yy.c: In function ‘main’:
yy.c:20: warning: format ‘%d’ expects type ‘int *’, but argument 3 has type ‘unsigned int *’


+ ./yy.32
Direct operations:
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 
Indirect operations:
Integer value:  2249559722
Integer value:  0x86158EAA
Dotted decimal: 170.142.21.134 

У меня нет хорошего объяснения значения 170.142.21.134; но это соответствует моей машине, на данный момент.

+ gcc -o yy.64 -m64 -std=c99 -pedantic -Wall -Wshadow -Wpointer-arith -Wstrict-prototypes -Wmissing-prototypes yy.c
yy.c: In function ‘main’:
yy.c:20: warning: format ‘%d’ expects type ‘int *’, but argument 3 has type ‘unsigned int *’


+ ./yy.64
Direct operations:
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 
Indirect operations:
Integer value:  2249559722
Integer value:  0x86158EAA
Dotted decimal: 170.142.21.134 

То же значение - даже в 64-битной версии вместо 32-битной. Может быть, проблема в том, что я пытаюсь объяснить поведение undefined, которое по определению более или менее необъяснимо (необъяснимо).

+ cat xx.c
#include <stdio.h>

static void print_value(unsigned int c)
{
    unsigned char* cptr  = (unsigned char*)&c;
    printf("Integer value:  %10u\n", c);
    printf("Integer value:  0x%08X\n", c);
    printf("Dotted decimal: %u.%u.%u.%u \n", *cptr, *(cptr+1), *(cptr+2), *(cptr+3));
}

static void scan_value(const char *str, const char *fmt, const char *tag)
{
    unsigned int c;
    printf("Indirect operations (%s):\n", tag);
    fmt = "%d";
    if (sscanf(str, fmt, &c) != 1)
        printf("Conversion failed for %s (format %s \"%s\")\n", str, tag, fmt);
    else
        print_value(c);
}

int main(void)
{
    const char str[] = "2249459722";
    unsigned int c = 2249459722U;

    printf("Direct operations:\n");
    print_value(c);
    scan_value(str, "%d", "signed");
    scan_value(str, "%u", "unsigned");

    return(0);
}

Использование такого аргумента функции означает, что GCC больше не может обнаруживать поддельный формат.

+ gcc -o xx.32 -m32 -std=c99 -pedantic -Wall -Wshadow -Wpointer-arith -Wstrict-prototypes -Wmissing-prototypes xx.c


+ ./xx.32
Direct operations:
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 
Indirect operations (signed):
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 
Indirect operations (unsigned):
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 

Результаты здесь согласуются.

+ gcc -o xx.64 -m64 -std=c99 -pedantic -Wall -Wshadow -Wpointer-arith -Wstrict-prototypes -Wmissing-prototypes xx.c


+ ./xx.64
Direct operations:
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 
Indirect operations (signed):
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134 
Indirect operations (unsigned):
Integer value:  2249459722
Integer value:  0x8614080A
Dotted decimal: 10.8.20.134

И это то же самое, что и 32-битный случай. Я официально сбит с толку. Основные наблюдения остаются точными — будьте осторожны, прислушивайтесь к предупреждениям компилятора (и вызывайте предупреждения компилятора) и не думайте, что «весь мир работает на чипах Intel» (раньше было «не думайте, что весь мир — это VAX", когда-то давно!).

person Jonathan Leffler    schedule 09.01.2010
comment
Хорошая точка зрения. Я думал упомянуть об этом в своем редактировании, но я не думал, что это будет понято. - person hobodave; 09.01.2010
comment
Причина предупреждения unsigned only in C90 заключается в том, что правила о том, какой тип целочисленного литерала в C находится между C90 и C99. В C90 пробовались типы int, long int и unsigned long int. В C99 это типы int, long int и long long int. Константа 2249459722 имеет тип unsigned long int в C90 и long long int в C99 на вашем компьютере. Добавление U делает его везде беззнаковым. - person Alok Singhal; 10.01.2010
comment
Технически функции printf() могут дать сбой, и их тоже следует проверять. Однако в примере кода это необычно, тогда как проверка входных данных, таких как sscanf() (или scanf() в оригинале), имеет решающее значение даже в примере кода. - person Jonathan Leffler; 10.01.2010
comment
Вы наверняка имели в виду != 1, а не != 0 в своем scanf() звонке? - person Alok Singhal; 10.01.2010
comment
@Jonathan: Не могли бы вы сказать мне размер int, long int и long long int на вашем компьютере с Solaris? Просто любопытно. - person Alok Singhal; 10.01.2010
comment
@Alok: в «gcc» и «gcc -m32» это 4/4/8; под «gcc -m64» это 4/8/8. Я думаю, что make-файл в каталоге, где я создал тестовую программу, по умолчанию использует «-m64». - person Jonathan Leffler; 10.01.2010
comment
@Jonathan: Вы будете ненавидеть меня за это, но в вашем yy.c ваш вызов sscanf сканирует "2249559722", а не str, который равен "2249459722". Увидеть разницу? 2249 4 59722 против 2249 5 59722. Сейчас? :-) - person Alok Singhal; 10.01.2010
comment
@Alok: хорошо замечено - и абсолютно никакой ненависти! Действительно, это облегчение знать, что есть рациональная причина проблемы. Однако я не уверен, почему scanf() не генерирует ошибку. Я не планирую исправлять свой ответ на данный момент. И я помню, как заметил, что в какой-то момент я не использовал созданную мной строку... но я не исправлял ее. Ну что ж, ç'est la vie, ну да ладно. - person Jonathan Leffler; 10.01.2010
comment
scanf() не выдает ошибку в случае переполнения. Поведение не определено. Итак, int c; sscanf("123456789012345678901234567890", "%d", &c); вернет (может) 1, а c может содержать что угодно. - person Alok Singhal; 10.01.2010
comment
@Alok: темные углы стандарта C действительно темные и извилистые. Даже спецификацию для strtol() и др. сложно использовать: если значение «строки темы» слишком велико, вы получаете соответствующее минимальное/максимальное целочисленное значение (LONG_MIN для отрицательного, LONG_MAX для положительного) и errno устанавливается в ERANGE. Я бы предпочел: extern errno_t str_to_long(const char *str, char *end, int base, long *result); с возвращаемым значением 0 для OK и ERANGE или другими значениями при ошибке (errno_t определено в TR 24731-1 - см. stackoverflow.com/questions/372980 для получения дополнительной информации об этом стандартном техническом отчете). - person Jonathan Leffler; 10.01.2010

%d для целых чисел со знаком

%u для беззнаковых целых чисел

Редактировать:

Пожалуйста, измените вашу программу следующим образом, чтобы увидеть, как ваш ввод действительно интерпретируется:

#include <stdio.h>
int main()
{
 unsigned int c ; 
 unsigned char* cptr  = (unsigned char*)&c ;
 while(1)
 {
  scanf("%d",&c) ;
  printf("Signed value: %d\n",c);
  printf("Unsigned value: %u\n",c);
  printf("%u.%u.%u.%u \n",*cptr, *(cptr+1), *(cptr+2), *(cptr+3) );
 }
}

Что происходит, когда вы указываете число больше, чем INT_MAX, так это то, что самый левый бит равен 1. Это указывает, что это целое число со знаком с отрицательным значением. Затем число интерпретируется как дополнение до двух.

person hobodave    schedule 09.01.2010
comment
Также вы можете рассмотреть некоторые исследования о дополнении 2. - person Erkan Haspulat; 09.01.2010
comment
Его можно интерпретировать как дополнение двух, дополнение единиц или любую другую кодировку, лежащую в основе. Технически scanf() мог бы сделать что угодно, поскольку поведение, когда ввод не соответствует типу данных, не определено. - person Alok Singhal; 10.01.2010

Чтобы ответить на ваш главный вопрос:

scanf("%d", &c);

Поведение scanf() не определено, когда преобразуемый ввод не может быть представлен в тип данных. 2249459722 на вашей машине не помещается в int, поэтому scanf() может делать что угодно, в том числе хранить мусор в c.

В C тип int гарантированно может хранить значения в диапазоне от -32767 до +32767. unsigned int — это гарантированные значения между 0 и 65535. Таким образом, 2249459722 не обязательно вписывается даже в unsigned int. Однако unsigned long может хранить значения до 4294967295 (2321), поэтому следует использовать unsigned long:

#include <stdio.h>
int main()
{
    unsigned long c ;
    unsigned char *cptr  = (unsigned char*)&c ;
    while(1)
    {
        if (scanf("%lu", &c) != 1) {
            fprintf(stderr, "error in scanf\n");
            return 0;
        }
        printf("Input value: %lu\n", c);
        printf("%u.%u.%u.%u\n", cptr[0], cptr[1], cptr[2], cptr[3]);
    }
    return 0;
}

Если у вас есть компилятор C99, вы можете использовать #include <inttypes.h>, а затем использовать uint32_t вместо unsigned long. Вызов scanf() становится scanf("%" SCNu32, &c);

person Alok Singhal    schedule 09.01.2010

Правильный способ записи с порядком следования байтов:

printf("Dotted decimal: %u.%u.%u.%u \n", (c >> 24) & 0xff, (c >> 16) & 0xff, (c >> 8) & 0xff, (c >> 0) & 0xff);
person starblue    schedule 10.01.2010