Por que precisamos informar ao printf () o tipo de dados em C?


8

Vamos considerar este código C:

#include <stdio.h>

main()
{
  int x=5;
  printf("x is ");
  printf("%d",5);
}

Nisso, quando escrevemos int x=5;, dissemos ao computador que xé um número inteiro. O computador deve se lembrar que xé um número inteiro. Mas quando produzimos o valor de xin printf(), precisamos dizer novamente ao computador que xé um número inteiro. Por que é que?

Por que o computador esquece que xera um número inteiro?


5
O compilador sabe que x é um número inteiro, mas printf não.
27914 luísubal

é assim que a convenção de chamada funciona, existe apenas uma implementação printf(char*, ...)e ela obtém (o que equivale a) um ponteiro para uma coleção de dados
catraca

@ratchetfreak Por que é assim? Por que printf não obtém o ponteiro do tipo de dados automaticamente como em C ++, onde a função "cout" conhece automaticamente o tipo de dados?
user106313

5
Você já considerou printf("x is %x in hex, and %d in decimal and %o as octal",x,x,x);?

1
Experimente com o número 163. Valores menores que 8 são bastante desinteressantes nessa situação. Ou melhor ainda, faça um loop de 0 a 255 e veja quais são os números. O ponto é que há mais em printf do que apenas eles digitam.

Respostas:


18

Há duas questões em jogo aqui:

Problema nº 1: C é uma linguagem de tipo estaticamente ; todas as informações de tipo são determinadas em tempo de compilação. Nenhuma informação de tipo é armazenada com nenhum objeto na memória, de modo que seu tipo e tamanho possam ser determinados no tempo de execução 1 . Se você examinar a memória em qualquer endereço específico enquanto o programa estiver em execução, tudo o que verá é uma lama de bytes; não há nada para dizer se esse endereço específico realmente contém um objeto, qual é o tipo ou tamanho desse objeto ou como interpretar esses bytes (como um número inteiro ou tipo de ponto flutuante ou sequência de caracteres em uma string etc.) ) Toda essa informação é inserida no código da máquina quando o código é compilado, com base nas informações de tipo especificadas no código-fonte; por exemplo, a definição da função

void foo( int x, double y, char *z )
{
  ...
}

informa ao compilador para gerar o código de máquina apropriado para manipular xcomo um número inteiro, ycomo um valor de ponto flutuante e zcomo um ponteiro para char. Observe que quaisquer incompatibilidades no número ou tipo de argumentos entre uma chamada de função e uma definição de função são detectadas apenas quando o código está sendo compilado 2 ; é somente durante a fase de compilação que qualquer informação de tipo é associada a um objeto.

Questão 2: printfé uma função variável ; é necessário um parâmetro fixo do tipo const char * restrict(a string de formato), juntamente com zero ou mais parâmetros adicionais, cujo número e tipo não são conhecidos no momento da compilação:

int printf( const char * restrict fmt, ... );

A printffunção não tem como saber qual é o número e os tipos de argumentos adicionais dos próprios argumentos passados; ele precisa confiar na string de formato para dizer como interpretar o lodo de bytes na pilha (ou nos registradores). Ainda melhor, por ser uma função variável, argumentos com certos tipos são promovidos para um conjunto limitado de tipos padrão (por exemplo, shorté promovido para int, floaté promovido para doubleetc.).

Novamente, não há informações associadas aos argumentos adicionais para fornecer printfpistas sobre como interpretá-los ou formatá-los. Daí a necessidade dos especificadores de conversão na string de formato.

Observe que, além de informar printfo número e o tipo de argumentos adicionais, os especificadores de conversão também informam printfcomo formatar a saída (larguras de campos, precisão, preenchimento, justificativa, base (decimal, octal ou hexadecimal para tipos inteiros) etc.).

Editar

Para evitar discussões extensas nos comentários (e como a página de bate-papo está bloqueada no meu sistema de trabalho - sim, eu estou sendo um garoto mau), vou abordar as duas últimas perguntas aqui.

