Me tropecé con una pregunta interesante en un foro hace mucho tiempo y quiero saber la respuesta.
Considere la siguiente función C:
f1.c
#include <stdbool.h>
bool f1()
{
int var1 = 1000;
int var2 = 2000;
int var3 = var1 + var2;
return (var3 == 0) ? true : false;
}
Esto siempre debe volver false
desde entonces var3 == 3000
. La main
función se ve así:
C Principal
#include <stdio.h>
#include <stdbool.h>
int main()
{
printf( f1() == true ? "true\n" : "false\n");
if( f1() )
{
printf("executed\n");
}
return 0;
}
Como f1()
siempre debe regresar false
, uno esperaría que el programa imprima solo uno falso en la pantalla. Pero después de compilarlo y ejecutarlo, también se muestra ejecutado :
$ gcc main.c f1.c -o test
$ ./test
false
executed
¿Porqué es eso? ¿Este código tiene algún tipo de comportamiento indefinido?
Nota: lo compilé con gcc (Ubuntu 4.9.2-10ubuntu13) 4.9.2
.
f1()
en el mismo archivo quemain()
, obtendría algunas rarezas: si bien es correcto en C ++ usar()
para una lista de parámetros vacía, en C que se usa para una función con una lista de parámetros aún no definida ( básicamente espera una lista de parámetros de estilo K&R después de)
). Para ser C correcto, debe cambiar su código abool f1(void)
.main()
podría simplificarseint main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }
: esto mostraría mejor la discrepancia.void
?true
yfalse
en K&R 1st ed., por lo que no hubo tales problemas en absoluto. Era solo 0 y no cero para verdadero y falso. ¿No es así? No sé si los prototipos estaban disponibles en ese momento._Bool
tipo ni<stdbool.h>
encabezado.Respuestas:
Como se señaló en otras respuestas, el problema es que se usa
gcc
sin opciones de compilación establecidas. Si hace esto, el valor predeterminado es lo que se llama "gnu90", que es una implementación no estándar del antiguo estándar C90 retirado de 1990.En el antiguo estándar C90 había una falla importante en el lenguaje C: si no declaraba un prototipo antes de usar una función, sería predeterminado
int func ()
(donde( )
significa "aceptar cualquier parámetro"). Esto cambia la convención de llamada de la funciónfunc
, pero no cambia la definición real de la función. Dado que el tamaño debool
yint
son diferentes, su código invoca un comportamiento indefinido cuando se llama a la función.Este comportamiento sin sentido peligroso se solucionó en el año 1999, con el lanzamiento del estándar C99. Las declaraciones de funciones implícitas fueron prohibidas.
Desafortunadamente, GCC hasta la versión 5.xx todavía usa el viejo estándar C por defecto. Probablemente no haya ninguna razón por la que deba compilar su código como algo más que el estándar C. Por lo tanto, debe decirle explícitamente a GCC que debe compilar su código como código C moderno, en lugar de una basura GNU no estándar de más de 25 años. .
Solucione el problema compilando siempre su programa como:
-std=c11
le dice que haga un intento poco entusiasta de compilar de acuerdo con el estándar C (actual) (informalmente conocido como C11).-pedantic-errors
le dice que haga de todo corazón lo anterior y que dé errores de compilación cuando escribe un código incorrecto que viola el estándar C.-Wall
significa darme algunas advertencias adicionales que podría ser bueno tener.-Wextra
significa darme algunas otras advertencias adicionales que podría ser bueno tener.fuente
-std=gnu11
es mucho más probable que funcione como se espera que-std=c11
, debido a que cualquiera o todos necesitan funcionalidad de biblioteca más allá de C11 (POSIX, X / Open, etc.) que está disponible en el "gnu" modos extendidos pero suprimidos en el modo de conformidad estricta; errores en los encabezados del sistema que están ocultos en los modos extendidos, por ejemplo, asumiendo la disponibilidad de tipos de letra no estándar; uso involuntario de trigrafos (esta mala función estándar está deshabilitada en modo "gnu").-pedantic-errors
es menos problemático que,-Werror
pero ambos pueden y hacen que los programas no se compilen en sistemas operativos que no están incluidos en las pruebas del autor original, incluso cuando no hay un problema real.bool
/ estándar_Bool
, puede escribir su código C de forma "C ++ - esque", donde asume que todas las comparaciones y operadores lógicos devuelven unbool
like en C ++, aunque devuelvan unint
C en, por razones históricas . Esto tiene la gran ventaja de que puede utilizar herramientas de análisis estático para verificar la seguridad de tipo de todas esas expresiones y exponer todo tipo de errores en tiempo de compilación. También es una forma de expresar la intención en forma de código autodocumentado. Y menos importante, también ahorra unos pocos bytes de RAM.No tiene un prototipo declarado
f1()
en main.c, por lo que se define implícitamente comoint f1()
, lo que significa que es una función que toma un número desconocido de argumentos y devuelve unint
.Si
int
ybool
son de diferentes tamaños, esto dará como resultado un comportamiento indefinido . Por ejemplo, en mi máquina,int
tiene 4 bytes ybool
un byte. Dado que la función está definida para devolverbool
, coloca un byte en la pila cuando regresa. Sin embargo, como se declara implícitamente que regresaint
de main.c, la función de llamada intentará leer 4 bytes de la pila.Las opciones de compiladores predeterminadas en gcc no le dirán que está haciendo esto. Pero si compila con
-Wall -Wextra
, obtendrá esto:Para solucionar esto, agregue una declaración para
f1
main.c, antesmain
:Tenga en cuenta que la lista de argumentos se establece explícitamente en
void
, lo que le dice al compilador que la función no toma argumentos, en oposición a una lista de parámetros vacía que significa un número desconocido de argumentos. La definiciónf1
en f1.c también debe cambiarse para reflejar esto.fuente
-Werror-implicit-function-declaration
a las opciones de GCC, de esta manera ya no se me escapa. Una opción aún mejor es-Werror
convertir todas las advertencias en errores. Te obliga a corregir todas las advertencias cuando aparecen.Creo que es interesante ver dónde sucede realmente la falta de coincidencia de tamaño mencionada en la excelente respuesta de Lundin.
Si compila con
--save-temps
, obtendrá archivos de ensamblaje que puede ver. Aquí está la parte dondef1()
hace la== 0
comparación y devuelve su valor:La parte de regreso es
sete %al
. En las convenciones de llamadas x86 de C, los valores de retorno de 4 bytes o menos (que incluyeint
ybool
) se devuelven a través del registro%eax
.%al
es el byte más bajo de%eax
. Por lo tanto, los 3 bytes superiores%eax
quedan en un estado no controlado.Ahora en
main()
:Esto comprueba si el conjunto de
%eax
es cero, ya que piensa que está poniendo a prueba un int.Agregar una declaración de función explícita cambia
main()
a:que es lo que queremos
fuente
Compile con un comando como este:
Salida:
Con tal mensaje, debe saber qué hacer para corregirlo.
Editar: después de leer un comentario (ahora eliminado), intenté compilar su código sin las banderas. Bueno, esto me llevó a errores de enlazador sin advertencias del compilador en lugar de errores del compilador. Y esos errores del enlazador son más difíciles de entender, por lo que incluso si
-std-gnu99
no es necesario, intente usarlo al menos para evitarle-Wall -Werror
mucho dolor.fuente