Como o método main () funciona em C?

96

Eu sei que existem duas assinaturas diferentes para escrever o método principal -

int main()
{
   //Code
}

ou para lidar com o argumento da linha de comando, nós o escrevemos como-

int main(int argc, char * argv[])
{
   //code
}

Em C++Eu sei que podemos sobrecarregar um método, mas em Ccomo o compilador lidar com essas duas assinaturas diferentes de mainfunção?

Ritesh
fonte
14
Sobrecarga refere-se a ter dois métodos com o mesmo nome no mesmo programa. Você só pode ter um mainmétodo em um único programa em C(ou, na verdade, em praticamente qualquer linguagem com tal construção).
Kyle Strand,
12
C não possui métodos; tem funções. Métodos são a implementação de back end de funções "genéricas" orientadas a objetos. O programa chama uma função com alguns argumentos de objeto, e o sistema de objetos escolhe um método (ou talvez um conjunto de métodos) com base em seus tipos. C não tem nada disso, a menos que você mesmo simule.
Kaz,
4
Para uma discussão profunda sobre os pontos de entrada do programa - não especificamente main- eu recomendo o livro clássico de John R. Levines "Linkers & Loaders".
Andreas Spindler,
1
Em C, a primeira forma é int main(void), não int main()(embora eu nunca tenha visto um compilador que rejeite a int main()forma).
Keith Thompson
1
@harper: o ()formulário é obsoleto e não está claro se é mesmo permitido main(a menos que a implementação o documente especificamente como um formulário permitido). O padrão C (consulte 5.1.2.2.1 Inicialização do programa) não menciona o ()formulário, que não é exatamente equivalente ao ()formulário. Os detalhes são muito longos para este comentário.
Keith Thompson

Respostas:

132

Algumas das características da linguagem C começaram como hacks que simplesmente funcionaram.

Múltiplas assinaturas para listas de argumentos principais, bem como de comprimento variável, é um desses recursos.

Os programadores notaram que podem passar argumentos extras para uma função e nada de ruim acontece com o compilador fornecido.

Este é o caso se as convenções de chamada forem tais que:

  1. A função de chamada limpa os argumentos.
  2. Os argumentos mais à esquerda estão mais próximos do topo da pilha ou da base da estrutura da pilha, de forma que argumentos espúrios não invalidem o endereçamento.

Um conjunto de convenções de chamada que obedece a essas regras é a passagem de parâmetro baseada em pilha, por meio da qual o chamador exibe os argumentos e eles são empurrados da direita para a esquerda:

 ;; pseudo-assembly-language
 ;; main(argc, argv, envp); call

 push envp  ;; rightmost argument
 push argv  ;; 
 push argc  ;; leftmost argument ends up on top of stack

 call main

 pop        ;; caller cleans up   
 pop
 pop

Em compiladores onde esse tipo de convenção de chamada é o caso, nada de especial precisa ser feito para oferecer suporte aos dois tipos main, ou mesmo tipos adicionais. mainpode ser uma função sem argumentos e, nesse caso, ignora os itens que foram colocados na pilha. Se for uma função de dois argumentos, ele encontra argce argvcomo os dois itens mais altos da pilha. Se for uma variante de três argumentos específica da plataforma com um ponteiro de ambiente (uma extensão comum), isso também funcionará: encontrará o terceiro argumento como o terceiro elemento do topo da pilha.

E assim, uma chamada fixa funciona para todos os casos, permitindo que um único módulo fixo de inicialização seja vinculado ao programa. Esse módulo pode ser escrito em C, como uma função semelhante a esta:

/* I'm adding envp to show that even a popular platform-specific variant
   can be handled. */
extern int main(int argc, char **argv, char **envp);

void __start(void)
{
  /* This is the real startup function for the executable.
     It performs a bunch of library initialization. */

  /* ... */

  /* And then: */
  exit(main(argc_from_somewhere, argv_from_somewhere, envp_from_somewhere));
}

Em outras palavras, este módulo inicial apenas chama um principal de três argumentos, sempre. Se main não leva argumentos, ou apenas int, char **, funciona bem, assim como se não leva argumentos, devido às convenções de chamada.

Se você fizesse esse tipo de coisa em seu programa, seria não portável e considerado um comportamento indefinido pelo ISO C: declarar e chamar uma função de uma maneira e defini-la de outra. Mas o truque de inicialização de um compilador não precisa ser portátil; não é guiado pelas regras para programas portáteis.

Mas suponha que as convenções de chamada sejam tais que não funcione dessa maneira. Nesse caso, o compilador deve tratar de forma mainespecial. Quando percebe que está compilando a mainfunção, pode gerar código compatível com, digamos, uma chamada de três argumentos.

Ou seja, você escreve isto:

int main(void)
{
   /* ... */
}

Mas quando o compilador o vê, ele essencialmente executa uma transformação de código para que a função que ele compila se pareça mais com isto:

int main(int __argc_ignore, char **__argv_ignore, char **__envp_ignore)
{
   /* ... */
}