Se eu fizer isso:
float b;          
float c;           
b=3.1;    
c=(5.0/9.0)*(b);
Na última declaração, como o compilador sabe que b é do tipo float?

Durante a tradução, o compilador mantém uma tabela (muitas vezes chamado de tabela de símbolos ), que armazena informações sobre nome, tipo, duração de armazenamento, o escopo de um objeto, etc. Você declarada b e ccomo float, então qualquer momento o compilador vê uma expressão com bou cna mesma, ele irá gerar o código da máquina para manipular um valor de ponto flutuante.

Peguei seu código acima e envolvi um programa completo em torno dele:

/**
 * 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;
}

Usei as opções -ge -Wa,-aldhcom o gcc para criar uma lista do código de máquina gerado intercalado com o código-fonte 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

Veja como ler a lista de montagem:

  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

Uma coisa a observar aqui. No código de montagem gerado, não há símbolos para bou c; eles existem apenas na lista de códigos-fonte. Quando mainexecutado em tempo de execução, o espaço para be c(juntamente com outras coisas) é alocado da pilha, ajustando o ponteiro da pilha:

subq    $16, %rsp

O código refere-se a esses objetos pelo deslocamento do ponteiro do quadro 4 , bsendo -8 bytes do endereço armazenado no ponteiro do quadro e c-4 bytes dele, da seguinte maneira:

   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 você declarou be ccomo flutuadores, o compilador gerou código de máquina para lidar especificamente com valores de ponto flutuante; o movsd, mulsd, cvtss2sdinstruções são todos específicos para operações de ponto flutuante, e os registros %xmm0e %xmm1são usados para armazenar dupla precisão valores de ponto flutuante.

Se eu alterar o código-fonte para que be csão inteiros, em vez de carros alegóricos, o compilador gera 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;
}

Compilando com gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.cdá:

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

Aqui está a mesma operação, mas com be cdeclarada como números inteiros:

   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

Isso foi o que eu quis dizer antes, quando disse que as informações de tipo eram "incorporadas" ao código da máquina. Quando o programa é executado, ele não examina bou cdetermina seu tipo; ele já sabe o seu tipo deve ser baseado no código de máquina gerado.

Se o compilador determina o tipo e tamanho no tempo de execução, por que o programa a seguir não funciona:
float b='H';         
printf(" value of b is %c \n",b);

Não funciona porque você está mentindo para o compilador. Você diz que bé um float, portanto, ele gerará código de máquina para lidar com valores de ponto flutuante. Quando você o inicializa, o padrão de bits correspondente à constante 'H'será interpretado como um valor de ponto flutuante, não um valor de caractere.

Você mente para o compilador novamente quando usa o %cespecificador de conversão, que espera um valor do tipo char, para o argumento b. Por isso, printfnão interpretará o conteúdo bcorretamente e você terminará com a saída de lixo 5 . Novamente, printfnão é possível saber o número ou os tipos de argumentos adicionais com base nos próprios argumentos; tudo o que vê é um endereço na pilha (ou vários registros). Ele precisa da string de formato para informar quais argumentos adicionais foram passados ​​e quais são seus tipos.


1. A única exceção são matrizes de comprimento variável; como o tamanho não é estabelecido até o tempo de execução, não há como avaliar sizeofum VLA em tempo de compilação.

2. A partir de C89, pelo menos. Antes disso, o compilador só podia detectar incompatibilidades no tipo de retorno da função; não foi possível detectar incompatibilidades nas listas de parâmetros de função.

3. Este código é gerado em um sistema SuSE Linux Enterprise 10 de 64 bits usando o gcc 4.1.2. Se você estiver em uma implementação diferente (arquitetura do compilador / OS / chip), as instruções exatas da máquina serão diferentes, mas o ponto geral ainda será válido; o compilador gerará instruções diferentes para lidar com flutuadores x ints x seqüências de caracteres etc.

4. Quando você chama uma função em um programa em execução, um quadro de pilhaé criado para armazenar os argumentos da função, variáveis ​​locais e o endereço da instrução após a chamada da função. Um registro especial chamado ponteiro do quadro é usado para acompanhar o quadro atual.

5. Por exemplo, assuma um sistema big endian em que o byte de alta ordem é o byte endereçado. O padrão de bits para Hserá armazenado em bcomo 0x00000048. No entanto, como o %cespecificador de conversão indica que o argumento deve ser a char, apenas o primeiro byte será lido, portanto printf, tentará escrever o caractere correspondente à codificação 0x00.


Se C é uma linguagem de tipo estaticamente, como putchar () imprime o tipo de dados correto sem mencionar o tipo?
user106313

@ user31782: A definição da putcharfunção diz que espera 1 argumento do tipo int; quando o compilador gera o código da máquina, ele assume que sempre recebe esse único argumento inteiro. Não há necessidade de especificar o tipo em tempo de execução.
John Bode

Eu posso imprimir alfabetos usando putchar ().
user106313

2
@ user31782: printfformata toda a sua saída como texto (ASCII ou não); o especificador de conversão informa como formatar a saída. printf( "%d\n", 65 );gravará a sequência de caracteres '6' e '5'a saída padrão, porque o %despecificador de conversão diz para formatar o argumento correspondente como um número inteiro decimal. printf( "%c\n", 65 );gravará o caractere 'A'na saída padrão, porque %cinforma printfpara formatar o argumento como um caractere do conjunto de caracteres de execução.
John Bode

1
@ user31782: Não sem uma alteração na definição de idioma. Claro, é possível (C ++ também é digitado estaticamente, mas é capaz de inferir tipos para o <<e >>operadores de E / S I), mas gostaria de acrescentar alguma complexidade para o idioma. Às vezes é difícil superar a inércia.
John Bode

8

Como no momento em que printfé chamado e faz seu trabalho, o compilador não está mais lá para dizer o que fazer.

A função não obtém nenhuma informação, exceto o que está em seus parâmetros, e os parâmetros vararg não têm nenhum tipo, portanto printf, não haveria idéia de como imprimi-los, se não obtivessem instruções explícitas por meio da string de formato. O compilador pode (normalmente) deduzir o tipo de argumento, mas você ainda precisará escrever uma sequência de formato para dizer onde imprimir cada argumento em relação ao texto constante. Compare "$%d"e "%d$"; eles fazem coisas diferentes, e o compilador não consegue adivinhar o que você deseja. Como você precisa compor uma string de formato manualmente de qualquer maneira para especificar posições de argumento , é uma escolha óbvia para descarregar a tarefa de declarar os tipos de argumento para o usuário também.

A alternativa seria o compilador varrer a string de formato em busca de posições, deduzir os tipos, reescrever a string de formato para adicionar as informações de tipo e compilar a string alterada no seu binário. Mas isso funcionaria apenas para strings de formato literal ; C também permite seqüências de caracteres de formato atribuídas dinamicamente e sempre haveria casos em que o compilador não pode reconstruir com precisão o que a sequência de caracteres de formato será no tempo de execução. (Além disso, às vezes você deseja imprimir algo como um tipo relacionado e diferente, realizando um elenco estreito; isso também é algo que nenhum compilador pode prever).


Portanto, a função printf () sabe que "x" é uma variável, mas não sabe qual é o seu tipo, mas o compilador sabe. Não podemos atualizar printf () para que ele possa saber o tipo de dados. Além disso, lembro que no C ++ "cout" pode imprimir dados conhecendo automaticamente seu tipo.
user106313

@ user31782 As chamadas de função C são extremamente simples. Tudo o que printf()é passado é um ponteiro para a string de formato e um ponteiro para o buffer, onde os argumentos podem ser encontrados. Nem mesmo o comprimento desse buffer é passado! Essa é uma das razões pelas quais C pode ser muito mais rápido que em outros idiomas. O que você está propondo é ordens de magnitude mais complexas.
grahamparks

@grahamparks É "cout" em C ++ mais lento que "printf". Criar printf () como "cout" tornaria mais lento?
user106313

3
@ user31782: não necessariamente mais lento no tempo de execução (depende, como de costume), mas requer recursos de linguagem que simplesmente não existem no C. C não possui sobrecarga de funções, muito menos os mecanismos de modelo usados coutno C ++.
Mat

5
Sim, mas você deve se lembrar que C é uma referência de 1972. Esses recursos foram inventados apenas muito mais tarde.
Kilian Foth

5

printf()é o que é conhecida como função variável, que aceita um número variável de argumentos.

Funções variáveis ​​em C usam um protótipo especial para informar ao compilador que a lista de argumentos é de tamanho desconhecido:

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

O padrão C fornece um conjunto de funções stdarg.hque podem ser usadas para recuperar os argumentos um de cada vez e convertê-los em um determinado tipo. Isso significa que funções variadas têm que decidir por si mesmas o tipo de cada argumento. printf()toma essa decisão com base no conteúdo da string de formato.

Esta é uma simplificação grosseira de como printf()realmente funciona, mas o processo é assim:

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);
}

O mesmo processo acontece para todos os tipos printf()capazes de converter. Você pode ver um exemplo disso no código fonte da implementação do OpenBSD vfprintf(), que é a função subjacente printf().

Alguns compiladores C são inteligentes o suficiente para localizar chamadas printf(), avaliar a sequência de formato se for uma constante e verificar se os tipos do restante dos argumentos são compatíveis com as conversões especificadas. Esse comportamento não é necessário, e é por isso que o padrão ainda exige o fornecimento do tipo como parte da cadeia de formato. Antes que esses tipos de verificação fossem feitos, as incompatibilidades entre a sequência de formatos e a lista de argumentos simplesmente produziam uma saída falsa.

Em C ++, <<é um operador, que faz uso de couttais como cout << foo << baruma expressão infix que podem ser avaliadas quanto à correção em tempo de compilação e se transformou em código que lança as expressões à direita em algo coutpode lidar com eles.


3

Os designers de C queriam tornar o compilador o mais simples possível. Embora fosse possível manipular E / S da mesma maneira que em outros idiomas, e exigir que o compilador forneça automaticamente à rotina de E / S informações sobre os tipos de parâmetros passados, e embora essa abordagem possa, em muitos casos, ter permitido código mais eficiente do que é possível com printf(*), definir coisas dessa maneira tornaria o compilador mais complicado.

Nos primeiros dias de C, o código que chamava de função não sabia nem se importava com os argumentos que esperava. Cada argumento colocaria um número de palavras na pilha de acordo com seu tipo, e as funções esperariam encontrar parâmetros diferentes no slot da pilha superior, de segunda a parte, etc. abaixo do endereço de retorno. Se um printfmétodo pudesse descobrir onde encontrar seus argumentos na pilha, não havia como o compilador tratá-lo de maneira diferente de qualquer outro método.

Na prática, o padrão de passagem de parâmetros previsto por C é muito raramente usado, exceto quando se chama funções variadas como printf, e se printftivesse sido definido como o uso de convenções especiais de passagem de parâmetros [por exemplo, ter o primeiro parâmetro sendo um compilador gerado const char*automaticamente informações sobre os tipos a serem transmitidos], os compiladores poderiam gerar um código melhor para ele (evitando a necessidade de promoções de números inteiros e de ponto flutuante, entre outras coisas).] Infelizmente, percebo a probabilidade zero de que qualquer compilador que adicione recursos tenha compiladores relatam tipos de variáveis ​​para o código chamado.

Acho curioso que ponteiros nulos sejam considerados o "erro de bilhões de dólares", dada sua utilidade, e dado que geralmente causam apenas comportamentos severamente ruins em linguagens que não prendem a aritmética e os acessos de ponteiros nulos. Eu consideraria printfmuito pior o dano causado por seqüências terminadas em zero.


0

Pense nisso como se estivesse passando variáveis ​​para outra função que você definiu. Você normalmente diz à outra função que tipo de dados ela deve esperar / receber. Da mesma maneira com printf(). Ele já está definido na stdio.hbiblioteca e requer que você informe quais dados está recebendo para que eles possam ser impressos no formato correto (como no seu caso int).

Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.