Loading...
Loading...
Loading...
Loading...
Loading...
Loading...
O resto da definição da função é chamado de corpo da função. Como qualquer corpo de função, este inicia com um {
, termina com um }
e contém zero ou mais comandos e declarações. Comandos especificam ações que o programa deve tomar. Declarações definem nomes de variáveis, funções, etc. Cada comando e cada declaração termina com um ponto-e-vírgula (;
).
Comandos e declarações frequentemente contém expressões; uma expressão é uma construção cuja execução produz um valor de algum tipo, mas também pode causar ações por meio de "efeitos colaterais" que alteram a execução subsequente. Diferentemente, um comando, não tem um valor; ele afeta a execução do programa somente através das ações que ele gera.
O corpo desta função fib
não contém declarações e contém somente um comando, mas este é um comando complexo uma vez que ele contém comandos aninhados. Essa função utiliza dois tipos de comandos:
return
O comando return
faz a função retornar imediatamente. Ele normalmente aparece assim:
Seu objetivo é computar o valor da expressão e sair da função, fazendo-a retornar o valor da expressão produzida. Por exemplo:
faz a função retornar o inteiro 1 e
faz a função retornar um valor computado ao fazer a soma dos resultados e duas chamadas de função, como especificado.
if…else
O comando if
…else
é um condicional. Sempre que ele executa, ele escolhe um dos seus dois sub-comandos para executar e ignora o outro. Veja:
O que ele faz é computar a expressão condicional e, se der "verdadeiro", ele executa o comando-se-verdadeiro. Do contrário, executa o comando-se-falso. Veja .
Dentro do comando if
…else
, condicional é simplesmente uma expressão. Ela é considerada "verdadeira" se seu valor for diferente de zero. (Uma operação de comparação como em n <= 2
, produz o valor 1 se for "verdadeiro" e 0 se for "falso.” Veja .) Portanto,
primeiro testa se o valor de n
é menor ou igual a 2. Se sim, a expressão n <= 2
tem valor 1. Daí a execução continua com o comando
Do contrário, a execução continua com o comando:
Cada um desses comandos encerra a execução da função e provê um valor para que ela retorne. Veja Comando return.
Calcular fib(n)
, que utiliza inteiros, funciona apenas quando n < 47
porque o resultado de fib (47)
é muito grande para caber num tipo int
. A operação de adição ao tentar somar fib (46)
e fib (45)
não consegue produzir o resultado correto. Isto é chamado de estouro de inteiro (integer overflow).
Estouros podem se manifestar de várias maneiras, mas uma coisa que eles não fazem é produzir o resultado correto já que este não cabe no espaço reservado para o valor. Veja .
Veja para uma explicação completa sobre funções.
Em nosso exemplo, as primeiras duas linhas da definição da função consistem no cabeçalho. Seu propósito é estabelecer o nome da função e dizer como esta deve ser chamada:
O cabeçalho acima diz que a função retorna um inteiro (tipo int
), que seu nome é fib
e que ela recebe um argumento chamado n
que também é um inteiro. (Tipos de dados serão explicados mais tarde, em .)
Para introduzir os conceitos mais básicos de C, vamos ver um código que implementa uma função matemática simples que faz cálculos com números inteiros. Esta função calcula o enésimo número na série de Fibonnaci, na qual cada número é a soma dos dois números anteriores: 1, 1, 2, 3, 5, 8, 13, 21, 34, 55, ….
Este simples programa ilustra vários recursos da linguagem C:
A definição de uma função cujas primeiras duas linhas constituem no cabeçalho da função. Veja Definição de Funções.
Um parâmetro de função n
, referenciado com a variável n
dentro do corpo da função. Veja Variáveis de Parâmetros de Função. Uma definição de função usa parâmetros para se referir aos valores dos argumentos passados para ela numa chamada de função.
Aritmética. Programas em C somam com +
e subtraem com -
. Veja .
Comparações numéricas. O operador <=
testa se é "menor ou igual". Veja .
Constantes inteiras escritas na base 10. Veja Constantes Inteiras.
Uma chamada de função. A chamada de função fib (n - 1)
chama a função fib
passando o valor n - 1
como seu argumento. Veja Chamadas de Função.
Um comentário que começa com /*
e termina com */
. O comentário não tem nenhum efeito na execução do programa. Seu propósito é prover explicações para pessoas lendo o código-fonte. Incluir comentários no código é tremendamente importante - eles adicionam contexto de forma que outras pessoas possam entender o código mais rapidamente. Veja .
Dois tipos de comandos, o return
e o if...else
. Veja Comandos.
Recursão. A função fib
chama a si mesma; isto é chamado de chamada recursiva. Isto é válido em C e bastante comum.
A função fib
não seria útil se ela não retornasse. Portanto, funções recursivas, para serem úteis, precisam evitar recursão infinita.
Esta definição de função previne a recursão infinita ao tratar o caso especial onde n
é dois ou menos. Portanto a máxima profundeza de chamadas recursivas é menor que n
.
Cabeçalho da Função O nome da função e como ela é chamada.
Corpo da Função Declarações que implementam a função.
Este capítulo apresenta o código-fonte para um programa em C muito simples e o utiliza para explicar alguns recursos da linguagem. Se você já sabe os pontos básicos de C apresentados neste capítulo, você pode só dar uma passada por ele ou pulá-lo.
Apresentamos exemplos de código C usando uma fonte de largura fixa já que esta é a maneira que eles aparecem quando você os edita num editor de textos como o GNU Emacs.
The Stack, And Stack Overflow
Recursão tem uma desvantagem: há um limite no número de níveis aninhados de chamadas de função que um programa pode fazer. Em C, cada chamada de função aloca um bloco de memória que é utilizado até que a chamada retorne. A linguagem C aloca estes blocos consecutivamente numa grande área de memória conhecida como pilha (stack), portanto chamamos este blocos de quadros de pilha (stack frames).
O tamanho da pilha é limitado; se um programa tenta usar muito dela, isso causa uma falha porque a pilha estará cheia. Isso é chamado de estouro de pilha (stack overflow).
Estouros de pilha no GNU/Linux se manifestam tipicamente como o sinal chamado de SIGSEGV
, também conhecido como "falha de segmentação" ("segmentation fault"). Por padrão, este sinal encerra a execução do programa imediatamente ao invés de permitir que o programa se recupere ou atinja seu fim esperado. (Nestes casos, nós normalmente falamos que o programa "crashou"). Veja .
Aqui me rendi ao neologismo "crashou", oriundo do inglês "crash". Até pensei em usar "travou", mas este termo nós brasileiros normalmente utilizamos quando o programa para de responder ("congela"), mas numa situação de estouro de pilha, o programa é encerrado e não fica "congelado" ou "travado".
Não é conveniente tentar observar um estouro de pilha passando um número grande como argumento para uma função recursiva que implemente Fibonacci porque o programa rodaria por muito tempo antes de dar erro. O algoritmo é simples, mas lentíssimo: ao calcular fib (n)
, o número de chamadas (recursivas) fib (1)
ou fib (2)
que ele vai fazer é igual ao resultado final.
Todavia, você pode rapidamente observar um estouro de pilha usando seguinte função:
Com um laptop Lemote Yeeloong rodando o sistema operacional gNewSense GNU/Linux com configuração padrão, um experimento demonstrou que há espaço suficiente na pilha para realizar 261906 chamadas aninhadas para essa função. Uma mais e a pilha estoura e o programa é encerrado. Em outra plataforma com uma configuração diferente ou com uma função diferente, o limite pode ser maior ou menor.
Em meus testes com o Windows 11 e Visual Studio 2022, a pilha estourou a partir de 4023 chamadas, ou seja, com fill_stack(4024)
.
Aqui vai um algoritmo muito mais rápido para computar a mesma sequência de Fibonacci. Ele é mais rápido por duas razões. Primeiro, ele usa iteração (isto é, repetição) ao invés de recursão, então ele não consome o tempo de um número alto de chamadas de função, mas principalmente, ele é mais rápido porque o número de repetições é pequeno - somente n
.
Essa definição calcula fib (n)
num tempo proporcional à n
. Os comentários na definição explicam como ela funciona: ela avança através da série sempre mantendo os últimos dois valores em ultimo
e anterior
, e os soma para obter o próximo valor.
Aqui vão os recursos adicionais da linguagem C que essa definição utiliza:
Dentro de uma função, onde um comando é chamado, você pode escrever um bloco. Seu formato é { ... }
e ele contém zero ou mais comandos e declarações. (Você também pode utilizar blocos adicionais como comandos dentro de um bloco.)
O corpo da função também contém um bloco, que é a razão deste conter comandos e declarações.
Veja .
O corpo da função contém tanto declarações quanto comandos. Há três declarações diretamente no corpo da função, assim como uma quarta declaração num bloco interno. Cada declaração começa com int
porque isso declara uma variável cujo tipo é inteiro. Uma declaração pode declarar várias variáveis, mas cada uma dessas declara somente uma variável.
Variáveis declaradas dentro de um bloco (seja ele o que define o corpo de uma função ou um bloco interno) são variáveis locais. Estas variáveis existem somente dentro daquele bloco; seus nomes não estão definidos fora daquele bloco e sair do bloco desaloca a memória ocupada por elas. Este exemplo declara quatro variáveis locais: ultimo
, anterior
, i
e proximo
.
A declaração de variável local mais básica é assim:
Por exemplo,
declara a variável local i
como um inteiro. Veja .
Quando você declara uma variável, você também pode especificar seu valor inicial, assim:
Por exemplo,
declara a variável local ultimo
como inteiro (tipo int
) e a inicializa com o valor 1. Veja Inicializadores.
Atribuição: um tipo de expressão específica, escrita com o operador =
, que armazena um novo valor numa variável ou em outro lugar. Portanto,
é uma expressão que computa valor
e armazena o valor na variável
. Veja .
Um comando de expressão é uma expressão seguida de um ponto-e-vírgula. Isso computa o valor da expressão e na sequência ignora este valor.
Um comando de expressão é útil quando a expressão muda algum dado ou tem outros efeitos colaterais — por exemplo, com chamadas de função ou com atribuições como neste exemplo. Veja .
Não faz sentido usar uma expresão sem efeitos colaterais num comando de expressão, exceto em casos muito especiais. Por exemplo, o comando de expressão x;
examinaria o valor de x
e o ignoraria.
O operador de incremento é o ++
. A expressão ++i
é uma forma abreviada de i = i + 1
. Veja .
for
O comando for
é uma maneira clara de executar um comando repetidamente—um laço (ver Comandos de Laço). Especificamente,
começa fazendo i = 1
(põe o um em i
) para preparar o laço. O laço em si consiste em
Testar i < n
e sair do laço se isso for falso.
Executar corpo.
Avançar o laço (executar ++i
, que incrementa i
).
O resultado é executar corpo com 1 em i
, então com 2 em i
e assim sucessivamente, parando imediatamente antes da repetição quando i
for igual a n
. Se n
é menor que 1, o laço vai executar o corpo zero vezes.
O corpo do laço for
precisa ser um e somente um comando. Você não pode escrever dois comandos numa mesma linha nele; se você tentar, somente o primeiro deles será tratado como parte do laço.
A forma de colocar múltiplos comandos no laço for
é agrupá-los como um bloco e é isso que fazemos neste exemplo.