Я давно наткнулся на интересный вопрос на форуме, и я хочу знать ответ.
Рассмотрим следующую C-функцию:
#include <stdbool.h>
bool f1()
{
int var1 = 1000;
int var2 = 2000;
int var3 = var1 + var2;
return (var3 == 0) ? true : false;
}
Это всегда должно возвращать false
с var3 == 3000
. Функция main
выглядит следующим образом:
#include <stdio.h>
#include <stdbool.h>
int main()
{
printf( f1() == true ? "true\n" : "false\n");
if( f1() )
{
printf("executed\n");
}
return 0;
}
Так как f1()
всегда должен возвращать false
, можно ожидать, что программа напечатает на экране только одно значение false. Но после компиляции и запуска его также выполняется:
$ gcc main.c f1.c -o test
$ ./test
false
executed
Почему? Этот код имеет своего рода поведение undefined?
Примечание. Я скомпилировал его с помощью gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2
.
Как отмечено в других ответах, проблема заключается в том, что вы используете gcc
без каких-либо параметров компилятора. Если вы это сделаете, по умолчанию используется так называемое "gnu90", что является нестандартной реализацией старого, снятого стандарта C90 с 1990 года.
В старом стандарте C90 был серьезный недостаток на языке C: если вы не объявили прототип перед использованием функции, по умолчанию он будет int func ()
(где ( )
означает "принять любой параметр" ), Это изменяет вызывающее соглашение функции func
, но не изменяет фактическое определение функции. Поскольку размер bool
и int
различен, ваш код вызывает поведение undefined при вызове функции.
Это опасное бессмысленное поведение было зафиксировано в 1999 году с выпуском стандарта C99. Неявные декларации функций были запрещены.
К сожалению, GCC до версии 5.x.x по-прежнему использует старый стандарт C по умолчанию. Вероятно, нет причин, по которым вам нужно будет скомпилировать ваш код как ничего, кроме стандартного C. Таким образом, вы должны явно указать GCC, что он должен скомпилировать ваш код как современный C-код вместо 25-летнего нестандартного GNU дерьма.
Устраните проблему, всегда компилируя свою программу как:
gcc -std=c11 -pedantic-errors -Wall -Wextra
-std=c11
говорит, что он делает половинчатую попытку скомпилировать согласно (текущему) стандарту C (неофициально известному как C11).-pedantic-errors
рассказывает об этом полностью и делает ошибки компилятора, когда вы пишете неправильный код, который нарушает стандарт C.-Wall
означает дать мне несколько дополнительных предупреждений, которые могут быть хорошими.-Wextra
означает дать мне несколько других дополнительных предупреждений, которые могут быть полезны.-std=gnu11
гораздо вероятнее будет работать, как ожидается, чем -std=c11
, из-за какой-либо или всех из них: нуждающихся в функциональности библиотеки помимо C11 (POSIX, X / Open и т. Д.) это доступно в расширенных режимах "gnu", но подавлено в режиме строгого соответствия; ошибки в системных заголовках, которые скрыты в расширенных режимах, например, при предположении наличия нестандартных typedefs; непреднамеренное использование триграфов (эта стандартная ошибка в режиме «gnu» отключена).
-pedantic-errors
меньше проблем, чем -Werror
но и то и другое может привести к сбоям компиляции программ в операционных системах, которые не были включены в первоначальное авторское тестирование, даже когда проблем нет.
У вас нет прототипа, объявленного для f1()
в main.c, поэтому он неявно определяется как int f1()
, что означает, что это функция, которая принимает неизвестное количество аргументов и возвращает int
.
Если int
и bool
имеют разные размеры, это приведет к undefined поведению. Например, на моей машине int
- 4 байта, а bool
- один байт. Поскольку функция определена для возврата bool
, она возвращает один байт в стек, когда он возвращается. Однако, поскольку он неявно объявлен для возврата int
из main.c, вызывающая функция попытается прочитать 4 байта из стека.
Параметры компилятора по умолчанию в gcc не скажут вам, что он это делает. Но если вы скомпилируете -Wall -Wextra
, вы получите следующее:
main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’
Чтобы исправить это, добавьте объявление для f1
в main.c, перед main
:
bool f1(void);
Обратите внимание, что список аргументов явно установлен на void
, который сообщает компилятору, что функция не принимает никаких аргументов, в отличие от пустого списка параметров, что означает неизвестное количество аргументов. Определение f1
в f1.c также должно быть изменено, чтобы отразить это.
-Werror-implicit-function-declaration
к опциям GCC, таким образом, этот больше не ускользает. Еще лучшим выбором является -Werror
чтобы превратить все предупреждения в ошибки. Заставляет вас исправить все предупреждения, когда они появляются.
Я думаю, что интересно посмотреть, где на самом деле происходит несоответствие размера, упомянутое в Lundin, на самом деле происходит.
Если вы скомпилируете с помощью --save-temps
, вы получите сборные файлы, на которые вы можете посмотреть. Здесь часть, где f1()
выполняет сравнение == 0
и возвращает ее значение:
cmpl $0, -4(%rbp)
sete %al
Возвращаемая часть sete %al
. В условных вызовах C x86 возвращаемые значения 4 байта или меньше (включая int
и bool
) возвращаются через регистр %eax
. %al
- младший байт %eax
. Таким образом, верхние 3 байта %eax
остаются в неконтролируемом состоянии.
Теперь в main()
:
call f1
testl %eax, %eax
je .L2
Это проверяет, является ли все %eax
равным нулю, поскольку он считает, что он тестирует int.
Добавление явного объявления функции изменяет main()
на:
call f1
testb %al, %al
je .L2
что мы хотим.
Скомпилируйте с помощью такой команды:
gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c
Вывод:
main.c: In function 'main':
main.c:14:5: error: implicit declaration of function 'f1' [-Werror=impl
icit-function-declaration]
printf( f1() == true ? "true\n" : "false\n");
^
cc1.exe: all warnings being treated as errors
С таким сообщением вы должны знать, что делать, чтобы исправить его.
Изменить: прочитав комментарий (теперь удаленный), я попытался скомпилировать ваш код без флагов. Ну, это привело меня к ошибкам компоновщика без предупреждений компилятора вместо ошибок компилятора. И эти ошибки компоновщика сложнее понять, поэтому даже если -std-gnu99
не требуется, попробуйте использовать как минимум -Wall -Werror
, это сэкономит вам много боли в заднице.
f1()
в тот же файл, что иmain()
, вы получите некоторую странность: хотя в C ++ правильно использовать()
для пустого списка параметров, в C, который используется для функции с еще не определенный список параметров (в основном он ожидает список параметров в стиле K & R после)
). Чтобы быть правильным C, вы должны изменить свой код наbool f1(void)
.main()
можно упростить доint main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }
- это лучше показало бы несоответствие.