exceto que os nomes __argc_ignorenão existem literalmente. Esses nomes não são introduzidos em seu escopo e não haverá nenhum aviso sobre argumentos não utilizados. A transformação do código faz com que o compilador emita o código com a ligação correta, que sabe que deve limpar três argumentos.

Outra estratégia de implementação é o compilador ou talvez o linker gerar a __startfunção de forma personalizada (ou o que quer que seja chamado), ou pelo menos selecionar uma das várias alternativas pré-compiladas. As informações podem ser armazenadas no arquivo de objeto sobre qual dos formulários suportados mainestá sendo usado. O vinculador pode consultar essas informações e selecionar a versão correta do módulo de inicialização que contém uma chamada maincompatível com a definição do programa. Implementações de C geralmente têm apenas um pequeno número de formas com suporte de, mainportanto, essa abordagem é viável.

Compiladores para a linguagem C99 sempre têm que tratar mainespecialmente, até certo ponto, para suportar o hack de que se a função terminar sem uma returninstrução, o comportamento é como se tivesse return 0sido executado. Isso, novamente, pode ser tratado por uma transformação de código. O compilador percebe que uma função chamada mainestá sendo compilada. Em seguida, ele verifica se a extremidade do corpo é potencialmente alcançável. Nesse caso, ele insere umreturn 0;

Kaz
fonte
34

NÃO há sobrecarga mainnem mesmo em C ++. A função principal é o ponto de entrada para um programa e apenas uma única definição deve existir.

Para Padrão C

Para um ambiente hospedado (esse é o normal), o padrão C99 diz:

5.1.2.2.1 Inicialização do programa

A função chamada na inicialização do programa é nomeada main. A implementação não declara nenhum protótipo para esta função. Deve ser definido com um tipo de retorno de inte sem parâmetros:

int main(void) { /* ... */ }

ou com dois parâmetros (referidos aqui como argce argv, embora quaisquer nomes possam ser usados, pois são locais para a função na qual são declarados):

int main(int argc, char *argv[]) { /* ... */ }

ou equivalente; 9) ou de alguma outra maneira definida pela implementação.

9) Assim, intpode ser substituído por um nome de typedef definido como int, ou o tipo de argvpode ser escrito como char **argv, e assim por diante.

Para C ++ padrão:

3.6.1 Função principal [basic.start.main]

1 Um programa deve conter uma função global chamada main, que é o início designado do programa. [...]

2 Uma implementação não deve predefinir a função principal. Esta função não deve ser sobrecarregada . Ele deve ter um tipo de retorno do tipo int, mas caso contrário, seu tipo é definido pela implementação. Todas as implementações devem permitir ambas as seguintes definições de principal:

int main() { /* ... */ }

e

int main(int argc, char* argv[]) { /* ... */ }

O padrão C ++ diz explicitamente "Ela [a função principal] deve ter um tipo de retorno do tipo int, mas de outra forma seu tipo é definido pela implementação", e requer as mesmas duas assinaturas que o padrão C.

Em um ambiente hospedado ( ambiente AC que também oferece suporte às bibliotecas C) - as chamadas do sistema operacional main.

Em um ambiente não hospedado (um destinado a aplicativos incorporados), você sempre pode alterar o ponto de entrada (ou saída) de seu programa usando as diretivas de pré-processador como

#pragma startup [priority]
#pragma exit [priority]

Onde a prioridade é um número integral opcional.

A inicialização do pragma executa a função antes da função principal (prioridade) e a saída do pragma executa a função após a função principal. Se houver mais de uma diretiva de inicialização, a prioridade decide qual será executada primeiro.

Sadique
fonte
4
Não acho que essa resposta realmente responda à questão de como o compilador realmente lida com a situação. A resposta dada por @Kaz dá mais informações, na minha opinião.
Tilman Vogel
4
Acho que essa resposta responde melhor à pergunta do que @Kaz. A pergunta original tem a impressão de que está ocorrendo sobrecarga de operador, e essa resposta resolve isso, mostrando que, em vez de alguma solução de sobrecarga, o compilador aceita duas assinaturas diferentes. Os detalhes do compilador são interessantes, mas não necessários para responder à pergunta.
Waleed Khan
1
Para ambientes independentes ("não hospedados"), há muito mais coisas acontecendo do que apenas #pragma. Há uma interrupção de reinicialização do hardware e é aí que o programa realmente começa. A partir daí, todas as configurações fundamentais são executadas: pilha de configuração, registros, MMU, mapeamento de memória etc. Em seguida, a cópia dos valores init do NVM para as variáveis ​​de armazenamento estático ocorre (segmento .data), bem como "zero-out" em todos variáveis ​​de armazenamento estático que devem ser definidas como zero (segmento .bss). Em C ++, são chamados construtores de objetos com duração de armazenamento estático. E uma vez que tudo isso é feito, o main é chamado.
Lundin,
8

Não há necessidade de sobrecarga. Sim, existem 2 versões, mas apenas uma pode ser usada no momento.

user694733
fonte
5

Esta é uma das estranhas assimetrias e regras especiais da linguagem C e C ++.

