Esta función C siempre debe devolver falso, pero no

317

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 falsedesde entonces var3 == 3000. La mainfunció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.

Dimitri Podborski
fuente
99
Otros han mencionado que necesita un prototipo porque sus funciones están en archivos separados. Pero incluso si copió f1()en el mismo archivo que main(), 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 a bool f1(void).
uliwitness
1
Esto main()podría simplificarse int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; }: esto mostraría mejor la discrepancia.
Palec
@uliwitness ¿Qué pasa con K&R 1st ed. (1978) cuando no había void?
Ho1
@uliwitness No hubo truey falseen 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.
Ho1
1
K&R 1st Edn precedió a los prototipos (y el estándar C) en más de una década (1978 para el libro vs 1989 para el estándar); de hecho, C ++ (C con clases) todavía estaba en el futuro cuando se publicó K & R1. Además, antes de C99, no había _Booltipo ni <stdbool.h>encabezado.
Jonathan Leffler

Respuestas:

396

Como se señaló en otras respuestas, el problema es que se usa gccsin 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ón func, pero no cambia la definición real de la función. Dado que el tamaño de booly intson 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:

gcc -std=c11 -pedantic-errors -Wall -Wextra
  • -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.
Lundin
fuente
19
Esta respuesta es correcta en general, pero para programas más complicados -std=gnu11es 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").
zwol
55
Por razones similares, aunque generalmente fomento el uso de niveles altos de advertencias, no puedo soportar el uso de modos de advertencias son errores. -pedantic-errorses menos problemático que, -Werrorpero 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.
zwol
77
@Lundin Por el contrario, el segundo problema que mencioné (errores en los encabezados del sistema que están expuestos por modos de conformidad estrictos) es omnipresente ; He realizado pruebas exhaustivas y sistemáticas, y no hay sistemas operativos ampliamente utilizados que no tengan al menos uno de esos errores (de todos modos, hace dos años). Los programas de C que requieren solo la funcionalidad de C11, sin más adiciones, también son la excepción más que la regla en mi experiencia.
zwol
66
@joop Si usa C 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 un boollike en C ++, aunque devuelvan un intC 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.
Lundin
77
Tenga en cuenta que la mayoría de las cosas nuevas en C99 provienen de esa basura GNU de más de 25 años.
Shahbaz
141

No tiene un prototipo declarado f1()en main.c, por lo que se define implícitamente como int f1(), lo que significa que es una función que toma un número desconocido de argumentos y devuelve un int.

Si inty boolson de diferentes tamaños, esto dará como resultado un comportamiento indefinido . Por ejemplo, en mi máquina, inttiene 4 bytes y boolun byte. Dado que la función está definida para devolver bool, coloca un byte en la pila cuando regresa. Sin embargo, como se declara implícitamente que regresa intde 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:

main.c: In function ‘main’:
main.c:6: warning: implicit declaration of function ‘f1’

Para solucionar esto, agregue una declaración para f1main.c, antes main:

bool f1(void);

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ón f1en f1.c también debe cambiarse para reflejar esto.

dbush
fuente
2
Algo que solía hacer en mis proyectos (cuando todavía usaba GCC) se agregaba -Werror-implicit-function-declarationa las opciones de GCC, de esta manera ya no se me escapa. Una opción aún mejor es -Werrorconvertir todas las advertencias en errores. Te obliga a corregir todas las advertencias cuando aparecen.
uliwitness
2
Tampoco debe usar paréntesis vacíos porque hacerlo es una característica obsoleta. Lo que significa que pueden prohibir dicho código en la próxima versión del estándar C.
Lundin
1
@uliwitness Ah. Buena información para aquellos que vienen de C ++ que solo incursionan en C.
SeldomNeedy
El valor de retorno generalmente no se coloca en la pila, sino en un registro. Ver la respuesta de Owen. Además, generalmente nunca pone un byte en la pila, sino un múltiplo del tamaño de la palabra.
rsanchez
Las versiones más recientes de GCC (5.xx) dan esa advertencia sin las banderas adicionales.
Overv
36

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 donde f1()hace la == 0comparación y devuelve su valor:

cmpl    $0, -4(%rbp)
sete    %al

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 incluye inty bool) se devuelven a través del registro %eax. %ales el byte más bajo de %eax. Por lo tanto, los 3 bytes superiores %eaxquedan en un estado no controlado.

Ahora en main():

call    f1
testl   %eax, %eax
je  .L2

Esto comprueba si el conjunto de %eaxes cero, ya que piensa que está poniendo a prueba un int.

Agregar una declaración de función explícita cambia main()a:

call    f1
testb   %al, %al
je  .L2

que es lo que queremos

Owen
fuente
27

Compile con un comando como este:

gcc -Wall -Wextra -Werror -std=gnu99 -o main.exe main.c

Salida:

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

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-gnu99no es necesario, intente usarlo al menos para evitarle -Wall -Werrormucho dolor.

jdarthenay
fuente