2016-10-13 3 views
1

Я играл с указателями и случайно ввели неправильный аргумент PRINTFПочему неопределенное поведение настолько последовательное?

#include <stdio.h> 

int 
main (void) 
{ 
    double * p1; 
    double * p2; 
    double d1, d2; 

    d1 = 1.2345; 
    d2 = 2.3456; 
    p1 = &d1; 
    p2 = &d2; 

    printf ("p1=%p\n, *p1=%g\n", (void *)p1, *p1); 
    printf ("p2=%p\n, *p2=%g\n", (void *)p2, p2); /* third argument should be *p2 */ 

    return 0; 
} 

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

предупреждение: формат «% г» ожидает аргумент типа «двойной», но аргумент 3 имеет тип 'двойной *'
р1 = 0x7ffc9aec46b8, * р1 = 1.2345
р2 = 0x7ffc9aec46c0, * р2 = 1,2345

Почему в этом случае выход p2 всегда равен выходу *p1?

Я использую компилятор gcc (v5.4.0) со стандартом по умолчанию для C (gnu11).

+3

Неопределенное поведение означает, что нет гарантии, что оно будет иметь конкретное поведение на любой платформе при любых условиях. У него нет никаких последствий, будете ли вы наблюдать за последовательным поведением между последовательными прогонами. – paddy

+0

Он не определяется только потому, что он не изменяется для каждого запуска программы. – Pixelchemist

+1

не определяется спецификацией языка, но компиляторы могут делать все, что им нравится. –

ответ

4

Это не определено - это все. То, что вы видите, вероятно, является результатом старого значения, оставшегося в любом регистре, используемом для передачи аргумента с плавающей запятой.

+0

Спасибо, это так. Я попытался передать указатель на другое значение с плавающей запятой (не 1.2345) до последнего оператора printf, а значение p2 на выходе изменилось с 1.2345 на другое значение. – tarashypka

3

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

Неопределенное поведение не означает случайное поведение, но поведение «не охвачено стандартным». Таким образом, это может быть все, что может сделать разработчик.

Стандарт определяет UB, поскольку он позволяет оптимизировать компиляцию, которая может быть невозможна в противном случае.

3

На уровне языка, как правило, мало значения для такого рода исследований.

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

Компилятор использует различные преходящие конвенции (область памяти, стеки, регистры), чтобы передавать различные типы аргументов. Указатели передаются одним способом (например, стек процессора), а значения double передаются другим способом (скажем, в стеке FPU). Вы передали указатель, но сказали printf, что это был номер double. printf отправился в зону для прохождения double s (например, сверху стека регистров FPU) и прочитал значение «мусора», которое было оставлено там предыдущим printf.

6

Код, который вызывает неопределенное поведение, может что-то сделать - вот почему это undefined.

Тем не менее, можно было бы сделать хороший догадываться, почему это происходит, чтобы сделать эту конкретную вещь на вашей машине используя ваш конкретный компилятор с точно параметры, которые вы использовали и скомпилирован на же будний день года с 6, вы понимаете, верно? Это undefined, и есть никаких объяснений, на которые можно положиться, даже если вы считаете, что знаете все переменные. Однажды, влажность падает, или что-то, и ваша программа может решить сделать что-то другое. Даже без перекомпиляции. Даже в двух итерациях того же цикла. Это просто неопределенное поведение.

В любом случае, аргументы с плавающей запятой в вашей платформе, вероятно, передаются в выделенные регистры с плавающей запятой (или выделенный стек с плавающей точкой), а не в основной стек. printf ("% g") ожидает аргумент с плавающей запятой, поэтому он выглядит в регистре с плавающей запятой. Но вы ничего не передавали в регистре с плавающей запятой; все, что вы передали, были двумя аргументами указателя, которые оба поместили в стек (или где бы ни были аргументы указателя, это также выходит за рамки стандарта C). Таким образом, второй вызов printf получает всякий мусор в этом конкретном регистре с плавающей запятой в последний раз, когда он был загружен. Так получилось, что последнее, что вы загрузили в этот регистр, было значением *p1, в последнем вызове printf, чтобы значение снова использовалось.

Правила, определяющие (среди прочего), где аргументы функции размещены, поэтому функция знает, где их искать, в совокупности называется соглашением . Вероятно, вы используете x86 или производную, поэтому можете найти интересующую вас Wikipedia page on x86 calling conventions. Но если вы хотите точно знать, что делает ваш компилятор, попросите его исправить язык ассемблера (gcc -S).

0

Другие ответы охватывают то, что не определено. Вот интересная статья, в которой описывается , почему так много неопределенного поведения в C, и какие преимущества могут быть. Это не потому, что K & R были ленивыми или не заботились.

http://blog.llvm.org/2011/05/what-every-c-programmer-should-know.html

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

+0

В этой статье описывается ошибочная философия, которая заставила авторов компилятора требовать, чтобы все оставалось неопределенным, но думайте, не думайте точно, почему они остались неопределенными на C89. До C89 многие модели поведения были определены на некоторых платформах, но не в других, и я не видел никаких доказательств того, что авторы C89 намеревались изменить это. Для многих действий, которые вызывают UB, предоставление реализации абсолютно ничего не дает мало оптимистичного преимущества, предоставляя ему свободу выбора произвольным образом из-за выбора возможных вариантов поведения. – supercat

+0

Знаете ли вы какие-либо доказательства того, что авторы C89 намеревались разрешить виды «оптимизации», которые современные компиляторы используют, чтобы оправдать принуждение программистов писать менее эффективный код? – supercat

+0

@supercat: Я не уверен, что понимаю ваш вопрос. Поведение, определяемое реализацией, по-прежнему является четко определенным поведением. Это означает, что автор компилятора должен учитывать и обрабатывать этот конкретный случай. В некоторых случаях это то, что вы хотите, но во многих случаях это может противоречить эффективности. Если вы хотите сказать, что до 1989 года все было по-другому, возможно, вы могли бы привести пример? – jforberg

0

Язык C широко использовался задолго до того, как был опубликован стандарт C89, и авторы Стандарта не хотели требовать, чтобы соответствующие компиляторы не могли делать все существующие компиляторы, как можно эффективнее, чем они уже делали Это. Если указание на то, что все компиляторы реализуют какое-то поведение, сделает какой-то компилятор менее подходящим для его задачи, это было бы оправданием для того, чтобы оставить поведение неопределенным. Даже если поведение было полезным и общепринятым, на 99% платформ авторы Стандарта не видели оснований полагать, что отказ от неопределенного поведения должен повлиять на это. Если бы авторы-компиляторы считали практичным и полезным поддерживать поведение в те дни, прежде чем какой-либо Стандарт все-таки санкционировал что-либо, не было никаких оснований ожидать, что им потребуется мандат для поддержки такой поддержки. Доказательства этой точки зрения можно найти в обосновании продвижения коротких беззнаковых целочисленных типов к подписанным.

Почему-то причудливое мнение подтвердило, что все должно быть санкционировано Стандартом или непредсказуемым. В Стандарте описываются общие последствия неопределенного поведения, и одним из наиболее распространенных в 1989 году было то, что реализация будет вести себя в документальной моде, характерной для реализации.

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