Na minha opinião, existe apenas por razões históricas e não há nenhuma lógica séria por trás disso. Observe que mainé especial também por outros motivos (por exemplo, mainem C ++ não pode ser recursivo e você não pode pegar seu endereço e em C99 / C ++ você pode omitir uma returndeclaração final ).

Observe também que mesmo em C ++ não é uma sobrecarga ... ou um programa tem a primeira forma ou a segunda forma; não pode ter ambos.

6502
fonte
Você também pode omitir a returninstrução em C (desde C99).
dreamlax,
Em C, você pode ligar main()e pegar seu endereço; C ++ aplica limites que C não aplica.
Jonathan Leffler
@JonathanLeffler: você está certo, consertado. A única coisa engraçada sobre main que encontrei nas especificações do C99, além da possibilidade de omitir o valor de retorno, é que como o padrão é escrito IIUC, você não pode passar um valor negativo para argcquando recorrente (5.1.2.2.1 não especifica limitações em argce se argvaplicam apenas à chamada inicial para main).
6502
4

O que é incomum mainnão é que possa ser definido de mais de uma maneira, mas apenas de uma de duas maneiras diferentes.

mainé uma função definida pelo usuário; a implementação não declara um protótipo para ele.

A mesma coisa é verdadeira para fooou bar, mas você pode definir funções com esses nomes da maneira que desejar.

A diferença é que mainé invocado pela implementação (o ambiente de tempo de execução), não apenas pelo seu próprio código. A implementação não está limitada à semântica de chamada de função C comum, portanto, ela pode (e deve) lidar com algumas variações - mas não é necessária para lidar com infinitas possibilidades. O int main(int argc, char *argv[])formulário permite argumentos de linha de comando e, int main(void)em C ou int main()C ++, é apenas uma conveniência para programas simples que não precisam processar argumentos de linha de comando.

Quanto ao modo como o compilador lida com isso, depende da implementação. A maioria dos sistemas provavelmente tem convenções de chamada que tornam as duas formas efetivamente compatíveis, e quaisquer argumentos passados ​​para um maindefinido sem parâmetros são silenciosamente ignorados. Do contrário, não seria difícil para um compilador ou vinculador tratar de maneira mainespecial. Se você está curioso para saber como funciona no seu sistema , pode consultar algumas listas de montagem.

E como muitas coisas em C e C ++, os detalhes são em grande parte resultado da história e de decisões arbitrárias feitas pelos projetistas das linguagens e seus predecessores.

Observe que C e C ++ permitem outras definições definidas pela implementação para main- mas raramente há uma boa razão para usá-las. E para implementações independentes (como sistemas embarcados sem sistema operacional), o ponto de entrada do programa é definido pela implementação e nem mesmo é necessariamente chamado main.

Keith Thompson
fonte
3

O mainé apenas um nome para um endereço inicial decidido pelo vinculador, onde mainé o nome padrão. Todos os nomes de função em um programa são endereços iniciais onde a função começa.

Os argumentos da função são colocados / retirados da pilha, portanto, se não houver nenhum argumento especificado para a função, não haverá argumentos colocados / retirados da pilha. É assim que main pode funcionar com ou sem argumentos.

AndersK
fonte
2

Bem, as duas assinaturas diferentes da mesma função main () aparecem apenas quando você as deseja, quero dizer, se seu programa precisa de dados antes de qualquer processamento real de seu código, você pode passá-los através do uso de -

    int main(int argc, char * argv[])
    {
       //code
    }

onde a variável argc armazena a contagem de dados que são passados ​​e argv é uma matriz de ponteiros para char que aponta para os valores passados ​​do console. Caso contrário, é sempre bom ir com

    int main()
    {
       //Code
    }

No entanto, em qualquer caso, pode haver um e apenas um main () em um programa, porque esse é o único ponto onde um programa começa sua execução e, portanto, não pode haver mais de um. (espero que valha a pena)

manish
fonte
2

Uma pergunta semelhante foi feita antes: Por que uma função sem parâmetros (em comparação com a definição real da função) é compilada?

Uma das respostas com melhor classificação foi:

Em C func()significa que você pode passar qualquer número de argumentos. Se você não quiser argumentos, deve declarar comofunc(void)

Então, eu acho que é como mainé declarado (se você pode aplicar o termo "declarado" a main). Na verdade, você pode escrever algo assim:

int main(int only_one_argument) {
    // code
}

e ainda será compilado e executado.

Varepsilon
fonte
1
Excelente observação! Parece que o vinculador é bastante indulgente main, pois há um problema ainda não mencionado: ainda mais argumentos a favor main! Adiciona "Unix (mas não Posix.1) e Microsoft Windows" char **envp(lembro que o DOS permitia isso também, não permitia?), E o Mac OS X e Darwin adicionavam mais um ponteiro char * para "informações arbitrárias fornecidas pelo SO". wikipedia
usr2564301
0

Você não precisa substituir isto, porque apenas um será usado por vez. Sim, existem 2 versões diferentes da função principal

gautam
fonte