Esta función de C siempre debería devolver falso, pero no lo hace.

9 minutos de lectura

avatar de usuario
Dimitri Podborsky

Me encontré 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 ya que var3 == 3000. los main la 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;
}

Ya que f1() siempre debe volver falseuno esperaría que el programa imprimiera solo una falso a la pantalla Pero después de compilarlo y ejecutarlo, ejecutado también se muestra:

$ 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.

  • Otros han mencionado que necesita un prototipo porque sus funciones están en archivos separados. Pero incluso si copiaste 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 la )). Para ser correcto C, debe cambiar su código a bool f1(void).

    – ulitestigo

    08/04/2016 a las 12:21


  • los main() podría simplificarse a int main() { puts(f1() == true ? "true" : "false"); puts(f1() ? "true" : "false"); return 0; } – esto mostraría mejor la discrepancia.

    – Palec

    10 de abril de 2016 a las 12:05

  • @uliwitness ¿Qué pasa con K&R 1st ed. (1978) cuando no había void?

    – Ho1

    11 de abril de 2016 a las 16:39


  • @uliwitness No hubo true y false en K&R 1st ed., por lo que no hubo tales problemas en absoluto. Era solo 0 y distinto de cero para verdadero y falso. ¿no es así? No sé si los prototipos estaban disponibles en ese momento.

    – Ho1

    11/04/2016 a las 16:45


  • K&R 1st Edn precedió a los prototipos (y al estándar C) por más de una década (1978 para el libro frente a 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 _Bool tipo y no <stdbool.h> encabezamiento.

    –Jonathan Leffler

    13 de abril de 2016 a las 6:50

avatar de usuario
Lundin

Como se señaló en otras respuestas, el problema es que 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 gran falla en el lenguaje C: si no declaraba un prototipo antes de usar una función, por defecto sería int func () (dónde ( ) significa “aceptar cualquier parámetro”). Esto cambia la convención de llamada de la función. func, pero no cambia la definición de la función real. Dado que el tamaño de bool y int son diferentes, su código invoca un comportamiento indefinido cuando se llama a la función.

Este peligroso comportamiento sin sentido se solucionó en el año 1999, con el lanzamiento del estándar C99. Se prohibieron las declaraciones de funciones implícitas.

Desafortunadamente, GCC hasta la versión 5.xx todavía usa el antiguo estándar C de forma predeterminada. Probablemente no haya ninguna razón por la que deba compilar su código como algo que no sea C estándar. 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) (conocido informalmente como C11).
  • -pedantic-errors le dice que haga lo anterior de todo corazón y que dé errores de compilación cuando escriba un código incorrecto que viole el estándar C.
  • -Wall significa darme algunas advertencias adicionales que podría ser bueno tener.
  • -Wextra significa darme otras advertencias adicionales que podría ser bueno tener.

  • Esta respuesta es correcta en general, pero para programas más complicados -std=gnu11 es mucho más probable que funcione como se espera que -std=c11, debido a alguno o todos los siguientes: necesidad de funcionalidad de biblioteca más allá de C11 (POSIX, X/Open, etc.) que está disponible en los modos extendidos “gnu” pero suprimida 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 definiciones de tipo no estándar; uso involuntario de trigraphs (esta característica incorrecta estándar está deshabilitada en el modo “gnu”).

    – zwol

    07/04/2016 a las 14:51


  • Por razones similares, aunque generalmente animo el uso de niveles altos de advertencias, no puedo apoyar el uso de modos de advertencias-son-errores. -pedantic-errors es menos problemático que -Werror pero ambos pueden y hacen que los programas no se compilen en los sistemas operativos que no están incluidos en las pruebas del autor original, incluso cuando no hay un problema real.

    – zwol

    07/04/2016 a las 14:55

  • @Lundin Por el contrario, el segundo problema que mencioné (errores en los encabezados del sistema que están expuestos por modos de conformidad estricta) es ubicuo; He realizado pruebas extensas y sistemáticas, y hay no sistemas operativos ampliamente utilizados que no tienen al menos uno de esos errores (desde hace dos años, de todos modos). Los programas C que requieren solo la funcionalidad de C11, sin más adiciones, también son la excepción y no la regla en mi experiencia.

    – zwol

    07/04/2016 a las 15:10


  • @joop Si usa C estándar bool/_Bool entonces puede escribir su código C en una forma “C++-esque”, donde asume que todas las comparaciones y operadores lógicos devuelven un bool como en C++, aunque devuelven un int en C, por razones históricas. Esto tiene la gran ventaja de que puede usar 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 lo que es menos importante, también ahorra unos pocos bytes de RAM.

    – Lundin

    08/04/2016 a las 13:47


  • 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

    10 de abril de 2016 a las 16:04

