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 x
in printf()
, precisamos dizer novamente ao computador que x
é um número inteiro. Por que é que?
Por que o computador esquece que x
era um número inteiro?
c
io
type-safety
user106313
fonte
fonte
printf(char*, ...)
e ela obtém (o que equivale a) um ponteiro para uma coleção de dadosprintf("x is %x in hex, and %d in decimal and %o as octal",x,x,x);
?Respostas:
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
informa ao compilador para gerar o código de máquina apropriado para manipular
x
como um número inteiro,y
como um valor de ponto flutuante ez
como um ponteiro parachar
. 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 tipoconst 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:A
printf
funçã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 paraint
,float
é promovido paradouble
etc.).Novamente, não há informações associadas aos argumentos adicionais para fornecer
printf
pistas 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
printf
o número e o tipo de argumentos adicionais, os especificadores de conversão também informamprintf
como 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.
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
ec
comofloat
, então qualquer momento o compilador vê uma expressão comb
ouc
na 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:
Usei as opções
-g
e-Wa,-aldh
com o gcc para criar uma lista do código de máquina gerado intercalado com o código-fonte C 3 :Veja como ler a lista de montagem:
Uma coisa a observar aqui. No código de montagem gerado, não há símbolos para
b
ouc
; eles existem apenas na lista de códigos-fonte. Quandomain
executado em tempo de execução, o espaço parab
ec
(juntamente com outras coisas) é alocado da pilha, ajustando o ponteiro da pilha:O código refere-se a esses objetos pelo deslocamento do ponteiro do quadro 4 ,
b
sendo -8 bytes do endereço armazenado no ponteiro do quadro ec
-4 bytes dele, da seguinte maneira:Desde que você declarou
b
ec
como flutuadores, o compilador gerou código de máquina para lidar especificamente com valores de ponto flutuante; omovsd
,mulsd
,cvtss2sd
instruções são todos específicos para operações de ponto flutuante, e os registros%xmm0
e%xmm1
são usados para armazenar dupla precisão valores de ponto flutuante.Se eu alterar o código-fonte para que
b
ec
são inteiros, em vez de carros alegóricos, o compilador gera código de máquina diferente:Compilando com
gcc -o c2 -g -std=c99 -pedantic -Wall -Werror -Wa,-aldh=c2.lst c2.c
dá:Aqui está a mesma operação, mas com
b
ec
declarada como números inteiros: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
b
ouc
determina seu tipo; ele já sabe o seu tipo deve ser baseado no código de máquina gerado.Não funciona porque você está mentindo para o compilador. Você diz que
b
é umfloat
, 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
%c
especificador de conversão, que espera um valor do tipochar
, para o argumentob
. Por isso,printf
não interpretará o conteúdob
corretamente e você terminará com a saída de lixo 5 . Novamente,printf
nã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
sizeof
um 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
H
será armazenado emb
como0x00000048
. No entanto, como o%c
especificador de conversão indica que o argumento deve ser achar
, apenas o primeiro byte será lido, portantoprintf
, tentará escrever o caractere correspondente à codificação0x00
.fonte
putchar
função diz que espera 1 argumento do tipoint
; 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.printf
formata 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%d
especificador 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%c
informaprintf
para formatar o argumento como um caractere do conjunto de caracteres de execução.<<
e>>
operadores de E / S I), mas gostaria de acrescentar alguma complexidade para o idioma. Às vezes é difícil superar a inércia.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).
fonte
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.cout
no C ++.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:
O padrão C fornece um conjunto de funções
stdarg.h
que 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: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 OpenBSDvfprintf()
, que é a função subjacenteprintf()
.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 decout
tais comocout << foo << bar
uma 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 algocout
pode lidar com eles.fonte
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
printf
mé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 seprintf
tivesse sido definido como o uso de convenções especiais de passagem de parâmetros [por exemplo, ter o primeiro parâmetro sendo um compilador geradoconst 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
printf
muito pior o dano causado por seqüências terminadas em zero.fonte
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 nastdio.h
biblioteca e requer que você informe quais dados está recebendo para que eles possam ser impressos no formato correto (como no seu casoint
).fonte