Consideremos este código C:
#include <stdio.h>
main()
{
int x=5;
printf("x is ");
printf("%d",5);
}
En esto, cuando escribimos int x=5;
le dijimos a la computadora que x
es un número entero. La computadora debe recordar que x
es un número entero. Pero cuando sacamos el valor de x
in printf()
tenemos que decirle nuevamente a la computadora que x
es un número entero. ¿Porqué es eso?
¿Por qué la computadora olvida que x
era un número entero?
c
io
type-safety
usuario106313
fuente
fuente
printf(char*, ...)
y que sólo se pone (lo que equivale a) un puntero a una colección de datosprintf("x is %x in hex, and %d in decimal and %o as octal",x,x,x);
?Respuestas:
Aquí hay dos problemas en juego:
Problema 1: C es un lenguaje estáticamente escrito ; Toda la información de tipo se determina en tiempo de compilación. No se almacena información de tipo con ningún objeto en la memoria, de modo que su tipo y tamaño se puedan determinar en el tiempo de ejecución 1 . Si examina la memoria en cualquier dirección en particular mientras el programa se está ejecutando, todo lo que verá es un lodo de bytes; no hay nada que le diga si esa dirección particular realmente contiene un objeto, cuál es el tipo o tamaño de ese objeto, o cómo interpretar esos bytes (como un entero, tipo de punto flotante, o secuencia de caracteres en una cadena, etc. ) Toda esa información se integra en el código de la máquina cuando se compila el código, según la información de tipo especificada en el código fuente; por ejemplo, la definición de la función
le dice al compilador que genere el código de máquina apropiado para manejar
x
como un entero,y
como un valor de punto flotante yz
como un puntero achar
. Tenga en cuenta que cualquier discrepancia en el número o tipo de argumentos entre una llamada de función y una definición de función solo se detecta cuando el código se está compilando 2 ; es solo durante la fase de compilación que cualquier tipo de información está asociada con un objeto.Problema # 2:
printf
es una función variadic ; toma un parámetro fijo de tipoconst char * restrict
(la cadena de formato), junto con cero o más parámetros adicionales, cuyo número y tipo no se conocen en tiempo de compilación:La
printf
función no tiene forma de saber cuál es el número y los tipos de argumentos adicionales de los argumentos pasados mismos; tiene que confiar en la cadena de formato para decirle cómo interpretar el lodo de bytes en la pila (o en los registros). Aún mejor, debido a que es una función variada, los argumentos con ciertos tipos se promueven a un conjunto limitado de tipos predeterminados (por ejemplo,short
se promueve aint
,float
se promueve adouble
, etc.).Nuevamente, no hay información asociada con los argumentos adicionales en sí mismos para dar
printf
pistas sobre cómo interpretarlos o formatearlos. De ahí la necesidad de los especificadores de conversión en la cadena de formato.Tenga en cuenta que, además de indicar
printf
el número y el tipo de argumentos adicionales, los especificadores de conversión también indicanprintf
cómo formatear la salida (anchos de campo, precisión, relleno, justificación, base (decimal, octal o hexadecimal para tipos enteros), etc.).Editar
Para evitar una discusión extensa en los comentarios (y debido a que la página de chat está bloqueada en mi sistema de trabajo, sí, soy un chico malo), voy a abordar las dos últimas preguntas aquí.
Durante la traducción, el compilador mantiene una tabla (a menudo llamado tabla de símbolos ) que almacena información acerca de un objeto nombre, tipo, duración de almacenamiento, alcance, etc. Usted declara
b
yc
comofloat
, por lo que cada vez que el compilador ve una expresión conb
oc
en ella, generará el código de máquina para manejar un valor de punto flotante.Tomé su código arriba y envolví un programa completo alrededor de él:
Utilicé las opciones
-g
y-Wa,-aldh
con gcc para crear una lista del código de máquina generado intercalado con el código fuente C 3 :Aquí le mostramos cómo leer la lista de ensamblaje:
Una cosa a tener en cuenta aquí. En el código de ensamblaje generado, no hay símbolos para
b
oc
; solo existen en el listado del código fuente. Cuando semain
ejecuta en tiempo de ejecución, el espacio parab
yc
(junto con algunas otras cosas) se asigna desde la pila ajustando el puntero de la pila:El código se refiere a esos objetos por su desplazamiento desde el puntero de cuadro 4 ,
b
siendo -8 bytes de la dirección almacenada en el puntero de cuadro yc
siendo -4 bytes de él, de la siguiente manera:Desde que declaró
b
yc
como flotantes, el compilador generó código de máquina para manejar específicamente los valores de punto flotante; elmovsd
,mulsd
,cvtss2sd
las instrucciones son todas específicas para operaciones de punto flotante, y los registros%xmm0
y%xmm1
se utilizan para almacenar los valores de doble precisión en coma flotante.Si cambio el código fuente para que
b
yc
son números enteros en lugar de los flotadores, el compilador genera código de máquina diferente:Compilar con
gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c
da:Aquí está la misma operación, pero con
b
yc
declarado como enteros:Esto es lo que quise decir antes cuando dije que la información de tipo estaba "integrada" en el código de la máquina. Cuando se ejecuta el programa, no examina
b
nic
determina su tipo; ya sabe cuál debería ser su tipo en función del código máquina generado.No funciona porque le estás mintiendo al compilador. Le dice que
b
es unfloat
, por lo que generará código de máquina para manejar valores de punto flotante. Cuando lo inicializa, el patrón de bits correspondiente a la constante'H'
se interpretará como un valor de coma flotante, no como un valor de carácter.Mientes al compilador nuevamente cuando usas el
%c
especificador de conversión, que espera un valor de tipochar
, para el argumentob
. Debido a esto,printf
no interpretará el contenidob
correctamente, y terminará con la salida de basura 5 . Nuevamente,printf
no puedo saber el número o los tipos de argumentos adicionales basados en los argumentos mismos; todo lo que ve es una dirección en la pila (o un montón de registros). Necesita la cadena de formato para indicar qué argumentos adicionales se han pasado y cuáles son sus tipos.1. La única excepción son las matrices de longitud variable; dado que su tamaño no se establece hasta el tiempo de ejecución, no hay forma de evaluar
sizeof
un VLA en tiempo de compilación.2. A partir de C89, de todos modos. Antes de eso, el compilador solo podía detectar desajustes en el tipo de retorno de función; no pudo detectar desajustes en las listas de parámetros de funciones.
3. Este código se genera en un sistema SuSE Linux Enterprise 10 de 64 bits con gcc 4.1.2. Si está en una implementación diferente (arquitectura del compilador / SO / chip), las instrucciones exactas de la máquina serán diferentes, pero el punto general seguirá siendo válido; el compilador generará diferentes instrucciones para manejar flotantes vs.ints vs. cadenas, etc.
4. Cuando llama a una función en un programa en ejecución, un marco de pilase crea para almacenar los argumentos de la función, las variables locales y la dirección de la instrucción que sigue a la llamada a la función. Se utiliza un registro especial llamado puntero de cuadro para realizar un seguimiento del cuadro actual.
5. Por ejemplo, suponga un sistema big-endian donde el byte de alto orden es el byte direccionado. El patrón de bits para
H
se almacenaráb
como0x00000048
. Sin embargo, debido a que el%c
especificador de conversión indica que el argumento debe ser achar
, solo se leerá el primer byte, porprintf
lo que intentará escribir el carácter correspondiente a la codificación0x00
.fuente
putchar
función dice que espera 1 argumento de tipoint
; cuando el compilador genera el código de máquina, ese código de máquina asumirá que siempre recibe ese argumento entero único. No es necesario especificar el tipo en tiempo de ejecución.printf
formatea toda su salida como texto (ASCII o de otro modo); el especificador de conversión le indica cómo formatear la salida.printf( "%d\n", 65 );
escribirá la secuencia de caracteres'6'
y'5'
la salida estándar, porque el%d
especificador de conversión le dice que formatee el argumento correspondiente como un entero decimal.printf( "%c\n", 65 );
escribirá el carácter'A'
en la salida estándar, porque%c
le diceprintf
que formatee el argumento como un carácter del conjunto de caracteres de ejecución.<<
y>>
los operadores de E / S), pero sería añadir un poco de complejidad a la lengua. La inercia es difícil de superar a veces.Porque en el momento en que
printf
se llama y hace su trabajo, el compilador ya no está allí para decirle qué hacer.La función no obtiene ninguna información, excepto lo que hay en sus parámetros, y los parámetros vararg no tienen ningún tipo, por
printf
lo que no tendría idea de cómo imprimirlos si no obtuviera instrucciones explícitas a través de la cadena de formato. El compilador podría (generalmente) deducir de qué tipo es cada argumento, pero aún así tendría que escribir una cadena de formato para decirle dónde imprimir cada argumento en relación con el texto constante. Compara"$%d"
y"%d$"
; hacen cosas diferentes, y el compilador no puede adivinar cuál quieres. Como de todos modos tiene que componer una cadena de formato manualmente para especificar las posiciones de los argumentos , es una opción obvia descargar la tarea de indicar también los tipos de argumentos al usuario.La alternativa sería que el compilador escanee la cadena de formato en busca de posiciones, luego deduzca los tipos, reescriba la cadena de formato para agregar la información del tipo y luego compile la cadena modificada en su binario. Pero eso solo funcionaría para cadenas de formato literal ; C también permite cadenas de formato asignadas dinámicamente, y siempre habrá casos en los que el compilador no pueda reconstruir con precisión cuál será la cadena de formato en tiempo de ejecución. (Además, a veces desea imprimir algo como un tipo diferente y relacionado, realizando de manera efectiva un reparto estrecho; eso también es algo que ningún compilador puede predecir).
fuente
printf()
se pasa es un puntero a la cadena de formato y un puntero al búfer donde se pueden encontrar los argumentos. ¡Ni siquiera se pasa la longitud de este búfer! Esta es una de las razones por las que C puede ser mucho más rápido que otros lenguajes. Lo que está proponiendo es órdenes de magnitud más complejos.cout
en C ++.printf()
es lo que se conoce como función variadic , que acepta una cantidad variable de argumentos.Las funciones variables en C usan un prototipo especial para decirle al compilador que la lista de argumentos es de longitud desconocida:
El estándar C proporciona un conjunto de funciones
stdarg.h
que se pueden usar para recuperar los argumentos de uno en uno y convertirlos en un tipo determinado. Esto significa que las funciones variadas tienen que decidir el tipo de cada argumento por sí mismas.printf()
toma esta decisión en función de lo que está en la cadena de formato.Esta es una simplificación excesiva de cómo
printf()
funciona realmente, pero el proceso es el siguiente:El mismo proceso ocurre para todos los tipos que
printf()
son capaces de convertir. Puede ver un ejemplo de esto en el código fuente para la implementación de OpenBSDvfprintf()
, que es la función que sustentaprintf()
.Algunos compiladores de C son lo suficientemente inteligentes como para detectar llamadas
printf()
, evaluar la cadena de formato si es una constante y verificar que los tipos del resto de los argumentos sean compatibles con las conversiones especificadas. Este comportamiento no es obligatorio, por lo que el estándar aún requiere proporcionar el tipo como parte de la cadena de formato. Antes de realizar este tipo de comprobaciones, las discrepancias entre la cadena de formato y la lista de argumentos simplemente producían resultados falsos.En C ++,
<<
es un operador, que hace uso decout
, por ejemplo,cout << foo << bar
una expresión infija que puede evaluarse para su corrección en el momento de la compilación y convertirse en código que convierte las expresiones de la mano derecha en algo con lo quecout
puede lidiar.fuente
Los diseñadores de C querían hacer el compilador lo más simple posible. Si bien hubiera sido posible manejar la E / S de la misma manera que otros lenguajes, y requerir que el compilador proporcione automáticamente la rutina de E / S con información sobre los tipos de parámetros pasados, y aunque tal enfoque podría en muchos casos tener permitió un código más eficiente de lo que es posible con
printf
(*), definir las cosas de esa manera habría complicado el compilador.En los primeros días de C, el código que llamaba a una función no sabía ni le importaba qué argumentos esperaba. Cada argumento empujaría un cierto número de palabras en la pila de acuerdo con su tipo, y las funciones esperarían encontrar diferentes parámetros en la ranura de la pila superior, segunda a superior, etc. debajo de la dirección de retorno. Si un
printf
método pudiera averiguar dónde encontrar sus argumentos en la pila, el compilador no podría tratarlo de manera diferente a ningún otro método.En la práctica, el patrón de paso de parámetros previsto por C ya casi nunca se usa, excepto cuando se llaman funciones variadas como
printf
, y siprintf
se ha definido como el uso de convenciones especiales de paso de parámetros [por ejemplo, tener el primer parámetro como un compilador generado queconst char*
contiene autogenerado información sobre los tipos que se van a pasar], los compiladores habrían podido generar un mejor código para ello (evitando la necesidad de promociones enteras y de punto flotante, entre otras cosas).] Desafortunadamente, percibo cero posibilidades de que cualquier compilador agregue características para tener los compiladores informan los tipos de variables al código llamado.Me parece curioso que los punteros nulos se consideren el "error de mil millones de dólares", dada su utilidad, y dado que generalmente solo causan un comportamiento gravemente malo en idiomas que no atrapan la aritmética y los accesos de punteros nulos. Consideraría que el daño causado por las
printf
cadenas terminadas en cero es mucho peor.fuente
Piénselo como si estuviera pasando variables a otra función que haya definido. Normalmente le dice a la otra función qué tipo de datos debe esperar / recibir. De la misma manera con
printf()
. Ya está definido en lastdio.h
biblioteca y requiere que le diga qué datos está recibiendo para que pueda generarlos en el formato correcto (como en su casoint
).fuente