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 x
como um número inteiro, y
como um valor de ponto flutuante e z
como 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 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 para int
, float
é promovido para double
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 informam printf
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.
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 c
como float
, então qualquer momento o compilador vê uma expressão com b
ou c
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:
/**
* 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 -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 :
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 b
ou c
; eles existem apenas na lista de códigos-fonte. Quando main
executado em tempo de execução, o espaço para b
e 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 , b
sendo -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 b
e c
como flutuadores, o compilador gerou código de máquina para lidar especificamente com valores de ponto flutuante; o movsd
, 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
e c
sã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.c
dá:
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 b
e c
declarada 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 b
ou c
determina 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 %c
especificador de conversão, que espera um valor do tipo char
, para o argumento b
. Por isso, printf
não interpretará o conteúdo b
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 em b
como 0x00000048
. No entanto, como o %c
especificador 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
.