avatar de usuario
arbusto

No tienes un prototipo declarado para 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 int y bool son de diferentes tamaños, esto resultará en comportamiento indefinido. Por ejemplo, en mi máquina, int es de 4 bytes y bool es un byte. Como la función es definido regresar bool, pone un byte en la pila cuando regresa. Sin embargo, dado que es implícitamente declarado regresar int desde 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 compilas con -Wall -Wextraobtendrás esto:

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

Para arreglar esto, agregue una declaración para f1 en main.c, antes main:

bool f1(void);

Tenga en cuenta que la lista de argumentos se establece explícitamente en void, que le dice al compilador que la función no toma argumentos, a diferencia de una lista de parámetros vacía que significa un número desconocido de argumentos. La definición f1 en f1.c también debe cambiarse para reflejar esto.

  • Algo que solía hacer en mis proyectos (cuando todavía usaba GCC) era agregar -Werror-implicit-function-declaration a las opciones de GCC, así esta ya no se escapa. Una opción aún mejor es -Werror para convertir todas las advertencias en errores. Te obliga a corregir todas las advertencias cuando aparecen.

    – ulitestigo

    08/04/2016 a las 12:41


  • 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

    08/04/2016 a las 13:52

  • @uliwitness Ah. Buena información para los que vienen de C++ y solo incursionan en C.

    – Rara vez ‘¿Dónde está Mónica?’ Needy

    08/04/2016 a las 18:00

  • El valor de retorno generalmente no se coloca en la pila, sino en un registro. Ver la respuesta de Owen. Además, por lo general, nunca coloca un byte en la pila, sino un múltiplo del tamaño de la palabra.

    – rsanchez

    09/04/2016 a las 22:00

  • Las versiones más nuevas de GCC (5.xx) dan esa advertencia sin las banderas adicionales.

    – Overv

    12/04/2016 a las 19:20

Creo que es interesante ver dónde ocurre realmente el desajuste de tamaño mencionado en la excelente respuesta de Lundin.

Si compilas con --save-temps, obtendrá archivos de ensamblaje que puede consultar. Aquí está la parte donde f1() hace el == 0 comparación y devuelve su valor:

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

La parte que regresa es sete %al. En las convenciones de llamadas x86 de C, devuelva valores de 4 bytes o menos (lo que incluye int y bool) se devuelven a través del registro %eax. %al es el byte más bajo de %eax. Entonces, los 3 bytes superiores de %eax quedan en un estado descontrolado.

Ahora en main():

call    f1
testl   %eax, %eax
je  .L2

Esto comprueba si el entero de %eax es cero, porque cree que está probando un int.

Agregar cambios en la declaración de una función explícita main() a:

call    f1
testb   %al, %al
je  .L2

que es lo que queremos.

avatar de usuario
jdarthenay

Compile con un comando como este:

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

Producción:

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), traté de compilar su código sin las banderas. Bueno, esto me llevó a errores del enlazador sin advertencias del compilador en lugar de errores del compilador. Y esos errores del enlazador son más difíciles de entender, así que incluso si -std-gnu99 no es necesario, intente usar siempre al menos -Wall -Werror te ahorrará muchos dolores de cabeza.

¿Ha sido útil esta solución?

Esta web utiliza cookies propias y de terceros para su correcto funcionamiento y para fines analíticos y para mostrarte publicidad relacionada con sus preferencias en base a un perfil elaborado a partir de tus hábitos de navegación. Al hacer clic en el botón Aceptar, acepta el uso de estas tecnologías y el procesamiento de tus datos para estos propósitos. Configurar y más información
Privacidad