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
void foo( int x, double y, char *z )
{
...
}
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 y z
como un puntero a char
. 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 tipo const 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:
int printf( const char * restrict fmt, ... );
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 a int
, float
se promueve a double
, 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 indican printf
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í.
SI hago esto: float b;
float c;
b=3.1;
c=(5.0/9.0)*(b);
En la última declaración, ¿cómo sabe el compilador que b es de tipo flotante?
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
y c
como float
, por lo que cada vez que el compilador ve una expresión con b
o c
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:
/**
* c1.c
*/
#include <stdio.h>
int main( void )
{
float b;
float c;
b = 3.1;
c = (5.0 / 9.0) * b;
printf( "c = %f\n", c );
return 0;
}
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 :
GAS LISTING /tmp/ccmGgGG2.s page 1
1 .file "c1.c"
9 .Ltext0:
10 .section .rodata
11 .LC2:
12 0000 63203D20 .string "c = %f\n"
12 25660A00
13 .align 8
14 .LC1:
15 0008 721CC771 .long 1908874354
16 000c 1CC7E13F .long 1071761180
17 .text
18 .globl main
20 main:
21 .LFB2:
22 .file 1 "c1.c"
1:c1.c **** #include <stdio.h>
2:c1.c **** int main( void )
3:c1.c **** {
23 .loc 1 3 0
24 0000 55 pushq %rbp
25 .LCFI0:
26 0001 4889E5 movq %rsp, %rbp
27 .LCFI1:
28 0004 4883EC10 subq $16, %rsp
29 .LCFI2:
4:c1.c **** float b;
5:c1.c **** float c;
6:c1.c **** b = 3.1;
30 .loc 1 6 0
31 0008 B8666646 movl $0x40466666, %eax
31 40
32 000d 8945F8 movl %eax, -8(%rbp)
7:c1.c **** c = (5.0 / 9.0) * b;
33 .loc 1 7 0
34 0010 F30F5A4D cvtss2sd -8(%rbp), %xmm1
34 F8
35 0015 F20F1005 movsd .LC1(%rip), %xmm0
35 00000000
36 001d F20F59C1 mulsd %xmm1, %xmm0
37 0021 F20F5AC0 cvtsd2ss %xmm0, %xmm0
38 0025 F30F1145 movss %xmm0, -4(%rbp)
38 FC
8:c1.c ****
9:c1.c **** printf( "c = %f\n", c );
39 .loc 1 9 0
40 002a F30F5A45 cvtss2sd -4(%rbp), %xmm0
40 FC
41 002f BF000000 movl $.LC2, %edi
41 00
42 0034 B8010000 movl $1, %eax
42 00
43 0039 E8000000 call printf
43 00
10:c1.c **** return 0;
44 .loc 1 10 0
45 003e B8000000 movl $0, %eax
GAS LISTING /tmp/ccmGgGG2.s page 2
11:c1.c **** }
46 .loc 1 11 0
47 0043 C9 leave
48 0044 C3 ret
Aquí le mostramos cómo leer la lista de ensamblaje:
40 002a F30F5A45 cvtss2sd -4(%rbp), %xmm0
40 FC
^ ^ ^ ^ ^
| | | | |
| | | | +-- Instruction operands
| | | +------------------ Instruction mnemonic
| | +---------------------------------------- Actual machine code (instruction and operands)
| +--------------------------------------------- Byte offset of instruction from subroutine entry point
+------------------------------------------------ Line number of assembly listing
Una cosa a tener en cuenta aquí. En el código de ensamblaje generado, no hay símbolos para b
o c
; solo existen en el listado del código fuente. Cuando se main
ejecuta en tiempo de ejecución, el espacio para b
y c
(junto con algunas otras cosas) se asigna desde la pila ajustando el puntero de la pila:
subq $16, %rsp
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 y c
siendo -4 bytes de él, de la siguiente manera:
7:c1.c **** c = (5.0 / 9.0) * b;
.loc 1 7 0
cvtss2sd -8(%rbp), %xmm1 ;; converts contents of b from single- to double-
;; precision float, stores result to floating-
;; point register xmm1
movsd .LC1(%rip), %xmm0 ;; writes the pre-computed value of 5.0/9.0
;; to floating point register xmm0
mulsd %xmm1, %xmm0 ;; multiply contents of xmm1 by xmm0, store result
;; in xmm0
cvtsd2ss %xmm0, %xmm0 ;; convert result in xmm0 from double- to single-
;; precision float
movss %xmm0, -4(%rbp) ;; save result to c
Desde que declaró b
y c
como flotantes, el compilador generó código de máquina para manejar específicamente los valores de punto flotante; el movsd
, 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
y c
son números enteros en lugar de los flotadores, el compilador genera código de máquina diferente:
/**
* c2.c
*/
#include <stdio.h>
int main( void )
{
int b;
int c;
b = 3;
c = (9 / 4) * b; // changed these values since integer 5/9 == 0, making for
// some really boring machine code.
printf( "c = %d\n", c );
return 0;
}
Compilar con gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c
da:
GAS LISTING /tmp/ccyxHwid.s page 1
1 .file "c2.c"
9 .Ltext0:
10 .section .rodata
11 .LC0:
12 0000 63203D20 .string "c = %d\n"
12 25640A00
13 .text
14 .globl main
16 main:
17 .LFB2:
18 .file 1 "c2.c"
1:c2.c **** #include <stdio.h>
2:c2.c **** int main( void )
3:c2.c **** {
19 .loc 1 3 0
20 0000 55 pushq %rbp
21 .LCFI0:
22 0001 4889E5 movq %rsp, %rbp
23 .LCFI1:
24 0004 4883EC10 subq $16, %rsp
25 .LCFI2:
4:c2.c **** int b;
5:c2.c **** int c;
6:c2.c **** b = 3;
26 .loc 1 6 0
27 0008 C745F803 movl $3, -8(%rbp)
27 000000
7:c2.c **** c = (9 / 4) * b;
28 .loc 1 7 0
29 000f 8B45F8 movl -8(%rbp), %eax
30 0012 01C0 addl %eax, %eax
31 0014 8945FC movl %eax, -4(%rbp)
8:c2.c ****
9:c2.c **** printf( "c = %d\n", c );
32 .loc 1 9 0
33 0017 8B75FC movl -4(%rbp), %esi
34 001a BF000000 movl $.LC0, %edi
34 00
35 001f B8000000 movl $0, %eax
35 00
36 0024 E8000000 call printf
36 00
10:c2.c **** return 0;
37 .loc 1 10 0
38 0029 B8000000 movl $0, %eax
38 00
11:c2.c **** }
39 .loc 1 11 0
40 002e C9 leave
41 002f C3 ret
Aquí está la misma operación, pero con b
y c
declarado como enteros:
7:c2.c **** c = (9 / 4) * b;
.loc 1 7 0
movl -8(%rbp), %eax ;; copy value of b to register eax
addl %eax, %eax ;; since 9/4 == 2 (integer arithmetic), double the
;; value in eax
movl %eax, -4(%rbp) ;; write result to c
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
ni c
determina su tipo; ya sabe cuál debería ser su tipo en función del código máquina generado.
Si el compilador determina el tipo y el tamaño en tiempo de ejecución, ¿por qué no funciona el siguiente programa?
float b='H';
printf(" value of b is %c \n",b);
No funciona porque le estás mintiendo al compilador. Le dice que b
es un float
, 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 tipo char
, para el argumento b
. Debido a esto, printf
no interpretará el contenido b
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
como 0x00000048
. Sin embargo, debido a que el %c
especificador de conversión indica que el argumento debe ser a char
, solo se leerá el primer byte, por printf
lo que intentará escribir el carácter correspondiente a la codificación 0x00
.