Considere el siguiente código "C":
#include<stdio.h>
main()
{
printf("func:%d",Func_i());
}
Func_i()
{
int i=3;
return i;
}
Func_i()
se define al final del código fuente y no se proporciona ninguna declaración antes de su uso en main()
. En el mismo momento en que el compilador ve Func_i()
en main()
, sale de la main()
y se entera Func_i()
. El compilador de alguna manera encuentra el valor devuelto por Func_i()
y se lo da printf()
. También sé que el compilador no puede encontrar el tipo de retorno de Func_i()
. Por defecto, toma (¿adivina?) El tipo de retorno de Func_i()
ser int
. Es decir, si el código tuviera float Func_i()
entonces el compilador daría el error: Tipos conflictivos paraFunc_i()
.
De la discusión anterior vemos que:
El compilador puede encontrar el valor devuelto por
Func_i()
.- Si el compilador puede encontrar el valor devuelto al
Func_i()
salirmain()
y buscar el código fuente, entonces ¿por qué no puede encontrar el tipo de Func_i (), que se menciona explícitamente ?
- Si el compilador puede encontrar el valor devuelto al
El compilador debe saber que
Func_i()
es de tipo flotante, es por eso que da el error de tipos en conflicto.
- Si el compilador sabe que
Func_i
es de tipo flotante, ¿por qué sigue suponiendoFunc_i()
que es de tipo int y da el error de tipos en conflicto? ¿Por qué no hace forzosamenteFunc_i()
ser de tipo flotante?
Tengo la misma duda con la declaración de variable . Considere el siguiente código "C":
#include<stdio.h>
main()
{
/* [extern int Data_i;]--omitted the declaration */
printf("func:%d and Var:%d",Func_i(),Data_i);
}
Func_i()
{
int i=3;
return i;
}
int Data_i=4;
El compilador da el error: 'Data_i' no declarado (primer uso en esta función).
- Cuando el compilador ve
Func_i()
, baja al código fuente para encontrar el valor devuelto por Func_ (). ¿Por qué el compilador no puede hacer lo mismo para la variable Data_i?
Editar:
No conozco los detalles del funcionamiento interno del compilador, ensamblador, procesador, etc. La idea básica de mi pregunta es que si por fin digo (escribo) el valor de retorno de la función en el código fuente, después del uso de esa función, entonces el lenguaje "C" permite que la computadora encuentre ese valor sin dar ningún error. Ahora, ¿por qué la computadora no puede encontrar el tipo de manera similar? ¿Por qué no se puede encontrar el tipo de Data_i como se encontró el valor de retorno de Func_i ()? Incluso si uso la extern data-type identifier;
declaración, no estoy diciendo que el valor sea devuelto por ese identificador (función / variable). Si la computadora puede encontrar ese valor, ¿por qué no puede encontrar el tipo? ¿Por qué necesitamos la declaración adelantada?
Gracias.
fuente
Func_i
inválido. Nunca hubo una regla para declarar implícitamente variables indefinidas, por lo que el segundo fragmento siempre estaba mal formado. (Sí, los compiladores todavía aceptan la primera muestra porque era válida, si era descuidada, según C89 / C90.)Respuestas:
Debido a que C es un solo pase , tipos estáticos- , débilmente tipificado , compilado lenguaje.
Single-pass significa que el compilador no mira hacia adelante para ver la definición de una función o variable. Como el compilador no mira hacia adelante, la declaración de una función debe venir antes del uso de la función; de lo contrario, el compilador no sabe cuál es su firma de tipo. Sin embargo, la definición de la función puede estar más adelante en el mismo archivo, o incluso en un archivo completamente diferente. Ver punto 4.
La única excepción es el artefacto histórico de que se supone que las funciones y variables no declaradas son del tipo "int". La práctica moderna es evitar la tipificación implícita declarando siempre funciones y variables explícitamente.
Tipo estático significa que toda la información de tipo se calcula en tiempo de compilación. Esa información se utiliza para generar código de máquina que se ejecuta en tiempo de ejecución. No hay ningún concepto en C de mecanografía en tiempo de ejecución. Una vez int, siempre int, una vez flotante, siempre flotante. Sin embargo, ese hecho está algo oscurecido por el siguiente punto.
Tipo débil significa que el compilador de C genera automáticamente código para convertir entre tipos numéricos sin requerir que el programador especifique explícitamente las operaciones de conversión. Debido al tipeo estático, la misma conversión siempre se realizará de la misma manera cada vez a través del programa. Si un valor flotante se convierte en un valor int en un punto dado en el código, un valor flotante siempre se convertirá en un valor int en ese punto en el código. Esto no se puede cambiar en tiempo de ejecución. El valor en sí mismo puede cambiar de una ejecución del programa a la siguiente, por supuesto, y las declaraciones condicionales pueden cambiar qué secciones del código se ejecutan en qué orden, pero una sola sección de código dada sin llamadas a funciones o condicionales siempre realizará exactamente mismas operaciones cada vez que se ejecuta.
Compilado significa que el proceso de analizar el código fuente legible por humanos y transformarlo en instrucciones legibles por máquina se lleva a cabo antes de que se ejecute el programa. Cuando el compilador está compilando una función, no tiene conocimiento de lo que encontrará más abajo en un archivo fuente dado. Sin embargo, una vez que se ha completado la compilación (y ensamblaje, vinculación, etc.), cada función en el ejecutable finalizado contiene punteros numéricos a las funciones que llamará cuando se ejecute. Es por eso que main () puede llamar a una función más abajo en el archivo fuente. Para cuando se ejecute main (), contendrá un puntero a la dirección de Func_i ().
El código de la máquina es muy, muy específico. El código para agregar dos enteros (3 + 2) es diferente del que se usa para agregar dos flotantes (3.0 + 2.0). Ambos son diferentes de agregar un int a un flotante (3 + 2.0), y así sucesivamente. El compilador determina para cada punto de una función qué operación exacta necesita llevarse a cabo en ese punto, y genera un código que lleva a cabo esa operación exacta. Una vez hecho esto, no se puede cambiar sin volver a compilar la función.
Al unir todos estos conceptos, la razón por la cual main () no puede "ver" más abajo para determinar el tipo de Func_i () es que el análisis de tipo ocurre al comienzo del proceso de compilación. En ese momento, solo se ha leído y analizado la parte del archivo fuente hasta la definición de main (), y el compilador aún no conoce la definición de Func_i ().
La razón por la que main () puede "ver" dónde Func_i () debe llamarlo es porque la llamada ocurre en tiempo de ejecución, después de que la compilación ya ha resuelto todos los nombres y tipos de todos los identificadores, el ensamblaje ya ha convertido todos los funciones al código de máquina, y la vinculación ya ha insertado la dirección correcta de cada función en cada lugar que se llama.
Por supuesto, he omitido la mayoría de los detalles sangrientos. El proceso real es mucho, mucho más complicado. Espero haber proporcionado suficiente información general de alto nivel para responder a sus preguntas.
Además, recuerde que lo que he escrito anteriormente se aplica específicamente a C.
En otros idiomas, el compilador puede realizar múltiples pases a través del código fuente, por lo que el compilador podría elegir la definición de Func_i () sin que esté previamente declarado.
En otros idiomas, las funciones y / o variables se pueden escribir dinámicamente, por lo que una sola variable podría contener, o se podría pasar o devolver una sola función, un entero, un flotante, una cadena, una matriz o un objeto en diferentes momentos.
En otros idiomas, la escritura puede ser más fuerte, lo que requiere que se especifique explícitamente la conversión de punto flotante a entero. En otros idiomas, la escritura puede ser más débil, permitiendo que la conversión de la cadena "3.0" al float 3.0 al entero 3 se realice automáticamente.
Y en otros idiomas, el código puede interpretarse una línea a la vez, o compilarse en código de bytes y luego interpretarse, o compilarse justo a tiempo, o pasar por una amplia variedad de otros esquemas de ejecución.
fuente
Func_()+1
: aquí en el tiempo de compilación el compilador tiene que saber el tipo deFunc_i()
fin de generar el código de máquina apropiada. Quizás o no sea posible que el ensamblado lo manejeFunc_()+1
llamando al tipo en tiempo de ejecución, o sea posible, pero hacerlo hará que el programa se ralentice en el tiempo de ejecución. Creo que es suficiente para mí por ahora.int func(...)
... es decir, toman una lista de argumentos variadic. Esto significa que si define una función comoint putc(char)
pero se olvida de declararla, en su lugar se llamará comoint putc(int)
(porque el char pasado a través de una lista de argumentos variados es promovido aint
). Entonces, aunque el ejemplo del OP funcionó porque su firma coincidía con la declaración implícita, es comprensible por qué este comportamiento se desalentó (y se agregaron las advertencias apropiadas).Una restricción de diseño del lenguaje C era que se suponía que debía ser compilado por un compilador de un solo paso, lo que lo hace adecuado para sistemas con limitaciones de memoria. Por lo tanto, el compilador solo conoce en cualquier momento las cosas que se mencionaron anteriormente. El compilador no puede saltar hacia adelante en la fuente para encontrar una declaración de función y luego volver a compilar una llamada a esa función. Por lo tanto, todos los símbolos deben declararse antes de ser utilizados. Puede pre-declarar una función como
en la parte superior o en un archivo de encabezado para ayudar al compilador.
En sus ejemplos, utiliza dos características dudosas del lenguaje C que deben evitarse:
Si una función se usa antes de que se declare correctamente, se usa como una "declaración implícita". El compilador utiliza el contexto inmediato para descubrir la firma de la función. El compilador no escaneará el resto del código para descubrir cuál es la declaración real.
Si se declara algo sin un tipo, se considera que el tipo es
int
. Este es, por ejemplo, el caso de variables estáticas o tipos de retorno de funciones.Entonces
printf("func:%d",Func_i())
, tenemos una declaración implícitaint Func_i()
. Cuando el compilador alcanza la definición de la funciónFunc_i() { ... }
, esto es compatible con el tipo. Pero si escribistefloat Func_i() { ... }
en este punto, tienes la implicidad declaradaint Func_i()
y la explícitamente declaradafloat Func_i()
. Como las dos declaraciones no coinciden, el compilador le da un error.Aclarando algunas ideas falsas
El compilador no encuentra el valor devuelto por
Func_i
. La ausencia de un tipo explícito significa que el tipo de retorno esint
por defecto. Incluso si haces esto:entonces el tipo será
int Func_i()
, ¡y el valor de retorno se truncará silenciosamente!El compilador finalmente conoce el tipo real de
Func_i
, pero no conoce el tipo real durante la declaración implícita. Solo cuando más tarde alcanza la declaración real puede averiguar si el tipo declarado implícitamente era correcto. Pero en ese punto, el ensamblado para la llamada a la función ya podría haberse escrito y no se puede cambiar en el modelo de compilación C.fuente
Unit
hace un buen tipo predeterminado desde el punto de vista de la teoría de tipos, pero falla en los aspectos prácticos de la programación cercana a los sistemas metálicos para los que B y C fueron diseñados.Func_i()
, genera y guarda inmediatamente el código para que el procesador salte a otra ubicación, luego reciba un número entero y luego continúe. Cuando el compilador luego encuentra laFunc_i
definición, se asegura de que las firmas coincidan, y si lo hacen, coloca el ensambladoFunc_i()
en esa dirección y le dice que devuelva algún número entero. Cuando ejecuta el programa, el procesador sigue esas instrucciones con el valor3
.Primero, sus programas son válidos para el estándar C90, pero no para los siguientes. int implícito (que permite declarar una función sin dar su tipo de retorno), y la declaración implícita de funciones (que permite usar una función sin declararla) ya no son válidas.
Segundo, eso no funciona como piensas.
El tipo de resultado es opcional en C90, no dar uno significa un
int
resultado. Que también es cierto para la declaración de variables (pero debe dar una clase de almacenamiento,static
oextern
).Lo que hace el compilador cuando ve que
Func_i
se llama sin una declaración previa, es asumir que hay una declaraciónno busca más en el código para ver qué tan efectivamente
Func_i
se declara. SiFunc_i
no fue declarado o definido, el compilador no cambiaría su comportamiento al compilarmain
. La declaración implícita es solo para función, no hay ninguna para variable.Tenga en cuenta que la lista de parámetros vacía en la declaración no significa que la función no toma parámetros (debe especificar
(void)
para eso), significa que el compilador no tiene que verificar los tipos de parámetros y será el mismo conversiones implícitas que se aplican a argumentos pasados a funciones variadas.fuente
extern int Func_i()
. No se ve a ninguna parte.-S
(si lo está usandogcc
) le permitirá ver el código de ensamblaje generado por el compilador. Luego puede tener una idea de cómo se manejan los valores de retorno en tiempo de ejecución (normalmente usando un registro de procesador o algo de espacio en la pila de programas).Escribiste en un comentario:
Esa es una idea falsa: la ejecución no es don línea por línea. La compilación se realiza línea por línea, y la resolución de nombres se realiza durante la compilación, y solo resuelve nombres, no devuelve valores.
Un modelo conceptual útil es este: cuando el compilador lee la línea:
emite un código equivalente a:
El compilador también hace una nota en alguna tabla interna que
function #2
es una función aún no declarada llamadaFunc_i
, que toma un número no especificado de argumentos y devuelve un int (el valor predeterminado).Más tarde, cuando analiza esto:
el compilador busca
Func_i
en la tabla mencionada anteriormente y comprueba si los parámetros y el tipo de retorno coinciden. Si no lo hacen, se detiene con un mensaje de error. Si lo hacen, agrega la dirección actual a la tabla de funciones internas y pasa a la siguiente línea.Entonces, el compilador no "buscó"
Func_i
cuándo analizó la primera referencia. Simplemente hizo una nota en alguna tabla, y continuó analizando la siguiente línea. Y al final del archivo, tiene un archivo de objeto y una lista de direcciones de salto.Más tarde, el enlazador toma todo esto y reemplaza todos los punteros a la "función # 2" con la dirección de salto real, por lo que emite algo como:
Mucho más tarde, cuando se ejecuta el archivo ejecutable, la dirección de salto ya está resuelta y la computadora puede saltar a la dirección 0x1215. No se requiere búsqueda de nombre.
Descargo de responsabilidad : como dije, ese es un modelo conceptual, y el mundo real es más complicado. Los compiladores y enlazadores hacen todo tipo de optimizaciones locas hoy. Incluso podrían "subir y bajar" para buscar
Func_i
, aunque lo dudo. Pero los lenguajes C se definen de una manera que podría escribir un compilador súper simple como ese. Entonces, la mayoría de las veces, es un modelo muy útil.fuente
1. call "function #2", put the return-type onto the stack and put the return value on the stack?
printf(..., Func_i()+1);
? El compilador tiene que saber el tipo deFunc_i
, por lo que puede decidir si debe emitir unaadd integer
o unaadd float
instrucción. Puede encontrar algunos casos especiales en los que el compilador podría continuar sin la información de tipo, pero el compilador tiene que funcionar para todos los casos.float
los valores pueden vivir en un registro FPU, entonces no habría ninguna instrucción en absoluto. El compilador solo realiza un seguimiento de qué valor se almacena en cada registro durante la compilación, y emite cosas como "agregar constante 1 al registro FP X". O podría vivir en la pila, si no hay registros libres. Luego habría una instrucción "aumentar el puntero de pila en 4", y el valor sería "referenciado" como algo así como "puntero de pila - 4". Pero todas estas cosas solo funcionan si los tamaños de todas las variables (antes y después) en la pila se conocen en tiempo de compilación.Func_i()
o / yData_i
, tiene que determinar sus tipos; no es posible en lenguaje ensamblador realizar una llamada al tipo de datos. Necesito estudiar las cosas en detalle para estar seguro.C y varios otros lenguajes que requieren declaraciones fueron diseñados en una era en la que el tiempo de procesador y la memoria eran caros. El desarrollo de C y Unix fue de la mano durante bastante tiempo, y este último no tenía memoria virtual hasta que apareció 3BSD en 1979. Sin el espacio adicional para trabajar, los compiladores tendían a ser asuntos de un solo paso porque no requieren la capacidad de mantener alguna representación del archivo completo en la memoria de una vez.
Los compiladores de un solo paso están, como nosotros, cargados con la incapacidad de ver el futuro. Esto significa que lo único que pueden saber con certeza es lo que se les ha dicho explícitamente antes de que se compile la línea de código. Está claro para cualquiera de nosotros que
Func_i()
se declara más adelante en el archivo fuente, pero el compilador, que opera en una pequeña porción de código a la vez, no tiene ni idea de que se acerca.A principios de C (AT&T, K&R, C89), el uso de una función
foo()
antes de la declaración resultó en una declaración de facto o implícita deint foo()
. Su ejemplo funciona cuandoFunc_i()
se declaraint
porque coincide con lo que el compilador declaró en su nombre. Cambiarlo a cualquier otro tipo dará lugar a un conflicto porque ya no coincide con lo que el compilador eligió en ausencia de una declaración explícita. Este comportamiento se eliminó en C99, donde el uso de una función no declarada se convirtió en un error.Entonces, ¿qué pasa con los tipos de retorno?
La convención de llamada para el código objeto en la mayoría de los entornos requiere conocer solo la dirección de la función que se llama, lo cual es relativamente fácil de manejar para los compiladores y enlazadores. La ejecución salta al inicio de la función y regresa cuando regresa. Cualquier otra cosa, en particular los arreglos de pasar argumentos y un valor de retorno, está determinada enteramente por la persona que llama y quien llama en un acuerdo llamado convención de llamada . Siempre y cuando ambos compartan el mismo conjunto de convenciones, es posible que un programa invoque funciones en otros archivos de objetos si se compilaron en cualquier lenguaje que comparta esas convenciones. (En informática científica, te encuentras con un montón de C llamando a FORTRAN y viceversa, y la capacidad de hacerlo proviene de tener una convención de llamadas).
Otra característica de principios de C fue que los prototipos como los conocemos ahora no existían. Podría declarar el tipo de retorno de una función (por ejemplo,
int foo()
), pero no sus argumentos (es decir,int foo(int bar)
no era una opción). Esto existió porque, como se describió anteriormente, el programa siempre se atuvo a una convención de llamada que podría ser determinada por los argumentos. Si llamó a una función con el tipo incorrecto de argumentos, era una situación de basura, basura.Debido a que el código objeto tiene la noción de un retorno pero no un tipo de retorno, un compilador tiene que conocer el tipo de retorno para manejar el valor devuelto. Cuando ejecuta las instrucciones de la máquina, todo son solo bits y al procesador no le importa si la memoria en la que está tratando de comparar
double
realmente tiene unaint
. Simplemente hace lo que pides, y si lo rompes, eres el propietario de ambas piezas.Considere estos bits de código:
El código de la izquierda se reduce a una llamada
foo()
seguida de copiando el resultado proporcionado a través de la convención de llamada / retorno en cualquier lugar dondex
esté almacenado. Ese es el caso fácil.El código de la derecha muestra una conversión de tipo y es por eso que los compiladores necesitan conocer el tipo de retorno de una función. Los números de punto flotante no se pueden volcar en la memoria donde otro código esperará ver un
int
porque no hay conversión mágica que tenga lugar. Si el resultado final tiene que ser un número entero, debe haber instrucciones que guíen al procesador para realizar la conversión antes del almacenamiento. Sin conocer el tipo de retornofoo()
antes de tiempo, el compilador no tendría idea de que el código de conversión es necesario.Los compiladores de pasadas múltiples permiten todo tipo de cosas, una de las cuales es la capacidad de declarar variables, funciones y métodos después de su primer uso. Esto significa que cuando el compilador llega a compilar el código, ya ha visto el futuro y sabe qué hacer. Java, por ejemplo, exige múltiples pasadas en virtud del hecho de que su sintaxis permite la declaración después del uso.
fuente
Func_i()
el valor de retorno?double foo(); int x; x = foo();
simplemente da el error. Sé que no podemos hacer esto. Mi pregunta es que en la llamada a la función el procesador solo encuentra el valor de retorno; ¿por qué no puede encontrar también el tipo de retorno también?foo()
, por lo que el compilador sabe qué hacer con él.