¿Por qué tenemos que decirle a printf () el tipo de datos en C?

8

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 xes un número entero. La computadora debe recordar que xes un número entero. Pero cuando sacamos el valor de xin printf()tenemos que decirle nuevamente a la computadora que xes un número entero. ¿Porqué es eso?

¿Por qué la computadora olvida que xera un número entero?

usuario106313
fuente
55
El compilador sabe que x es un número entero, pero printf no.
luiscubal
es sólo cómo funciona la convención de llamada, sólo hay 1 implementación de printf(char*, ...)y que sólo se pone (lo que equivale a) un puntero a una colección de datos
de trinquete monstruo
@ratchetfreak ¿Por qué es así? ¿Por qué printf no obtiene el puntero del tipo de datos automáticamente como en C ++ donde la función "cout" conoce automáticamente el tipo de datos?
user106313
55
¿Lo has considerado printf("x is %x in hex, and %d in decimal and %o as octal",x,x,x);?
1
Pruébelo con el número 163. Los valores inferiores a 8 no son interesantes en esta situación. O mejor aún, pase de 0 a 255 y vea cuáles son los números. El punto es que hay más en printf que solo escribir.

Respuestas:

18

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 xcomo un entero, ycomo un valor de punto flotante y zcomo 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: printfes 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 printffunció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, shortse promueve a int, floatse promueve a double, etc.).

Nuevamente, no hay información asociada con los argumentos adicionales en sí mismos para dar printfpistas 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 printfel número y el tipo de argumentos adicionales, los especificadores de conversión también indican printfcó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 ccomo float, por lo que cada vez que el compilador ve una expresión con bo cen 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 -gy -Wa,-aldhcon 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 bo c; solo existen en el listado del código fuente. Cuando se mainejecuta en tiempo de ejecución, el espacio para by 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 , bsiendo -8 bytes de la dirección almacenada en el puntero de cuadro y csiendo -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ó by ccomo flotantes, el compilador generó código de máquina para manejar específicamente los valores de punto flotante; el movsd, mulsd, cvtss2sdlas instrucciones son todas específicas para operaciones de punto flotante, y los registros %xmm0y %xmm1se utilizan para almacenar los valores de doble precisión en coma flotante.

Si cambio el código fuente para que by cson 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.cda:

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 by cdeclarado 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 bni cdetermina 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 bes 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 %cespecificador de conversión, que espera un valor de tipo char, para el argumento b. Debido a esto, printfno interpretará el contenido bcorrectamente, y terminará con la salida de basura 5 . Nuevamente, printfno 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 sizeofun 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 Hse almacenará bcomo 0x00000048. Sin embargo, debido a que el %cespecificador de conversión indica que el argumento debe ser a char, solo se leerá el primer byte, por printflo que intentará escribir el carácter correspondiente a la codificación 0x00.

John Bode
fuente
Si C es un lenguaje de tipo estático, ¿cómo imprime putchar () el tipo de datos correcto sin mencionar el tipo?
usuario106313
@ user31782: La definición de la putcharfunción dice que espera 1 argumento de tipo int; 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.
John Bode
Puedo imprimir alfabetos usando putchar ().
user106313
2
@ user31782: printfformatea 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 %despecificador 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 %cle dice printfque formatee el argumento como un carácter del conjunto de caracteres de ejecución.
John Bode
1
@ user31782: no sin un cambio en la definición del idioma. Claro, es posible (C ++ también se escribe de forma estática, sino que es capaz de inferir los tipos para el <<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.
John Bode
8

Porque en el momento en que printfse 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 printflo 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).

Kilian Foth
fuente
Entonces, la función printf () sabe que "x" es una variable pero no sabe cuál es su tipo, pero el compilador lo sabe. ¿No podemos actualizar printf () para que pueda conocer el tipo de datos. Además, recuerdo que en C ++ "cout" puede imprimir datos al conocer automáticamente su tipo.
user106313
@ user31782 Las llamadas a la función C son extremadamente simples. Todo lo que 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.
Grahamparks
@grahamparks es "cout" en C ++ más lento que "printf". ¿Hacer printf () como "cout" lo haría lento?
user106313
3
@ user31782: no necesariamente es más lento en tiempo de ejecución (depende, como de costumbre), pero requiere características de lenguaje que simplemente no existen en C. C no tiene sobrecarga de funciones, y mucho menos los mecanismos de plantilla utilizados couten C ++.
Mat
55
Sí, pero debe recordar que C se remonta a 1972. Estas características se inventaron mucho más tarde.
Kilian Foth
5

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:

int printf(const char *format, ...);

El estándar C proporciona un conjunto de funciones stdarg.hque 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:

int printf(const char *format, ...) {

    /* Get ready to process arguments that follow 'format' */
    va_list ap;
    va_start(ap, format);

    /* Deep in the function, something that's dissected the
       format string has decided that the next argument is a
       string.  Grab the next argument, cast it to char * and
       write it to wherever it should go.
     */
    char *string = va_arg(ap, char *);
    write_string_to_output(string);

    /* Conclude processing of arguments */
    va_end(ap);
}

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 OpenBSD vfprintf(), que es la función que sustenta printf().

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 de cout, por ejemplo, cout << foo << baruna 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 que coutpuede lidiar.

Blrfl
fuente
3

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 printfmé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 si printfse ha definido como el uso de convenciones especiales de paso de parámetros [por ejemplo, tener el primer parámetro como un compilador generado que const 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 printfcadenas terminadas en cero es mucho peor.

Super gato
fuente
0

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 la stdio.hbiblioteca y requiere que le diga qué datos está recibiendo para que pueda generarlos en el formato correcto (como en su caso int).

Hammerton Mwawuda
fuente