Поведение printf при печати %d без указания имени переменной

Я только что столкнулся со странной проблемой, я пытаюсь напечатать целочисленную переменную, но я забыл указать имя переменной, т.е.

printf("%d");

вместо

printf("%d", integerName);

На удивление программа компилируется, вывод есть и не случайный. На самом деле, это то самое целое число, которое я хотел напечатать в первую очередь, и оно равно m-1.

Ошибочный оператор printf будет постоянно выводить m-1, пока программа продолжает работать... Другими словами, он ведет себя точно так, как если бы оператор читал

printf("%d", m-1);

Кто-нибудь знает причину такого поведения? Я использую g++ без каких-либо параметров командной строки.

#include <iostream>
#define maxN 100
#define ON 1
#define OFF 0

using namespace std;

void clearArray(int* array, int n);
int fillArray(int* array, int m, int n);

int main()
{
    int n = -1, i, m;
    int array[maxN];
    int found;

    scanf("%d", &n);

    while(n!=0)
    {
        found=0;
        m = 1;
        while(found!=1)
        {
            if(m != 2 && m != 3 && m != 4 && m != 6 && m != 12)
            {
                clearArray(array, n);
                if(fillArray(array, m, n) == 0)
                {
                    found = 1;
                }
            }
            m++;
        }

        printf("%d\n");

        scanf("%d", &n);
    }

    return 0;
}

void clearArray(int* array, int n)
{
    for(int i = 1; i <= n; i++)
        array[i] = ON;
}

int fillArray(int* array, int m, int n)
{
    int i = 1, j, offCounter = 0, incrementCounter;

    while(offCounter != n)
    {
        if(*(array+i)==ON) 
        {
            *(array+i) = OFF;
            offCounter++;       
        }
        else 
        {
            j = 0;
            while((*array+i+j)==OFF)
            {
                j++;
            }
            *(array+i+j) = OFF;
            offCounter++;           
        }
        if(*(array+13) == OFF && offCounter != n) return 1;
        if(offCounter ==n) break;

        incrementCounter = 0;       
        while(incrementCounter != m)
        {
            i++;
            if(i > n) i = 1;
            if(*(array+i) == ON) incrementCounter++; 
        }       
    }

    return 0;
}

person Yew Long    schedule 13.01.2009    source источник


Ответы (6)


Вы говорите, что "на удивление программа компилируется". На самом деле, это совсем не удивительно. C и C++ позволяют функциям иметь переменные списки аргументов. Определение printf примерно такое:

int printf(char*, ...);

«...» означает, что у функции есть ноль или более необязательных аргументов. На самом деле, одна из основных причин, по которой C имеет необязательные аргументы, заключается в поддержке семейства функций printf и scanf.

C не имеет специальных знаний о функции printf. В вашем примере:

printf("%d");

Компилятор не анализирует строку формата и не определяет отсутствие целочисленного аргумента. Это совершенно законный код C. Тот факт, что вам не хватает аргумента, является семантической проблемой, которая появляется только во время выполнения. Функция printf предполагает, что вы предоставили аргумент, и ищет его в стеке. Он подхватит все, что там есть. Просто бывает, что в вашем частном случае печатает нужное, но это исключение. В общем, вы получите мусорные данные. Это поведение будет варьироваться от компилятора к компилятору, а также будет меняться в зависимости от того, какие параметры компиляции вы используете; если вы включите оптимизацию компилятора, вы, скорее всего, получите другие результаты.

Как указано в одном из комментариев к моему ответу, некоторые компиляторы имеют возможности типа «lint», которые могут фактически обнаруживать ошибочные вызовы printf/scanf. При этом компилятор анализирует строку формата и определяет количество ожидаемых дополнительных аргументов. Это очень специфическое поведение компилятора, и в общем случае ошибки не обнаруживаются. т. е. если вы напишите свою собственную функцию «printf_better», которая имеет ту же сигнатуру, что и printf, компилятор не обнаружит отсутствие каких-либо аргументов.

