Me gusta más C++ que C, pero este simple ejemplo de código es una gran sorpresa para mí:
int foo() { return 3; } int main() { int x; foo(&x); return x; }https://godbolt.org/z/4ov9nTzjM
Sin advertencias, sin problemas de vinculación, pero este código no es válido.
Me encontré con esto cuando un principiante presentó una pregunta con un código que tenía este extraño problema. Se hizo una pregunta en otro sitio (no en inglés) y me gustaría brindarle una mejor explicación de por qué se compila (y aprender algo sobre C). Su código original si alguien necesita verlo.
Sospecho que esto está relacionado de alguna manera con la declaración de función implícita, pero no estoy completamente seguro. ¿Por qué el compilador no se queja de que foo se llama con argumentos?
Aquí hay una asamblea :
foo: push rbp mov rbp, rsp mov eax, 3 pop rbp ret main: push rbp mov rbp, rsp sub rsp, 16 lea rax, [rbp-4] mov rdi, rax mov eax, 0 call foo mov eax, DWORD PTR [rbp-4] leave ret como puede ver, x permanece sin inicializar.
Esta función es 100% correcta.
En lenguaje C int foo() { significa: definir la función foo devolviendo int y tomando un número no especificado de parámetros.
Su confusión proviene de C++ plus, donde int foo(void) e int foo() significan exactamente lo mismo.
En el lenguaje C para definir una función que no toma ningún parámetro, debe definirla como:
int foo(void) {El compilador debe emitir un mensaje para este programa.
int foo() { return 3; } int main() { int x; foo(&x); return x; }porque la función foo llamó con un argumento aunque su lista de identificadores está vacía. Entonces el programa tiene un comportamiento indefinido.
Según el estándar C *6.7.6.3 Declaradores de funciones (incluidos los prototipos)
14 Una lista de identificadores declara solo los identificadores de los parámetros de la función. Una lista vacía en un declarador de función que forma parte de una definición de esa función especifica que la función no tiene parámetros. La lista vacía en un declarador de función que no forma parte de una definición de esa función especifica que no se proporciona información sobre el número o los tipos de parámetros.
Así que el programa no es válido.
Podrías convertirlo en un programa válido de la siguiente manera
int foo(); int main() { int x; foo(&x); return x; } int foo( int *p ) { return 3; } Aunque el compilador puede emitir una advertencia de que el parámetro p no se usa.
En este caso la declaración de la función que no es su definición significa que no hay información sobre el número y tipos de parámetros.
Opuesto a C en C++ esta declaración
int foo();es equivalente a
int foo( void );Los paréntesis vacíos son parte de una gramática C antigua para declaraciones de funciones que no proporcionaban verificación de tipos. Antes de la creación de listas de parámetros con tipos, las definiciones de funciones tenían un formato como este:
int foo(a, b, c) char a; int *b; float c; { // body of function } Luego, las declaraciones de funciones que no eran definiciones simplemente usarían () , como en int foo(); . Entonces, una definición de función mostraría qué argumentos esperaba una función, incluido el número de ellos y sus tipos, pero una declaración no.
Por supuesto, las definiciones de funciones a menudo no son visibles al compilar un archivo fuente, ya que las definiciones están en otros archivos, aunque, por supuesto, se puede proporcionar una declaración, a menudo en archivos de encabezado. Entonces, cuando se llama a una función, el compilador generalmente no vería la definición y generalmente no sabría qué argumentos requiere la función. Por lo tanto, no pudo verificar los argumentos. Por lo tanto, no es necesario que el compilador proporcione un mensaje de diagnóstico para una falta de coincidencia con las funciones declaradas con esta gramática antigua.
El programador es responsable de hacer coincidir los argumentos. La definición de la función especifica cuántos parámetros tiene la función y especifica sus tipos, y el estándar C no define el comportamiento si hay una falta de coincidencia. Pero ese es un problema de tiempo de ejecución; no es algo que el compilador deba diagnosticar, y no es algo que el compilador pueda diagnosticar si la definición de la función no es visible para él.
Cuando la definición es visible, el compilador podría emitir un diagnóstico, especialmente si la definición aparece antes de la llamada. Aunque GCC no diagnostica esto, Clang lo hace , si la definición aparece primero. Así que esta es una oportunidad perdida para los desarrolladores de GCC.
Cuando se creó la gramática para prototipos de funciones (declaraciones de funciones con tipos para los parámetros), () no se podía usar para indicar que la función no tenía parámetros, ya que ya estaba en uso para la gramática anterior. Así que la notación (void) se inventó para esto; para declarar, fuera de una definición, que una función no toma parámetros, debes usar (void) , como en int foo(void); . Esto también se puede usar en una definición de función, int foo(void) { … } .
Cuando una declaración de función tiene esta nueva forma, se requiere que el compilador verifique el número y los tipos de argumentos en una llamada a la función y emita un diagnóstico si hay una discrepancia.