#include <stdio.h>
#include <stdlib.h>
hello_world.c – Estrutura de um programa em CEste programa ilustra a estrutura básica de um programa escrito na linguagem C. O objectivo deste programa é imprimir no ecrã o factorial de um número inteiro introduzido pelo utilizador do programa.
De modo a mantê-lo curto, optámos por omitir deste programa todo o código de verificação de erros do utilizador. Todos os programas têm de saber lidar com erros dos seus utilizadores! Não o fazer é um erro grave. Este programa está, assim, gravemente errado. Não diga que não o avisámos. :-)
Este programa consiste numa única unidade de compilação. Ou seja,
corresponde a um único ficheiro destinado a ser compilado. Os ficheiros em
linguagem C que se destinam a ser compilados possuem convencionalmente a
extensão .c. O nome do ficheiro é, assim, hello_world.c.
Este ficheiro não é suficiente para construir o ficheiro executável, pois
recorre a ferramentas da biblioteca padrão do C. Em particular, recorre a
rotinas de leitura e escrita de dados e recorre à macro EXIT_SUCCESS.
Os passos usuais na construção de um ficheiro executável a partir de um programa em linguagem C são os seguintes:
Pré-processar cada um dos ficheiros .c - O pré-processador é um programa
que lê um ficheiro contendo código C e directivas de pré-processamento e
interpreta estas directivas, produzindo um ficheiro contendo linguagem C
apenas (bem como algumas directivas de pré-processamento indicando a
proveniência das linhas de código). A este ficheiro contendo apenas
linguagem C (ou quase), damos o nome de unidade de compilação.
Usualmente as unidades de compilação não têm senão uma existência efémera,
podendo mesmo não chegar a existir enquanto ficheiros no sistema de
ficheiros do computador.
Compilar cada um dos ficheiros resultantes do pré-processamento, ou seja,
compilar cada uma das unidades de compilação. A compilação é o processo de
tradução da linguagem C para a linguagem máquina do computador em causa. O
resultado da compilação de uma unidade de compilação é um ficheiro
objecto. Os ficheiros objecto têm, em Linux, a extensão .o. Em Windows
é típico a extensão ser .obj. Os ficheiros objecto não são ainda
executáveis, pois tipicamente cada ficheiro objecto contêm código máquina
que recorre a ferramentas externas a esse ficheiro objecto.
Depois de gerados os ficheiros objecto, estes têm de ser fundidos num
único ficheiro executável. A fusão resolve as dependências entre os vários
ficheiro objecto e entre estes e colecções de ficheiros objecto arquivados
em bibliotecas. As bibliotecas, em Linux, têm a extensão .a (de
archive), quando estáticas, ou .so, quando dinâmicas. Em Windows as
extensões correspondentes são .lib e .dll. O resultado de uma fusão
com sucesso é um ficheiro executável. Em Linux os ficheiros executáveis
podem ser qualquer extensão. Em Windows, a extensão de um ficheiro
executável é .exe.
É usual os ficheiros .c, ou seja, os ficheiros que são pré-processados, se
iniciem com a inclusão de um conjunto de ficheiros de cabeçalho padrão.
Cada uma destas inclusões é realizada pelo pré-processador quando encontra a
directiva de pré-processamento #include. Esta directiva é obrigatoriamente
seguida do nome do ficheiro de cabeçalho a incluir. Este nome coloca-se entre
parênteses agudos (<>) quando os ficheiros a incluir se encontram nas
pastas de ficheiros de cabeçalho padrão e entre aspas ("") quando estes se
encontram na mesma pasta do ficheiro .c em pré-processamento.
Os ficheiros de cabeçalho, também conhecidos por ficheiros de interface, contêm sobretudo declarações e, ocasionalmente, também definições, necessárias para que o código presente no ficheiro que os inclui possa recorrer a ferramentas definidas noutros locais. A diferença entre declaração e definição é subtil. Geralmente, uma declaração limita-se a dar a conhecer ao leitor ou ao compilador a existência de uma dada entidade (e.g., uma rotina ou uma estrutura), sem a definir. Uma definição é sempre também uma declaração, mas não se limita a declarar a existência de algo: define-o de forma completa. As declarações estão, por isso, associadas às interfaces das ferramentas declaradas e a definições à sua implementação.
Neste caso particular incluímos dois dos ficheiros de cabeçalho da biblioteca padrão do C, i.e., da biblioteca que acompanha a linguagem C tal como definida pela sua norma:
stdio.h – Declara os procedimentos printf() e scanf(), usados
respectivamente para escrever no ecrã (ou melhor, no canal de saída) e
para ler do teclado (ou melhor, do canal de entrada).
stdlib.h – Define a macro EXIT_SUCCESS, que é usada para
assinalar sucesso na execução de um programa, quando este termina, ou
seja, quando se sai do programa.
#include <stdio.h>
#include <stdlib.h>
factorial()Em C é possível «empacotar» código para o poder usar em vários locais. Ao
código assim «empacotado» chama-se um rotina. As rotinas em C podem ser
funções, se não alterarem o mundo, limitando-se a resultar num valor, ou
procedimentos, se agirem sobre algum aspecto do programa ou do mundo que o
rodeia, provocando potencialmente alterações. Neste caso estamos a definir
uma função chamada factorial().
A interface de uma rotina diz como a rotina se usa e o que a rotina faz ou calcula. Pelo contrário, a implementação de uma rotina especifica como a rotina funciona. A definição de uma rotina, seja ela função ou procedimento, consiste numa interface, que corresponde ao chamado cabeçalho da rotina, e numa implementação, que corresponde ao chamado corpo dessa mesma rotina.
factorial()O cabeçalho de uma rotina é parte da sua interface. Ou seja, daquilo que é necessário conhecer para a conseguir utilizar. O cabeçalho de uma rotina indica:
O seu nome – Neste caso o nome é factorial, pois é suposto esta
função calcular e devolver o factorial do valor que lhe for passado como
argumento.
A lista dos seus parâmetros, incluindo o tipo e o nome de cada uma desses parâmetros – Os parâmetros são variáveis locais (i.e., visíveis apenas num contexto local, e.g., no corpo de uma rotina), automáticas (i.e., construídas ao entrar no respectivo contexto e destruídas ao dele sair), que têm a particularidade de ser inicializadas com o valor do argumento correspondente sempre que a rotina é executada como resultado de uma sua invocação.
O tipo do valor devolvido quando a rotina retorna – As funções (bem como alguns procedimentos) são invocadas para se obter um dado valor como resultado. Esse valor é calculado pela rotina e devolvido quando a rotina terminar, i.e., quando se retorna ao local onde essa rotina foi invocada.
A função factorial() recebe um int como argumento. Esse int é guardado
dentro do parâmetro n, que, aliás, é uma constante, pois o qualificador
const altera a natureza de n, que deixa de ser uma variável e se torna
constante. É muito importante perceber a diferença entre um argumento, que
é uma expressão ou valor passado quando se invoca uma rotina, e um
parâmetro, que é uma variável ou constante especial que, no início da
execução da rotina, é inicializada automaticamente com o valor do argumento
passado aquando da invocação da mesma rotina. Mais abaixo se verão invocações
desta função.
Quando a função factorial() termina, altura em que o fluxo de execução
retorna ao ponto de invocação, devolve um valor. Esse valor é o factorial
do valor recebido como argumento, e é do tipo int, neste caso. Isso é
indicado pela presença da palavra-chave int antes do nome da função no
cabeçalho da definição da função.
int factorial(const int n)
factorial()O corpo de uma rotina contém a sua implementação. Em C, o corpo de uma rotina consiste sempre num bloco de instruções, ou seja, numa sequência de instruções envoltas por chavetas.
A implementação que usamos para a função factorial() nasce numa definição
recursiva da noção de factorial.
O factorial de um número representa-se por
e corresponde ao produto de todos os números
inteiros entre 0 e
, se
, tendo, por definição, o valor 1 quando
. Ou seja, se
,
. É fácil concluir daqui que
, desde que
.
Ou seja,
A implementação é consequência directa desta definição.
Note que esta função não faz qualquer esforço para lidar com valores
inválidos do seu argumento. Se se passar à função um valor negativo ou um
valor cujo factorial seja demasiado grande para representar num int, a
função pura e simplesmente não funciona. Isto é um erro de programação,
naturalmente. Uma forma de resolver este erro seria:
Incluir no topo deste ficheiro, para além dos ficheiros de cabeçalho já
incluídos, o ficheiro de cabeçalho assert.h.
Alterar o corpo da função se modo a fazer a verificação da validade do seu
argumento usando uma rotina especial (na realidade uma macro do
pré-processador) chamada assert():
{
assert(0 <= n && n <= 12);
if (n == 0)
return 1;
return n * factorial(n - 1);
}
Note que o limite superior para o valor do argumento é muito baixo: 12. Isso deve-se a dois factos:
Os int em C, nos nossos ambientes típicos, são guardados em apenas 32
bit interpretados em complemento para dois, pelo que um inteiro só pode
tomar valores entre e
, ou seja, entre
-2 147 483 648 e 2 147 483 647.
O factorial é uma função de crescimento muito rápido, pelo que 13!, com o
valor 6 227 020 800, excede já o limite superior dos
int.
{
Começamos por verificar se se aplica o caso especial em que o valor de n é
zero.
if (n == 0)
Se se aplicar, retorna-se imediatamente (i.e., termina-se a função, retornando ao ponto de invocação) devolvendo o valor 1.
return 1;
No caso contrário, retorna-se devolvendo o produto entre o valor de n e o
valor devolvido por... uma nova invocação da mesma função, embora com um
valor mais pequeno passado como argumento, ou seja, n - 1 em vez de n. A
uma invocação deste tipo chama-se uma invocação recursiva. Note-se que o
funcionamento desta função decorre do facto de, cada vez que a função é
invocada, as respectivas variáveis locais serem construídas na pilha (uma
zona especial da memória de um processo em execução). Isso significa que,
durante a execução da função decorrente de uma invocação factorial(4), por
exemplo, chega a haver cinco instâncias ou versões do parâmetro n em
existência em simultâneo.
return n * factorial(n - 1);
}
main()Todos os programas escritos em C têm de definir um procedimento main(),
i.e., com o nome main. A execução de um programa escrito em C começa sempre
com a invocação dessa rotina, que por isso é o ponto de entrada no programa.
O nosso programa tem como objectivo calcular o factorial de um valor introduzido pelo utilizador no teclado. Um possível exemplo de interacção do programa com o utilizador é o seguinte:
mises:hello_world mmsequeira$ bin/Debug/hello_world
Hello world! Let's calculate the factorial of a number.
Please enter a non-negative integer: 10
10! = 3628800
Tal como referido anteriormente, este programa está errado, pois não faz qualquer tentativa de lidar com erros do utilizador. Por exemplo, se o utilizador não introduzir um número, mas sim o seu nome, o resultado é inesperado:
mises:hello_world mmsequeira$ bin/Debug/hello_world
Hello world! Let's calculate the factorial of a number.
Please enter a non-negative integer: Manuel
32767! = 0
Note que o valor 32767 nada tem de especial. Quando uma leitura falha, é usual que a variável na qual se guardaria o valor lido seja deixada sem qualquer alteração. Não tendo sido inicializada, o valor que contém é indefinido, no sentido em que pode ser qualquer valor inteiro. O facto de o valor ter sido 32767 reflecte apenas o ambiente particular de compilação e execução do programa usado ao gerar esta documentação.
Também ocorre um erro, embora de outro tipo, se o utilizador introduzir um valor negativo:
mises:hello_world mmsequeira$ bin/Debug/hello_world
Hello world! Let's calculate the factorial of a number.
Please enter a non-negative integer: -1
Segmentation fault: 11
Neste caso o erro ocorre porque, partindo de um valor negativo, só se atingiria o caso especial da definição recursiva, i.e., o caso em que o argumento da função é 0, depois de passar por todos os possíveis valores negativos e positivos dos inteiros (quando se subtrai um ao mais pequeno dos inteiros obtém-se o maior dos inteiros, por estranho que pareça). Trata-se de um número demasiado grande de execuções recursivas todas em curso, pelo que o espaço de memória, chamado «pilha» (stack), reservado pelo sistema operativo para guardar informação sobre as rotinas em execução, incluindo as respectivas variáveis locais, acaba por se esgotar, levando o programa a abortar.
Finalmente, outro erro deste programa é não fazer qualquer esforço para verificar se o valor máximo dos valores do tipo int é ultrapassado durante o cálculo do factorial. Isso leva, mais uma vez, a resultados inesperados, mesmo perante respostas aparentemente válidas do utilizador:
mises:hello_world mmsequeira$ bin/Debug/hello_world
Hello world! Let's calculate the factorial of a number.
Please enter a non-negative integer: 17
17! = -288522240
Em todos estes casos, mesmo nos dois primeiros, trata-se de erros de programação: um programa tem de saber lidar com os seus próprios limites e com quaisquer erros do utilizador. Se não o fizer, está errado. Ou seja, este programa está errado, pois optámos por não fazer qualquer verificação de erros, de modo a mantê-lo simples.
Uma versão correcta do código é apresentada mais abaixo, com alguns comentários.
Neste caso declaramos a rotina como não possuindo qualquer parâmetro: daí a
palavra-chave void que colocámos entre parênteses após o nome da rotina.
(Note- se, o entanto, que poderíamos ter declarado a rotina de forma
diferente, de modo a poder receber argumentos vindos da linha de comandos.)
Declaramos também a rotina como devolvendo um valor do tipo int. Este tipo
de devolução é obrigatório, no caso da rotina main(). O valor devolvido
pela rotina quando retorna é usado pela entidade que executou o programa para
saber se a execução teve ou não sucesso.
int main(void)
{
Começamos por escrever no ecrã o nome e o propósito do programa. Para
isso, invocamos o procedimento printf() passando-lhe como argumento
uma cadeia de caracteres, neste caso uma cadeia de caracteres
literal, pois usamos a notação do C das cadeias de caracteres
literais. Ou seja, uma sequência de caracteres escritos entre aspas.
O único caso especial aqui é o da sequência de caracteres \n.
Quando o C encontra o caractere \ numa cadeia de caracteres, não o
interpreta literalmente: escapa para um modo de interpretação
diferente, em que os caracteres que se sequem ao \ têm um
significado especial. Neste caso usamos a sequência \n, que
significa o caractere fim-de-linha. Este caractere termina a linha
corrente e dá início a uma nova linha de caracteres no ecrã.
printf("Hello world! Let's calculate the factorial of a number.\n");
Uma vez que o objectivo do programa é calcular o factorial de um número, começamos por pedir ao utilizador para introduzir um número inteiro não negativo. Note que neste caso não terminamos a linha, pois pretendemos que a resposta do utilizador surja visualmente à frente do texto que escrevemos. Em vez disso, escrevemos um espaço, para que o número introduzido pelo utilizador não surja «colado» ao nosso texto. Note ainda que a leitura dos caracteres introduzidos pelo utilizador só é realizada depois de este carregar em «enter», o que terá o efeito visual de terminar esta linha.
printf("Please enter a non-negative integer: ");
Antes de proceder à leitura propriamente dita, temos de definir uma
variável para guardar o valor introduzido pelo utilizador. Chamamos a
essa variável number e atribuímos-lhe o tipo int, compatível com
o parâmetro de entrada da função factorial() já definida. A
definição de uma variável em C começa sempre por um tipo. Nos casos
mais simples, esse tipo é seguido do nome da variável em definição,
tal como fazemos aqui. Este formato de definição é exactamente o
mesmo que usámos no cabeçalho da função factorial(), aliás.
int number;
Para proceder à leitura do número introduzido pelo utilizador
invocamos o procedimento scanf(), passando-lhe dois argumentos. O
primeiro é uma cadeia de caracteres literal a que se chama formato
e que contém uma chamada especificação de conversão, iniciada pelo
caractere &. A leitura é controlada por esta cadeia. Quando o
procedimento encontra a especificação de conversão %d, procede à
leitura dos próximos caracteres da entrada admitindo que estes
correspondem à representação decimal usual de um número inteiro.
scanf("%d", &number);
Finalmente, podemos calcular o factorial pretendido e escrever o
resultado no ecrã. Para isso invocamos o procedimento printf()
passando-lhe três argumentos.
O primeiro argumento, "%d! = %d\n", é uma cadeia de caracteres de
formação. Esta cadeia da caracteres é interpretada pelo procedimento
printf(), que imprime directamente no ecrã os caracteres da cadeia
de formação, excepto quando encontra especificações de formatação
que, tal como no caso do procedimento scanf(), são introduzidas
pelo caractere %. Neste caso temos duas especificações de
formatação, ambas %d.
Quando o procedimento printf() encontra a primeira especificação de
formatação, transforma o valor do segundo argumento, que tem de ser
um int, uma vez que após o caractere % se encontra o caractere
d, numa sequência de caracteres e escreve-os no ecrã. Isso leva a
que o valor da variável number seja escrito no ecrã, na notação
decimal usual. Quando o procedimento printf() encontra a segunda
especificação de formatação, transforma o valor do terceiro argumento
numa sequência de caracteres e escreve-os no ecrã. Isso leva a que o
valor devolvido pela função factorial() quando invocada com o
argumento number seja escrito no ecrã, na notação decimal usual.
Por exemplo, admitindo que o valor de number é 5, então esta
instrução escreve no ecrã a seguinte linha:
5! = 120
printf("%d! = %d\n", number, factorial(number));
Terminada a impressão do factorial calculado, podemos terminar o
programa retornando da execução da rotina main(). Uma vez que a
execução teve sucesso (ou pelo menos admite-se que sim...),
devolvemos o valor especial EXIT_SUCCESS ao retornar.
return EXIT_SUCCESS;
}
O código tal como está tem erros, como vimos. Uma versão completa (e
complexa!) do código com todos os erros corrigidos (esperamos!) é a seguinte
(ver também hello_world_correct.c):
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>
#include <assert.h>
int factorial(const int n)
{
assert(0 <= n && n <= 12);
if (n == 0)
return 1;
return n * factorial(n - 1);
}
int main(void)
{
printf("Hello world! Let's calculate the factorial of a number.\n");
int number;
while(true) {
printf("Please enter a non-negative integer "
"between 0 and 12: ");
if (scanf("%d", &number) == 1 && 0 <= number && number <= 12)
break;
if (feof(stdin) || ferror(stdin)) {
fprintf(stderr,
"Unrecoverable error reading user response. "
"Exiting.\n");
return EXIT_FAILURE;
}
printf("Invalid response. Try again.\n");
scanf("%*[^\n]%*1[\n]");
}
printf("%d! = %d\n", number, factorial(number));
return EXIT_SUCCESS;
}
Alguns comentários:
É um bom exercício sobre a linguagem C tentar compreender este código do princípio ao fim.
A inclusão de stdbool.h deve-se à necessidade de usar o valor booleano
true como guarda do ciclo while.
A leitura do valor do teclado é feita dentro de um ciclo que permite ao utilizador corrigir os seus próprios erros.
O ciclo usado é um ciclo infinito que, ao contrário do que o nome
sugere, termina logo que o utilizador introduza um valor válido. A utilização
de um ciclo infinito while(true) deve-se ao facto de as condições de
paragem do ciclo se encontrarem a meio do seu passo.
O ciclo tem duas condições de paragem. A primeira ocorre quando a leitura
tem sucesso e o valor lido é válido. A função scanf() devolve o número de
atribuições a itens de entrada realizadas com sucesso. No nosso caso podemos
saber se foi lido um valor inteiro com sucesso verificando se o valor
devolvido foi 1. Para saber se o valor lido é válido, verificamos se se
encontra entre 0 e 12. Caso tudo tenha corrido bem, o ciclo é interrompido
através da instrução break.
A segunda condição de paragem ocorre quando, depois de termos tentado ler
e de não termos tido sucesso, verificamos que o canal de leitura stdin
ficou marcado como tendo sido atingido o fim do ficheiro (a entrada de um
programa pode ser redireccionada de modo a ser feita a partir de um ficheiro)
ou tendo ocorrido um outro erro de leitura. Nesse caso terminamos o programa
devolvendo o valor especial EXIT_FAILURE para comunicar este erro à
entidade que executou o programa, isto depois de escrevermos uma mensagem de
erro apropriada no canal de escrita de erros stderr.
Quando detectamos um erro de leitura não fatal, avisamos o utilizador do problema e, depois de descartar a linha errónea introduzida pelo utilizador, repetimos o passo do ciclo.
Para descartar uma linha completa de caracteres, invocamos o procedimento
scanf() com a cadeia de formação %*[^\n]%*1[\n]. O significado é o
seguinte:
Primeira especificação de conversão, %*[^\n]: Ler e descartar (daí o
caractere *) todos os caracteres até o próximo fim-de-linha, exclusive.
Segunda especificação de conversão, %*1[\n]: Ler e descartar (daí o
caractere *) exactamente um caractere fim-de-linha. Colocar \n na
cadeia de formatação em vez desta especificação de conversão não teria o
resultado pretendido, pois o procedimento scanf() interpreta todos os
caracteres «brancos» como uma ordem para ignorar um número arbitrário de
caracteres brancos até ser encontrado o primeiro caractere não branco, o
que levaria a que não surgisse novo pedido de inserção de um número
válido enquanto o utilizador fosse carregando em «enter».
Se tudo isto lhe pareceu complexo, não se preocupe: é mesmo complexo. :-)