person Mike Thompson    schedule 13.01.2009
comment
Я не знал, что компилятор не анализирует аргументы printf... Спасибо, что сказали мне - person Yew Long; 13.01.2009
comment
Вы можете указать компилятору проверить аргументы. Мы знаем, что в данном случае используется компилятор g++, поэтому используйте опцию -Wformat, которая включена в -Wall. - person Rob Kennedy; 13.01.2009
comment
Использование -Wformat было бы довольно нестандартным расширением (хотя и полезным). Предположительно, это работает только для предопределенного списка функций. - person Mike Thompson; 13.01.2009
comment
GCC позволяет вам аннотировать ваши собственные функции как функции printf/scanf. не позволяет вам определять новые форматы, хотя - person Hasturkun; 13.01.2009
comment
Rust забавен тем, что формат обрабатывается во время компиляции. - person Paul Stelian; 24.01.2018
comment
C не имеет специальных знаний о функции printf — очень неверное утверждение. На самом деле компилятор C имеет специальные знания о функции printf - это функция из стандартной библиотеки с таким названием! И многие компиляторы оптимизируют на основе этого, включая замену printf("%s\n", t) на puts(t)! - person Antti Haapala; 23.06.2019

Происходящее выглядит так.

printf("%d", m);

В большинстве систем адрес строки помещается в стек, а затем 'm' в виде целого числа (при условии, что это int/short/char). Предупреждения нет, потому что printf в основном объявляется как 'int printf(const char *, ...);' - ... означает "все идет".

Итак, поскольку «все идет», когда вы помещаете туда переменные, происходят некоторые странные вещи. Любой целочисленный тип, меньший, чем int, рассматривается как int и тому подобное. Ничего не отправлять — тоже нормально.

В реализации printf (или, по крайней мере, в «простой» реализации) вы найдете использование va_list и va_arg (имена иногда немного отличаются в зависимости от соответствия). Это то, что реализация использует для обхода части '...' списка аргументов. Проблема здесь в том, что НЕТ проверки типов. Поскольку проверки типов нет, printf будет извлекать случайные данные из стека выполнения, когда просматривает строку формата ("%d") и думает, что дальше должна быть 'int'.

Случайный выстрел в темноте сказал бы, что вызов функции, который вы сделали непосредственно перед printf, возможно, передал 'm-1' в качестве второго параметра? Это одна из многих возможностей, но было бы интересно, если бы это произошло именно так. :)

Удачи.

Кстати, большинство современных компиляторов (я полагаю, GCC?) имеют предупреждения, которые можно включить для обнаружения этой проблемы. Я думаю, Lint тоже. К сожалению, я думаю, что с VC вам нужно использовать флаг /analyze вместо того, чтобы получать бесплатно.

person Joe    schedule 13.01.2009
comment
Понимаю. Обычно я бы не беспокоился и просто относился к нему как к мусорному значению, но, поскольку так получилось, что он выводит разумные данные, я подумал, что, возможно, есть что-то еще. Вы очень ясно объяснили, спасибо за помощь, Джо. - person Yew Long; 13.01.2009
comment
Обычно параметры помещаются в стек справа налево. Затем, когда строка извлекается, она извлекается из стека, поскольку ей нужны параметры. - person Cade Roux; 13.01.2009
comment
Не настоящий Кейд. Это правильно для stdcall, но для соглашения о вызовах vararg cdecl не было бы способа найти «первый» параметр, не просматривая все ... параметры. cdecl/vararg идет слева направо. - person Joe; 13.01.2009
comment
Чтобы гарантировать, что первый параметр находится на вершине стека, аргументы помещаются справа налево. То же самое в cdecl и stdcall. Единственная разница между cdecl и stdcall заключается в том, кто отвечает за очистку стека. - person Rob Kennedy; 13.01.2009
comment
Роб - я стою смиренно. Понятия не имею, о чем я думал — конечно, вы правы. Прости Кейд. - person Joe; 19.01.2009

Он получил int из стека.

http://en.wikipedia.org/wiki/X86_calling_conventions

person Cade Roux    schedule 13.01.2009
comment
причудливые возражения: предполагается стек, предполагается x86, оба из которых могут быть недействительными. - person Jens; 17.05.2012

Вы вглядываетесь в стек. Измените значения оптимизатора, и это может измениться. Измените порядок объявлений ваших переменных (в частности) m. Сделайте m переменной регистра. Сделайте m глобальной переменной.

Вы увидите некоторые варианты того, что происходит.

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

person S.Lott    schedule 13.01.2009

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

person Joshua    schedule 13.01.2009

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

person Otávio Décio    schedule 13.01.2009
comment
Да.. Я понял, что это может быть не м-1 для всех случаев. - person Yew Long; 13.01.2009