Considere o seguinte código "C":
#include<stdio.h>
main()
{
printf("func:%d",Func_i());
}
Func_i()
{
int i=3;
return i;
}
Func_i()
é definido no final do código-fonte e nenhuma declaração é fornecida antes do uso main()
. No momento em que o compilador vê Func_i()
em main()
, ele sai do main()
e descobre Func_i()
. O compilador, de alguma forma, encontra o valor retornado por Func_i()
e o entrega printf()
. Eu também sei que o compilador não pode encontrar o tipo de retorno de Func_i()
. Por padrão, assume (adivinha?) O tipo de retorno de Func_i()
ser int
. Ou seja, se o código tivesse float Func_i()
, o compilador apresentaria o erro: Tipos conflitantes paraFunc_i()
.
A partir da discussão acima, vemos que:
O compilador pode encontrar o valor retornado por
Func_i()
.- Se o compilador pode encontrar o valor retornado
Func_i()
saindo domain()
e pesquisando o código-fonte, por que não consegue encontrar o tipo de Func_i (), mencionado explicitamente .
- Se o compilador pode encontrar o valor retornado
O compilador deve saber que
Func_i()
é do tipo float - é por isso que gera o erro de tipos conflitantes.
- Se o compilador sabe que
Func_i
é do tipo float, então por que ele ainda supõeFunc_i()
ser do tipo int e gera o erro de tipos conflitantes? Por que não faz com queFunc_i()
seja do tipo float?
Eu tenho a mesma dúvida com a declaração da variável . Considere o seguinte código "C":
#include<stdio.h>
main()
{
/* [extern int Data_i;]--omitted the declaration */
printf("func:%d and Var:%d",Func_i(),Data_i);
}
Func_i()
{
int i=3;
return i;
}
int Data_i=4;
O compilador fornece o erro: 'Data_i' não declarado (primeiro uso nesta função).
- Quando o compilador vê
Func_i()
, ele desce para o código-fonte para encontrar o valor retornado por Func_ (). Por que o compilador não pode fazer o mesmo para a variável Data_i?
Editar:
Não conheço os detalhes do trabalho interno do compilador, montador, processador etc. A idéia básica da minha pergunta é que, se eu contar (escrever) o valor de retorno da função no código-fonte, finalmente, após o uso dessa função, o idioma "C" permite que o computador encontre esse valor sem dar nenhum erro. Agora, por que o computador não consegue encontrar o tipo da mesma forma. Por que o tipo de Data_i não pode ser encontrado, pois o valor de retorno de Func_i () foi encontrado. Mesmo se eu usar a extern data-type identifier;
instrução, não estou dizendo o valor a ser retornado por esse identificador (função / variável). Se o computador consegue encontrar esse valor, por que não consegue encontrar o tipo? Por que precisamos da declaração direta?
Obrigado.
fonte
Func_i
inválido. Nunca houve uma regra para declarar implicitamente variáveis indefinidas; portanto, o segundo fragmento sempre estava mal formado. (Sim, compiladores não aceitar a primeira amostra ainda porque era válida, se desleixado, sob C89 / C90.)Respostas:
Porque C é uma única passagem , estaticamente digitado , fracamente tipado , compilado linguagem.
Passagem única significa que o compilador não olha para a frente para ver a definição de uma função ou variável. Como o compilador não olha para o futuro, a declaração de uma função deve vir antes do uso da função, caso contrário, o compilador não sabe qual é a sua assinatura de tipo. No entanto, a definição da função pode ser posterior no mesmo arquivo ou até mesmo em um arquivo diferente. Veja o ponto 4.
A única exceção é o artefato histórico que funções e variáveis não declaradas são do tipo "int". A prática moderna é evitar a digitação implícita, sempre declarando funções e variáveis explicitamente.
Tipo estaticamente significa que todas as informações de tipo são computadas em tempo de compilação. Essas informações são usadas para gerar código de máquina que é executado em tempo de execução. Não há conceito em C de digitação em tempo de execução. Uma vez que um int, sempre um int, uma vez um float, sempre um float. No entanto, esse fato é um pouco obscurecido pelo próximo ponto.
Tipo fraco significa que o compilador C gera automaticamente código para converter entre tipos numéricos sem exigir que o programador especifique explicitamente as operações de conversão. Por causa da digitação estática, a mesma conversão sempre será realizada da mesma maneira sempre no programa. Se um valor flutuante é convertido em um valor int em um determinado ponto no código, um valor flutuante sempre será convertido em um valor int nesse ponto no código. Isso não pode ser alterado no tempo de execução. O valor em si pode mudar de uma execução do programa para a seguinte, é claro, e as instruções condicionais podem alterar quais seções do código são executadas em que ordem, mas uma única seção do código sem chamadas de função ou condicionais sempre executará exatamente o mesmas operações sempre que for executada.
Compilado significa que o processo de analisar o código-fonte legível por humanos e transformá-lo em instruções legíveis por máquina é totalmente realizado antes da execução do programa. Quando o compilador está compilando uma função, ele não tem conhecimento do que encontrará mais adiante em um determinado arquivo de origem. No entanto, depois que a compilação (e montagem, vinculação, etc.) for concluída, cada função no executável finalizado conterá ponteiros numéricos para as funções que ele chamará quando for executado. É por isso que main () pode chamar uma função mais abaixo no arquivo de origem. Quando o main () for executado, ele conterá um ponteiro para o endereço de Func_i ().
O código da máquina é muito, muito específico. O código para adicionar dois números inteiros (3 + 2) é diferente do código para adicionar dois números flutuantes (3.0 + 2.0). Eles são diferentes de adicionar um int a um float (3 + 2.0) e assim por diante. O compilador determina para cada ponto de uma função qual operação exata precisa ser executada nesse ponto e gera código que executa essa operação exata. Feito isso, ele não pode ser alterado sem recompilar a função.
Juntando todos esses conceitos, a razão pela qual main () não pode "ver" mais abaixo para determinar o tipo de Func_i () é que a análise de tipo ocorre no início do processo de compilação. Nesse ponto, apenas a parte do arquivo de origem até a definição de main () foi lida e analisada, e a definição de Func_i () ainda não é conhecida pelo compilador.
O motivo pelo qual main () pode "ver" onde Func_i () deve chamá- lo é que a chamada acontece no tempo de execução, depois que a compilação já resolveu todos os nomes e tipos de todos os identificadores, o assembly já converteu todos os funções ao código da máquina e a vinculação já inseriu o endereço correto de cada função em cada local em que é chamada.
Evidentemente, deixei de fora a maioria dos detalhes sangrentos. O processo real é muito, muito mais complicado. Espero ter fornecido uma visão geral de alto nível o suficiente para responder às suas perguntas.
Além disso, lembre-se de que o que escrevi acima se aplica especificamente a C.
Em outros idiomas, o compilador pode fazer várias passagens pelo código-fonte e, assim, o compilador pode pegar a definição de Func_i () sem que seja pré-declarado.
Em outros idiomas, funções e / ou variáveis podem ser digitadas dinamicamente, para que uma única variável possa reter ou uma única função possa ser passada ou retornada, um número inteiro, um número flutuante, uma sequência, uma matriz ou um objeto em momentos diferentes.
Em outros idiomas, a digitação pode ser mais forte, exigindo que a conversão do ponto flutuante para o número inteiro seja especificada explicitamente. Em outros idiomas, a digitação pode ser mais fraca, permitindo que a conversão da string "3.0" para o float 3.0 no número inteiro 3 seja realizada automaticamente.
E em outros idiomas, o código pode ser interpretado uma linha de cada vez, ou compilado em código de bytes e, em seguida, interpretado, ou compilado na hora certa, ou submetido a uma ampla variedade de outros esquemas de execução.
fonte
Func_()+1
: aqui, no momento da compilação, o compilador precisa saber o tipo deFunc_i()
, para gerar o código de máquina apropriado. Talvez não seja possível para o assembly manipularFunc_()+1
chamando o tipo em tempo de execução, ou é possível, mas fazê-lo tornará o programa lento no tempo de execução. Eu acho que é o suficiente para mim por enquanto.int func(...)
... ou seja, recebem uma lista de argumentos variados. Isso significa que, se você definir uma função comoint putc(char)
mas esquecer de declará-la, ela será chamada comoint putc(int)
(porque char passado por uma lista de argumentos variados é promovido paraint
). Portanto, enquanto o exemplo do OP funcionou porque sua assinatura correspondia à declaração implícita, é compreensível que esse comportamento tenha sido desencorajado (e os avisos apropriados foram adicionados).Uma restrição de design da linguagem C era que ela deveria ser compilada por um compilador de passagem única, o que a torna adequada para sistemas com muita restrição de memória. Portanto, o compilador sabe a qualquer momento apenas sobre as coisas mencionadas anteriormente. O compilador não pode pular adiante na fonte para encontrar uma declaração de função e depois voltar para compilar uma chamada para essa função. Portanto, todos os símbolos devem ser declarados antes de serem utilizados. Você pode pré-declarar uma função como
na parte superior ou em um arquivo de cabeçalho para ajudar o compilador.
Nos seus exemplos, você usa dois recursos duvidosos da linguagem C que devem ser evitados:
Se uma função é usada antes de ser declarada corretamente, é usada como uma "declaração implícita". O compilador usa o contexto imediato para descobrir a assinatura da função. O compilador não varrerá o restante do código para descobrir qual é a declaração real.
Se algo for declarado sem um tipo, o tipo será considerado
int
. É o caso, por exemplo, de variáveis estáticas ou tipos de retorno de função.Então
printf("func:%d",Func_i())
, temos uma declaração implícitaint Func_i()
. Quando o compilador atinge a definição de funçãoFunc_i() { ... }
, isso é compatível com o tipo Mas se você escreveufloat Func_i() { ... }
neste momento, você tem a implicação declaradaint Func_i()
e explicitamente declaradafloat Func_i()
. Como as duas declarações não coincidem, o compilador apresenta um erro.Limpando alguns equívocos
O compilador não encontra o valor retornado por
Func_i
. A ausência de um tipo explícito significa que o tipo de retorno éint
por padrão. Mesmo se você fizer isso:então o tipo será
int Func_i()
e o valor de retorno será truncado silenciosamente!O compilador finalmente conhece o tipo real de
Func_i
, mas não conhece o tipo real durante a declaração implícita. Somente quando mais tarde alcança a declaração real, é possível descobrir se o tipo declarado implicitamente estava correto. Mas nesse ponto, o assembly da chamada de função já pode ter sido gravado e não pode ser alterado no modelo de compilação C.fonte
Unit
faz um bom tipo padrão do ponto de vista da teoria dos tipos, mas falha nos aspectos práticos da programação próxima aos sistemas metálicos para os quais B e C foram projetados.Func_i()
, ele imediatamente gera e salva o código para o processador saltar para outro local, receber um número inteiro e continuar. Quando o compilador encontra aFunc_i
definição posteriormente , ele garante que as assinaturas correspondam e, se o fizerem, coloca o assemblyFunc_i()
nesse endereço e diz para ele retornar algum número inteiro. Quando você executar o programa, o processador, em seguida, segue essas instruções com o valor3
.Primeiro, seus programas são válidos para o padrão C90, mas não para os seguintes. int implícito (permitindo declarar uma função sem fornecer seu tipo de retorno) e declaração implícita de funções (permitindo usar uma função sem declará-la) não são mais válidas.
Segundo, isso não funciona como você pensa.
O tipo de resultado é opcional no C90, não fornecendo um significa um
int
resultado. Isso também é válido para a declaração de variáveis (mas você precisa fornecer uma classe de armazenamentostatic
ouextern
).O que o compilador faz ao ver a
Func_i
chamada é sem uma declaração anterior, está assumindo que existe uma declaraçãoele não procura mais no código para ver com que eficácia
Func_i
é declarada. SeFunc_i
não fosse declarado ou definido, o compilador não mudaria seu comportamento ao compilarmain
. A declaração implícita é apenas para função, não há para variável.Observe que a lista de parâmetros vazia na declaração não significa que a função não aceita parâmetros (é necessário especificar
(void)
isso), significa que o compilador não precisa verificar os tipos dos parâmetros e terá o mesmo conversões implícitas que são aplicadas a argumentos passados para funções variadas.fonte
extern int Func_i()
. Não parece em lugar algum.-S
(se você estiver usandogcc
) permitirá que você veja o código de montagem gerado pelo compilador. Em seguida, você pode ter uma idéia de como os valores de retorno são tratados no tempo de execução (normalmente usando um registro do processador ou algum espaço na pilha do programa).Você escreveu em um comentário:
Isso é um equívoco: a execução não é realizada linha por linha. A compilação é feita linha por linha, e a resolução de nomes é feita durante a compilação, e só resolve nomes, não retorna valores.
Um modelo conceitual útil é o seguinte: Quando o compilador lê a linha:
emite código equivalente a:
O compilador também faz uma anotação em alguma tabela interna que ainda
function #2
é uma função declarada denominadaFunc_i
, que pega um número não especificado de argumentos e retorna um int (o padrão).Mais tarde, quando ele analisa isso:
o compilador
Func_i
consulta a tabela mencionada acima e verifica se os parâmetros e o tipo de retorno correspondem. Caso contrário, para com uma mensagem de erro. Se o fizerem, adiciona o endereço atual à tabela de funções internas e segue para a próxima linha.Portanto, o compilador não "procurou"
Func_i
quando analisou a primeira referência. Simplesmente anotou em alguma tabela e continuou analisando a próxima linha. E no final do arquivo, ele possui um arquivo de objeto e uma lista de endereços de salto.Posteriormente, o vinculador pega tudo isso e substitui todos os ponteiros da "função # 2" pelo endereço de salto real, para emitir algo como:
Muito mais tarde, quando o arquivo executável é executado, o endereço de salto já está resolvido e o computador pode simplesmente pular para o endereço 0x1215. Nenhuma pesquisa de nome é necessária.
Isenção de responsabilidade : como eu disse, esse é um modelo conceitual e o mundo real é mais complicado. Compiladores e vinculadores fazem todos os tipos de otimizações malucas hoje. Eles podem até "pular para cima" para procurar
Func_i
, embora eu duvide. Mas a linguagem C é definida de uma maneira que você pode escrever um compilador super simples como esse. Então, na maioria das vezes, é um modelo muito útil.fonte
1. call "function #2", put the return-type onto the stack and put the return value on the stack?
printf(..., Func_i()+1);
- o compilador precisa saber o tipo deFunc_i
, para poder decidir se deve emitir umaadd integer
ou umaadd float
instrução. Você pode encontrar alguns casos especiais em que o compilador pode continuar sem as informações de tipo, mas o compilador precisa funcionar para todos os casos.float
valores podem viver em um registro FPU - então não haveria nenhuma instrução. O compilador apenas controla qual valor é armazenado em qual registro durante a compilação e emite coisas como "adicione constante 1 ao registro FP X". Ou poderia permanecer na pilha, se não houver registros livres. Em seguida, haveria "aumentar o ponteiro da pilha em 4" e o valor seria "referenciado" como algo como "ponteiro da pilha - 4". Mas todas essas coisas só funcionam se os tamanhos de todas as variáveis (antes e depois) na pilha forem conhecidos em tempo de compilação.Func_i()
ou / eData_i
, ele deve determinar seus tipos; não é possível na linguagem assembly fazer uma chamada para o tipo de dados. Eu preciso estudar as coisas em detalhes para ter certeza.C e várias outras linguagens que exigem declarações foram projetadas em uma época em que o tempo e a memória do processador eram caros. O desenvolvimento de C e Unix andou de mãos dadas por algum tempo, e este último não tinha memória virtual até o 3BSD aparecer em 1979. Sem espaço extra para trabalhar, os compiladores tendiam a ser casos de passagem única , porque não tinham. requer a capacidade de manter alguma representação de todo o arquivo na memória de uma só vez.
Compiladores de passagem única são, como nós, sobrecarregados com a incapacidade de ver o futuro. Isso significa que as únicas coisas que eles podem ter certeza são o que disseram explicitamente antes da compilação da linha de código. É claro para qualquer um de nós que
Func_i()
é declarado posteriormente no arquivo de origem, mas o compilador, que opera em um pequeno pedaço de código de cada vez, não tem idéia de que está chegando.No início de C (AT&T, K&R, C89), o uso de uma função
foo()
antes da declaração resultou em uma declaração de fato ou implícita deint foo()
. Seu exemplo funciona quandoFunc_i()
é declaradoint
porque corresponde ao que o compilador declarou em seu nome. Mudá-lo para qualquer outro tipo resultará em conflito porque não corresponde mais ao que o compilador escolheu na ausência de uma declaração explícita. Esse comportamento foi removido no C99, onde o uso de uma função não declarada se tornou um erro.E os tipos de retorno?
A convenção de chamada para código de objeto na maioria dos ambientes requer conhecer apenas o endereço da função que está sendo chamada, o que é relativamente fácil de lidar com compiladores e vinculadores. A execução pula para o início da função e volta quando ela retorna. Qualquer outra coisa, principalmente arranjos de transmissão de argumentos e um valor de retorno, é determinada inteiramente pelo chamador e chamado em um arranjo chamado de convenção de chamada . Desde que ambos compartilhem o mesmo conjunto de convenções, torna-se possível para um programa chamar funções em outros arquivos de objeto, independentemente de terem sido compilados em qualquer idioma que compartilhe essas convenções. (Na computação científica, você encontra muitas chamadas em C do FORTRAN e vice-versa, e a capacidade de fazer isso vem de ter uma convenção de chamadas.)
Uma outra característica do C inicial era que os protótipos como os conhecemos agora não existiam. Você pode declarar o tipo de retorno de uma função (por exemplo,
int foo()
), mas não seus argumentos (por exemplo,int foo(int bar)
não era uma opção). Isso existia porque, conforme descrito acima, o programa sempre mantinha uma convenção de chamada que poderia ser determinada pelos argumentos. Se você chamou uma função com o tipo errado de argumentos, era uma situação de entrada e saída de lixo.Como o código do objeto tem a noção de um retorno, mas não um tipo de retorno, um compilador precisa conhecer o tipo de retorno para lidar com o valor retornado. Quando você está executando as instruções da máquina, são apenas bits e o processador não se importa se a memória em que você está tentando comparar um
double
realmente tem umint
. Ele apenas faz o que você pede e, se você o quebrar, possui as duas peças.Considere estes bits de código:
O código à esquerda é compilado em uma chamada para,
foo()
seguido pela cópia do resultado fornecido pela convenção de chamada / retorno para onde quer quex
esteja armazenado. Esse é o caso fácil.O código à direita mostra uma conversão de tipo e é por isso que os compiladores precisam conhecer o tipo de retorno de uma função. Números de ponto flutuante não podem ser despejados na memória, onde outros códigos esperam ver um
int
porque não há conversão mágica que ocorre. Se o resultado final precisar ser um número inteiro, é necessário que haja instruções que orientem o processador para fazer a conversão antes do armazenamento. Sem saber o tipo de retornofoo()
antecipadamente, o compilador não teria idéia de que o código de conversão é necessário.Os compiladores de múltiplas passagens permitem todo tipo de coisa, uma das quais é a capacidade de declarar variáveis, funções e métodos após a primeira utilização. Isso significa que quando o compilador começa a compilar o código, ele já viu o futuro e sabe o que fazer. Java, por exemplo, exige múltiplas passagens em virtude do fato de sua sintaxe permitir a declaração após o uso.
fonte
Func_i()
o valor de retorno foi encontrado.double foo(); int x; x = foo();
simplesmente dá o erro. Eu sei que não podemos fazer isso. Minha pergunta é que, na chamada de função, o processador encontra apenas o valor de retorno; por que não pode também encontrar o tipo de retorno também?foo()
que o compilador saiba o que fazer com ele.