paulo1205
(usa Ubuntu)
Enviado em 28/02/2019 - 15:39h
Nick-us escreveu:
Já tem algum tempo que venho tentando entender melhor a função main, mas sempre ficaram dúvidas... Aos poucos fui mudando minha forma de escrever, após entender os motivos para isso.
Começei escrevendo assim e adorava, pois ODIEI as mudanças:
main()
Aprendi e entendi que após a a revisão de 1999, tornou-se quase obrigatório TER que informar o tipo da função. Embora claro, não apreciei em nada essa mudança, pq se antes já era entendido que seu tipo qdo não informado era int. Tipo fiquei pensando... Os caras estudaram 10 anos para piorar a forma de declarar? Agora ter que escrever 3 letras na função que CHATO!
A questão é histórica, e tem a ver com a evolução da linguagem e com medidas para torná-la mais segura e mais produtiva para o desenvolvedor, permitindo transferir para o compilador parte dos diagnósticos sobre uso impróprio, os quais, de outro modo (e assim foi num passado não tão remoto), teriam de ser feitos com depuração manual pelo próprio desenvolvedor.
Parte dessa história é a seguinte.
C apareceu como evolução de outra linguagem, chamada, não por acaso, B. B era uma linguagem sem tipos de dados (ou, antes, todos os dados tinham o mesmo tipo, que era um inteiro com a mesma largura do barramento da máquina). Além de não ter tipos diferenciados para diferentes usos, essa linguagem também não possuía declarações de funções: se algum símbolo desconhecido aparecesse ao longo do programa com um aspecto de invocação de função, esse símbolo necessariamente denotava uma função que haveria de ser definida em algum momento mais à frente.
Mas como saber se a função invocada deveria receber argumentos e, se o fizesse, quantos argumentos seriam? E como saber se ela devolvia algum valor? E como fazer esses controles num compilador que executa numa máquina que tem apenas 9KiB (sim, apenas 9216 bytes!, mas dispostos como apenas 4096 palavras de 18 bits), sendo que mais da metade dessa memória era ocupada pelo sistema operacional?
Eis como:
• o compilador presume que qualquer função pode receber qualquer quantidade de argumentos;
• para tanto, a disposição na memória dos argumentos com que a função é invocada é feita pelo compilador como parte do código que chama a função e a limpeza desses elementos é feita também como parte do código que chamou a função, depois que ela retorna, e essas operações ocorrem em cada ponto do programa em que a função é chamada, de modo independente dos outros pontos (grande parte de linguagens contemporâneas a B — e mesmo ainda hoje — usam quantidade fixa de parâmetros, e a limpeza da memória usada para transferir os argumentos faz parte da função que foi chamada, não de quem a chamou);
• mais ainda: os argumentos são dispostos na memória numa ordem tal que facilite identificar onde está o primeiro elemento (se presente), de modo que frequentemente eles são dispostos em ordem reversa a como aparecem escritos (i.e., o último argumento é salvo primeiro, depois o penúltimo, e assim por diante — ou para trás — até o primeiro, o que, de novo, era diferente da maioria das linguagens que eram suas contemporâneas);
• toda função retorna algum valor a quem chamou, mesmo que não haja um comando de retorno explícito (se o comando estiver faltando, o valor de um dos registradores da máquina é usado — normalmente o do registrador que tem a função de acumulador);
• valores de retorno podem ser ignorados (isto é: não precisam ser atribuídos a nenhuma variável nem usados como parte de uma expressão) e gratuitamente descartados pelo programa.
Essas regras valiam sempre, mesmo que você tivesse uma função que recebesse uma quantidade fixa de parâmetros. Assim, mesmo que você tivesse a seguinte definição de função
swap(a, b){ /* Parâmetros têm nome mas não especificam tipo, pois todos os dados são do mesmo tipo */
auto t=*a; /* “auto” não especifica o tipo de dados (pois só há um tipo), mas que a variável é local e não é estática. */
*a=*b; /* Note que, apesar de não especificar tipo, aqui os parâmetros têm a função de ponteiros. */
*b=t; /* Logo, cabia também ao programador saber a função de cada variável, sem que o compilador o ajudasse a sabê-la através do tipo de dados. */
}
era possível chamá-la de qualquer uma das maneiras abaixo, sem que o compilador minimamente reclamasse.
swap(&x, &y); /* uso esperado: OK */
swap(&x, &y, &z); /* relativamente segura, apesar do desperdício: o terceiro argumento é ignorado. */
a=swap(&x, &y); /* “a” recebe um valor de retorno implícito e desconhecido; válido? sim; seguro? não! */
swap(&x); /* claramente inseguro (possivelmente vai trocar o valor de x com uma instrução do código do programa, mas o compilador não vai avisar) */
swap(); /* pior ainda, e mesmo assim sem que o compilador advirta */
*swap()=5; /* pior ainda (usa o valor de retorno implícito como ponteiro, e tenta gravar ali um valor) */
As origens do C remontam a uma evolução do B, de modo a se adaptar a uma nova máquina em que era útil ter diferentes tipos de dados. O então novíssimo PDP-11 permitia acesso a dados como inteiros de 16 bits ou como
bytes de 8 bits, e, pouco tempo depois, também como números de ponto flutuante, usando tanto precisão simples quanto precisão dupla, e seria muito conveniente se o B soubesse tratar cada um desses tipos de dados.
Contudo ninguém queria ter de reescrever todos os programas já escritos em B, e vem daí a bagagem de compatibilidade de
int implícito, herdada pelo C (e que hoje é considerada por muitos como um estorvo).
Como o processador do PDP-11 era de 16 bits (embora também pudesse manipular bytes) e principalmente porque os ponteiros eram também de 16 bits, decidiu-se que qualquer tipo não explicitamente definido implicava
int . Assim quase todos os programas já prontos em B poderiam ser prontamente usados na nova máquina.
Contudo, ao longo da evolução, coisas novas foram sendo incorporadas ao B mutante: estruturas, ponteiros como tipos de dados distintos dos demais novos tipos, uma nova forma de dispor
arrays em memória, e logo o novo B já não era mais tão compatível assim com o velho B. A linguagem mudou de nome — agora era C — mas não teve uma ruptura assim tão radical: além de programas em B compilarem em C com uma quantidade pequena de modificações, a filosofia não tinha mudado muito, para além daquilo que a nova máquina requeria e de algumas conveniências que o
hardware e a maior quantidade de memória permitiram incorporar. Como exemplo dessa inércia filosófica que imperava na época se pode apontar a percepção inicial de que intercambiar livremente ponteiros e inteiros (como ocorria em B, até porque não havia entre eles distinção formal) era um ponto positivo e desejável da nova linguagem.
Uma das conveniências do novo C, em particular, era o preprocessador, que permitia incluir outros arquivos como se fossem parte do arquivo de programa que o incluía. Isso permitia não ter de ficar declarando, em cada programa, todas as funções que o programa usava a partir de uma origem externa. Era muito melhor dizer simplesmente
#include <stdio.h>
do que ter declarar novamente
printf ,
getchar ,
scanf etc. em cada programa em que tais funções fossem usadas. No entanto, o conteúdo do arquivo que representava <stdio.h> não era muito elaborado. Não raro, por simplicidade, ela tinha uma forma bem chinfrim:
extern printf(); /* “extern” deixa bem claro que é só uma declaração, a definição reside em outro lugar. */
extern putchar(); /* Note que não se fala nada sobre tipo de retorno nem argumentos: */
extern scanf(); /* assume-se int implícito e qualquer quantidade de argumentos, */
extern getchar(); /* tal como era em B. */
/* etc. */
Apenas quando era preciso desfazer possíveis problemas com relação a tipo de retorno é que um tipo diferente era explicitamente especificado, como nos seguintes exemplos.
extern double sin(); /* (em <math.h>): valor de retorno é double, e o compilador precisa saber que não deve presumir int , mas não havia cuidado quanto ao parâmetro (que também era double). */
extern char *malloc(); /* (em <stdlib.h>): retorna um ponteiro para quantidade de bytes (chars) alocados (ATENÇÃO: essa versão é obsoleta; atualmente o tipo de retorno é “void *” ). */
Os tipos retorno diferentes de
int exigiam que as funções fossem declaradas antes de seu primeiro uso, para que o compilador não assumisse que tal tipo seria
int ao encontrar uma chamada a função ainda não declarada. Vem daí a prática de colocar comandos de inclusão no começo dos programas; antes, em B, quando não havia tipo a ser declarado, as declarações eram desnecessárias, e as eventuais definições de funções e variáveis globais costumavam vir
após a definição de
main () e/ou outras funções que as usassem.
Não havia, porém, como contornar — ou mesmo detectar automaticamente — problemas com a quantidade e os tipos dos argumentos. A função
sin (), por exemplo, requer um argumento do tipo
double . Se o usuário quisesse economizar memória no seu programa usando
float em lugar de
double para objetos que não precisassem realmente de dupla precisão, ele tinha de explicitamente converter suas variáveis para
double para poder usá-las como argumento de
sin (), sob pena de provocar mau funcionamento do programa ao fazer com que a função recebesse menos bytes do que esperava.
Essas características não eram um problema gigantesco no início porque, nesse período embrionário, os usuários do C eram seus próprios criadores e outras pessoas ligadas ao UNIX. Entretanto, durante a segunda metade da década de 1970 e início da de 1980, tanto o UNIX quanto o C cresceram muito em quantidade de usuários. O C, em particular, por ser, naquela altura, uma linguagem simples, mas relativamente poderosa, especialmente quando comparada a alternativas, permitia a criação relativamente fácil de compiladores que produziam código de máquina quase tão eficiente quanto o que se conseguia trabalhando diretamente em Assembly, mas através de uma linguagem muito mais fácil para o programador do que era o Assembly. Naquela época de memória e disco caríssimos e de CPUs muito lentas, isso provocou um entusiasmo muito grande pelo C. Logo começaram a aparecer compiladores em C para quase todos os sistemas, desde
mainframes aos mais simples microcomputadores de 8 bits da época.
Muitos dos aspectos mencionados anteriormente, no entanto, eram inóspitos para muitos programadores, sobretudo aqueles acostumados a outras linguagens. Havia um abismo muito grande entre um programa sintaticamente válido e um programa que fosse semanticamente correto. Em particular, o fato de qualquer função aceitar qualquer quantidade de parâmetros de qualquer tipo sem nenhum tipo de validação tornava muito difícil localizar e corrigir eventuais erros: uma simples leitura do código não bastava para identificar mesmo os erros mais grosseiros, pois era necessária muita atenção para acompanhar os tipos de cada variável e o que cada função que operasse sobre elas podia ou não receber como argumento e produzir como valor de retorno.
Não fosse isso suficiente, ainda havia o problema da falta de padronização, em que cada implementação de C subtraía ou acrescentava livremente partes e recursos à linguagem descrita no livro de Brian Kernigham e Dennis Ritchie (o famoso “K&R”, que era o mais próximo que havia de um padrão para a linguagem, mas que na verdade apenas descrevia a linguagem que acompanhava a sexta edição do UNIX, o sistema operacional gestado dentro do Bell Laboratories, ou Bell Labs., uma subsidiária da empresa de telefonia AT&T).
Com o tempo, algumas facilidades adicionais em relação ao C do K&R começaram a aparecer aqui e ali, e se tornaram tão interessantes que começaram a ser imitadas. Algumas procediam de fontes vinculadas ao próprio berço do C. Dois desses foram Steve Johnson e Bjarne Stroustrup, que trabalhavam no Bell Labs. e desenvolveram, respectivamente, uma versão do compilador C voltada para produzir código maximamente compatível (
pcc , abreviação de
Portable C Compiler ), que gerava alertas sobre construções potencialmente perigosas envolvendo conversões de tipo e usos potencialmente errôneos de função, e uma linguagem derivada do C, que depois viria a ser chamada C++, e que introduziu a ideia de declarações de funções como protótipos que indicavam claramente os tipos de seus argumentos, de modo que o compilador poderia não apenas indicar o uso de argumentos indevidos, mas também providenciar conversões automáticas, se tais conversões fossem possíveis. Houve também um esforço da padronização, patrocinado pelo Instituto de Padrões Nacionais Americanos (ANSI), que colheu boas ideias de várias fontes, como protótipos de função,
void para indicar funções que não devolviam valores,
enum , ponteiros para dados de nenhum tipo em particular (“
void * ”, usados, por exemplo, como tipo de retorno padrão da versão padronizada de
malloc ()), e que procurou produzir uma linguagem o mais neutra possível de qualquer variação ou equipamento em particular, ao mesmo tempo em que procurava não quebrar deliberadamente a compatibilidade com código antigo que funcionasse bem.
É claro que é muito difícil conseguir tudo isso ao mesmo tempo. Em particular, não dá para, de modo consistente e simultaneamente, fazer bom uso de protótipos de função e manter compatibilidade com declarações de funções compatíveis com K&R (ou pré-K&R). De certo modo, a compatibilidade atrasou a adoção de alguns recursos novos, de modo que durante muito tempo (mesmo até os dias de hoje! — a documentação das rotinas em C do sistema AIX, da IBM, é um desses exemplos, conforme você mesmo pode conferir
on-line ) ainda continuou existindo código e documentação de código com sintaxe pré-padronização de 1989 pelo ANSI.
Um desses fósseis é o famigerado
int implícito, aceito pelo padrão de 1989 como uma concessão em favor da compatibilidade, mas que se mostrou o maior bastião de resistência à adoção de práticas mais seguras de programação. Eis algo que o padrão seguinte, de 1999 procurou remediar ao tentar banir a falta de declaração de tipo de retorno (refletindo o que antes já fizera o C++, conforme seu próprio padrão, lançado em 1998). Porém, mesmo em C99 ou C++, o
int implícito ainda deixa suas marquinhas em outros cantos: por exemplo, os tipos
unsigned ,
short ,
long e
long long são, na verdade, versões resumidas e implícitas de
unsigned int ,
short int ,
long int e
long long int .
Então passei a escrever assim para não ficar me aborrecendo com as msg chatas do compilador, e pra não ter que escrever tanto para compilar claro.
int main()
Me chateia muito de verdade eu ter que informar qualquer coisa. Porque? EU NUNCA uso retorno de nada, pq se não funciona eu VEREI isso. Eu só quero que ele execute as linhas dentro! Pois pra mim na maioria dos casos o main não serve para nada a não ser como um GRUPO de { } para eu por o código importante!
O caso é que os programas em C que nós fazemos e usamos nos nossos computadores pessoais não executam no vazio. Antes do programa chegar ao início da função
main () que você define, um ambiente de execução hospedeiro teve de fazer uma série de atividades para preparar a execução para você. E esse ambiente de execução lhe oferece formas de se comunicar com o seu programa, através de argumentos que
main () pode receber, se você para tanto a preparar, e também espera que o seu programa devolva uma informação sobre o grau de sucesso da execução, quando terminar de executar, através do valor de retorno de
main () (ou através da função padronizada
exit ()).
Por que essa forma de comunicação, em vez de alguma outra, como variáveis de ambiente, ou canais de passagens de mensagens, ou alguma outra técnica ou tecnologia? Porque, como eu disse acima, o padrão da linguagem procurou fazê-la o mais independente possível de recursos particulares de uma implementação em particular, de modo que fazia pouco sentido exigir variáveis de ambiente de um Apple II ou
named pipes de um PC com MS-DOS. Usar uma função é uma solução relativamente simples e em harmonia com um recurso — funções — que tem de ser implementado de qualquer maneira para se poder suportar o restante da linguagem, requerendo apenas que o executável tenha um código fixo que executa antes de
main () e a chama, e de um epílogo que executa quando ela retorna, usando o valor que ela vier a devolver.
Esses prólogo e epílogo não são exclusivos do C, por sinal. Por exemplo, mesmo que você execute uma linguagem interpretada, em que pode sair colocando comandos diretamente no primeiro nível de interface de programação com a linguagem, o próprio carregamento do interpretador e sua preparação para receber esse comando imediato já constituem um prólogo, e provavelmente muito mais pesado do que o prólogo de um programa compilado a partir de código em C.
Logo minha opinião seria, se eu não informo é porque ele não tem que ser nada. Isso é problema meu!
Porém, se você quer usar C, tem de se amoldar ao que ele exige — e que está longe de não ser razoável.
Da mesma forma ter que informar void. PRA QUE? Se não tem nada, PRA QUE eu tenho que informar que não tem nada?
Para que o compilador possa ter certeza de que a falta de alguma coisa não foi esquecimento seu.
No caso específico de declarações de função, existe uma outra razão, que é a uniformidade sintática: toda declaração começa com um nome de tipo, mesmo que o nome de tipo indique algo que não representada dado nenhum. Isso facilita a construção do compilador, em vez de a dificultar.
AFF! É como se eu fosse passar por seguranças completamente Nú (PELADO), e o segurança me perguntar o que é que EU estou levando! Meio irritante isso!
Acho que não é uma boa analogia. Um guarda de segurança facilmente enxerga você (quase) todo de uma vez, e o identifica como um homem, como tantos outros que ele já viu. Mas mesmo com sua nudez, pode fazer sentido perguntar o que você, a exemplo do que fazem alguns traficantes de drogas e contrabandistas de jóias, pode estar levando escondido no estômago ou em algum orifício do corpo.
Já um compilador começa a enxergar uma sequência de caracteres e tem de determinar o que aquela sequência representa. Esse trabalho é mais simples se você puder reduzir a quantidade de opções.
Se alguém souber explicar o que EU não compreendi, para achar essas soluções tão ridículas e desnecessárias, agradeço!
Como você viu, é uma longa história, que por sinal está mais bem contada, e em grande parte em primeira pessoa pelo falecido Dennins Ritchie, em
https://www.bell-labs.com/usr/dmr/www/chist.html . Outras fontes de consulta foram
https://www.bell-labs.com/usr/dmr/www/btut.html e
http://www.linfo.org/pdp-7.html .
Outra questão: O Paulo respondeu a uma pessoa aqui no fórum o seguinte: no artigo:
https://www.vivaolinux.com.br/topico/C-C++/Usar-ioh-no-Linux
Se você estiver usando C (e não C++), é errado deixar os parênteses vazios na declaração de main(). O C entende que parênteses vazios significam que a função recebe uma quantidade qualquer de elementos de quaisquer tipos (C++, por outro lado, entende os parênteses vazios como nenhum argumento). Se você não quiser receber argumentos via main() em C, deve declará-la do seguinte modo
int main(void)
Logo quero saber, se o Paulo ler isso, e poder responder, ajuda muito claro, se então é DEFINITIVO que devo declarar da forma abaixo? E se for possível me explicar o porque, para que eu possa de verdade compreender e NUNCA MAIS tocar nesse assunto kkkkkkkkk Agradeço!
int main(void)
Acho que o textão acima responde isso. Se tiver sobrado alguma dúvida, pode perguntar.
... “Principium sapientiae timor Domini, et scientia sanctorum prudentia.” (Proverbia 9:10)