A declaração
printf("%f\n",0.0f);
imprime 0.
No entanto, a declaração
printf("%f\n",0);
imprime valores aleatórios.
Percebo que estou exibindo algum tipo de comportamento indefinido, mas não consigo descobrir o porquê especificamente.
Um valor de ponto flutuante em que todos os bits são 0 ainda é válido float
com valor de 0.
float
e int
têm o mesmo tamanho na minha máquina (se isso for relevante).
Por que usar um literal inteiro em vez de um literal de ponto flutuante printf
causa esse comportamento?
PS o mesmo comportamento pode ser visto se eu usar
int i = 0;
printf("%f\n", i);
c++
c
printf
implicit-conversion
undefined-behavior
Trevor Hickey
fonte
fonte
printf
está esperando umdouble
, e você está dando umint
.float
eint
pode ter o mesmo tamanho em sua máquina, mas0.0f
na verdade é convertido em umdouble
quando colocado em uma lista de argumentos variável (eprintf
espera isso). Resumindo, você não está cumprindo sua parte da barganha comprintf
base nos especificadores que está usando e nos argumentos que está fornecendo.(uint64_t)0
vez de0
e veja se você ainda obter o comportamento aleatório (assumindodouble
euint64_t
têm o mesmo tamanho e alinhamento). Provavelmente, a saída ainda será aleatória em algumas plataformas (por exemplo, x86_64) devido a tipos diferentes sendo passados em registros diferentes.Respostas:
O
"%f"
formato requer um argumento do tipodouble
. Você está dando a ele um argumento do tipoint
. É por isso que o comportamento é indefinido.O padrão não garante que todos os bits zero sejam uma representação válida de
0.0
(embora muitas vezes seja), ou de qualquerdouble
valor, ou queint
edouble
sejam do mesmo tamanho (lembre-se de quedouble
não éfloat
), ou, mesmo que sejam iguais tamanho, que eles são passados como argumentos para uma função variável da mesma maneira.Pode acontecer de "funcionar" em seu sistema. Esse é o pior sintoma possível de comportamento indefinido, porque torna difícil diagnosticar o erro.
N1570 7.21.6.1 parágrafo 9:
Argumentos do tipo
float
são promovidos adouble
, e é por isso queprintf("%f\n",0.0f)
funciona. Argumentos de tipos inteiros mais estreitos do queint
os promovidos paraint
ou paraunsigned int
. Estas regras de promoção (especificadas por N1570 6.5.2.2 parágrafo 6) não ajudam no caso deprintf("%f\n", 0)
.Observe que se você passar uma constante
0
para uma função não variada que espera umdouble
argumento, o comportamento é bem definido, assumindo que o protótipo da função esteja visível. Por exemplo,sqrt(0)
(after#include <math.h>
) converte implicitamente o argumento0
deint
paradouble
- porque o compilador pode ver pela declaração desqrt
que ele espera umdouble
argumento. Não tem essa informação paraprintf
. Funções variáveis comoprintf
são especiais e requerem mais cuidado ao escrever chamadas para elas.fonte
double
nãofloat
tão largura suposição do OP não pode (provavelmente não) espera. Em segundo lugar, a suposição de que zero inteiro e zero de ponto flutuante têm o mesmo padrão de bits também não é válida. Bom trabalhofloat
é promovido adouble
sem explicar o porquê, mas esse não era o ponto principal.printf
, embora o gcc, por exemplo, tenha alguns para que possa diagnosticar erros ( se a string de formato for literal). O compilador pode ver a declaração deprintf
de<stdio.h>
, que informa que o primeiro parâmetro é aconst char*
e os demais são indicados por, ...
. Não,%f
é paradouble
(efloat
é promovido paradouble
) e%lf
é paralong double
. O padrão C não diz nada sobre uma pilha. Ele especifica o comportamento deprintf
apenas quando é chamado corretamente.float
passado paraprintf
é promovido paradouble
; não há nada de mágico nisso, é apenas uma regra de linguagem para chamar funções variadas.printf
ele próprio sabe por meio da string de formato o que o chamador afirmou ter passado para ele; se essa afirmação estiver incorreta, o comportamento é indefinido.l
modificador de comprimento "não tem efeito sobre a seguintea
,A
,e
,E
,f
,F
,g
, ouG
especificador de conversão", o modificador de comprimento para umalong double
conversão éL
. (@ robertbristow-johnson também pode estar interessado)Primeiro, como tocou em várias outras respostas, mas não, na minha opinião, enunciado de forma suficientemente clara: Ele faz o trabalho para fornecer um número inteiro na maioria dos contextos em que uma função de biblioteca tem um
double
oufloat
argumento. O compilador irá inserir automaticamente uma conversão. Por exemplo,sqrt(0)
é bem definido e se comportará exatamente comosqrt((double)0)
, e o mesmo é verdadeiro para qualquer outra expressão do tipo inteiro usada lá.printf
é diferente. É diferente porque leva um número variável de argumentos. Seu protótipo de função éPortanto, quando você escreve
o compilador não tem nenhuma informação sobre que tipo
printf
espera que o segundo argumento seja. Ele tem apenas o tipo da expressão do argumento, ou sejaint
, para seguir. Portanto, ao contrário da maioria das funções de biblioteca, cabe a você, o programador, garantir que a lista de argumentos corresponda às expectativas da string de formato.(Compiladores modernos podem olhar para uma string de formato e dizer que você tem uma incompatibilidade de tipo, mas eles não vão começar a inserir conversões para realizar o que você pretendia, porque melhor seu código deve quebrar agora, quando você perceber , do que anos depois, quando reconstruído com um compilador menos útil.)
Agora, a outra metade da pergunta era: Dado que (int) 0 e (float) 0,0 são, na maioria dos sistemas modernos, ambos representados como 32 bits, todos sendo zero, por que não funciona mesmo assim, por acidente? O padrão C diz apenas "isso não é necessário para funcionar, você está sozinho", mas deixe-me explicar os dois motivos mais comuns pelos quais não funcionaria; isso provavelmente ajudará você a entender por que não é obrigatório.
Primeiro, por razões históricas, quando você passa um
float
por uma lista de argumentos de variável, ele é promovido paradouble
, que, na maioria dos sistemas modernos, tem 64 bits de largura. Portanto,printf("%f", 0)
passa apenas 32 bits zero para um receptor que espera 64 deles.A segunda razão, igualmente significativa, é que os argumentos da função de ponto flutuante podem ser passados em um lugar diferente dos argumentos inteiros. Por exemplo, a maioria das CPUs tem arquivos de registro separados para valores inteiros e de ponto flutuante, então pode ser uma regra que os argumentos de 0 a 4 vão nos registros r0 a r4 se forem inteiros, mas f0 a f4 se forem de ponto flutuante. Portanto,
printf("%f", 0)
procura no registro f1 por esse zero, mas ele não está lá.fonte
()
.AL
. (Sim, isso significa que a implementação dova_arg
é muito mais complicada do que costumava ser.)ret n
instrução do 8086, onden
era um inteiro embutido em código, que, portanto, não era aplicável para funções variáveis. No entanto, não sei se algum compilador C realmente tirou vantagem disso (compiladores não-C certamente tiraram).Normalmente, quando você chama uma função que espera um
double
, mas você fornece umint
, o compilador será automaticamente convertido em adouble
para você. Isso não acontece com oprintf
, porque os tipos dos argumentos não são especificados no protótipo da função - o compilador não sabe que uma conversão deve ser aplicada.fonte
printf()
em particular, é projetado para que seus argumentos possam ser de qualquer tipo. Você deve saber qual tipo é esperado por cada elemento na string de formato e deve fornecê-lo corretamente.Porque
printf()
não tem parâmetros digitados além doconst char* formatstring
do primeiro. Ele usa reticências (...
) no estilo c para todo o resto.Ele apenas decide como interpretar os valores passados de acordo com os tipos de formatação fornecidos na string de formato.
Você teria o mesmo tipo de comportamento indefinido de quando tenta
fonte
printf
podem funcionar dessa maneira (exceto que os itens passados são valores, não endereços). O padrão C não especifica comoprintf
e outras funções variáveis funcionam, apenas especifica seu comportamento. Em particular, não há menção de frames de pilha.printf
tem um parâmetro digitado, a string de formato, que é do tipoconst char*
. BTW, a questão está marcada com C e C ++, e C é realmente mais relevante; Eu provavelmente não teria usadoreinterpret_cast
como exemplo.Usar um
printf()
especificador"%f"
e tipo(int) 0
incompatíveis leva a um comportamento indefinido.Causas candidatas de UB.
É UB por especificação e a compilação é complicada - disse nuf.
double
eint
são de tamanhos diferentes.double
eint
pode passar seus valores usando diferentes pilhas (geral vs. pilha FPU ).A
double 0.0
pode não ser definido por um padrão de bits totalmente zero. (raro)fonte
Esta é uma daquelas grandes oportunidades de aprender com os avisos do compilador.
ou
Então,
printf
está produzindo um comportamento indefinido porque você está passando um tipo de argumento incompatível.fonte
Não tenho certeza do que é confuso.
Sua string de formato espera um
double
; em vez disso, você fornece umint
.Se os dois tipos têm a mesma largura de bits é totalmente irrelevante, exceto que pode ajudá-lo a evitar a obtenção de exceções de violação de memória física de código quebrado como este.
fonte
int
seria aceitável aqui.int
qualificado como um padrão flutuante válido? O complemento de dois e várias codificações de ponto flutuante não têm quase nada em comum.0
para um argumento digitadodouble
fará a coisa certa. Não é óbvio para um iniciante que o compilador não faz a mesma conversão paraprintf
slots de argumento endereçados por%[efg]
."%f\n"
garante resultado previsível apenas quando o segundoprintf()
parâmetro tem tipo dedouble
. Em seguida, argumentos extras de funções variáveis estão sujeitos à promoção de argumento padrão. Argumentos inteiros se enquadram na promoção de inteiros, que nunca resulta em valores digitados de ponto flutuante. E osfloat
parâmetros são promovidos paradouble
.Para completar: standard permite que o segundo argumento seja ou
float
oudouble
e nada mais.fonte
Por que é formalmente UB já foi discutido em várias respostas.
O motivo pelo qual você obtém especificamente esse comportamento depende da plataforma, mas provavelmente é o seguinte:
printf
espera seus argumentos de acordo com a propagação padrão de vararg. Isso significa que umfloat
será umdouble
e qualquer coisa menor que umint
será umint
.int
onde a função espera umdouble
. O seuint
é provavelmente de 32 bits, o seudouble
64 bit. Isso significa que os quatro bytes da pilha começando no lugar onde o argumento deveria estar0
, mas os quatro bytes seguintes têm conteúdo arbitrário. Isso é o que é usado para construir o valor que é exibido.fonte
A principal causa deste problema de "valor indeterminado" está no elenco do ponteiro no
int
valor passado para aprintf
seção de parâmetros da variável para um ponteiro nosdouble
tipos queva_arg
macro executa.Isso faz com que uma referência a uma área de memória que não foi completamente inicializada com o valor passado como parâmetro para o printf, porque o
double
tamanho da área do buffer de memória é maior queint
tamanho.Portanto, quando este ponteiro é desreferenciado, ele retorna um valor indeterminado, ou melhor, um "valor" que contém em parte o valor passado como parâmetro para
printf
, e o restante pode vir de outra área de buffer de pilha ou mesmo de uma área de código ( levantando uma exceção de falha de memória), um estouro de buffer real .Ele pode considerar essas partes específicas de implementações de código semplificado de "printf" e "va_arg" ...
printf
va_arg
fonte