Aulas de C

Aprendizado continuo. Linguagem antiga e moderna

Funções – Parte 1

Olá a todos!
Bem, agora já estamos começando a pensar em programas nós mesmos, não é?

Agora, vamos pensar um pouco no programas que fizemos lá atrás, quando falamos de Entrada de Dados e Variáveis. Aquele foi um programa razoavelmente grande. Agora imagine que você crie um programa realmente complexo, que realize atividades similares em diversos pontos do mesmo. Se você escrever esse programa como criamos o programa de exemplo de Entrada de Dados e Variáveis, você teria um grande programa com vários pontos repetidos. Desse modo, caso precisasse alterar o modo como essas atividades similares seriam executadas, você teria que mexer em vários pontos similiares, o que mesmo o melhor dos programadores não conseguirá com facilidade e sem a possibilidade de provocar erros.
Por isso, o C (como toda boa linguagem de programação) prevê formas de dividir o programa em “pedaços” que executem a mesma tarefa. Chamamos esses pedaços de funções.
Na realidade, já usamos muitas funções até aqui. Todo comando que mostramos até agora, à exceção de palavras chaves como if ou do…while, são funções. A vantagem de dividir-se o programa em funções é que podemos isolar determinadas atividades nelas, o que permite:
  1. Programas escritos de maneira mais legível;
  2. Melhor manutenção do código, em especial em projetos complexos; você foca só no que está dando errado e uma vez que tudo esteja OK as melhorias se refletem apenas no que está dando errado;
  3. Reutilização de código: por meio das funções podemos criar bibliotecas de funções (lembra do que falamos anteriormente sobre isso?) que englobem funções que usamos constantemente em um (ou mesmo em vários programas) e com isso reaproveitar esse código em muitos casos;

Claro que uma função deve ser criada para ser suficientemente genérica, mas feito isso ela pode ser aproveitada nos mais diversos momentos.
Bem, dito essa teoria, vamos ao nosso programa exemplo: um programa de médias escolares.

#include <stdio.h>

/**
* Essa função de média irá acrescentar os valores adicionados à média
* anterior e devolverá a média no momento
*/
float media(float nota)
{
   static float mediaAtual=0.0;
   mediaAtual=(mediaAtual==0)?nota:(mediaAtual+nota)/2;
   return mediaAtual;
}

int main(void)
{
    float nota=0.0;
    int notas=0;

    do
    {
        printf(“Digite a próxima nota ou -1 para sair:”);
        scanf(“%f”,&nota);

        if (nota!=-1)
        {
             notas++;
            printf(“Com essa nota, a média total é de %9.2f\n”,media(nota));
        }

    } while (nota!=-1);

    return 0;
}

Na parte do main() fica a ressalva de que mudamos um pouco o início: de int main (int argc, char** argv), estamos usando int main(void). Como dissemos no Hello World, essa construção (sobre a qual aproveitaremos para falar a seguir) pode ser mudada, embora o compilador possa gerar um aviso de que você está fugindo do padrão do C. No nosso caso, colocamos void nos parâmetros para indicar que não receberemos parâmetros (void é uma palavra reservada do C que indica algo “vazio” – veremos mais sobre isso adiante), ou melhor, que não utilizaremos parâmetros que sejam passados. De resto, o nosso main() engloba coisas que já vimos nas últimas semanas e que você deve estar afiado caso tenha seguido as sugestões que fizemos para mexer no código e compreendido o que fizemos até agora. Na verdade, tem algo que iremos falar, mas apenas depois que vermos nossa função:

float media(float nota)
{

   static float mediaAtual=0.0;

   mediaAtual=(mediaAtual==0)?nota:(mediaAtual+nota)/2;

   return mediaAtual;

}

Já falamos anteriormente sobre o conceito de blocos de código. Basicamente, um bloco de código é uma parte do programa que é isolada logicamente e considerada como um comando único. Para “isolar-se” um bloco de código em C, usa-se as chaves ({}). De maneira “rápida e suja”, podemos definir uma função como um “bloco de código com nome”. Na realidade, uma função pode estar em um outro ponto do programa ou até mesmo em um arquivo totalmente isolado. A única regra para uma função é que ela tem que vir de alguma forma “antes” de qualquer ponto do programa onde ele seja usado (na próxima semana, quando encerrarmos o assunto Funções, veremos que não é bem assim e existem técnicas simples que permitem ao programador colocar sua função onde deseejar).
Uma função na realidade é caracterizada por realizar alguma tarefa e retornar algum valor. Para facilitar a vida do programador e desobrigá-lo de saber o que a função realmente faz, toda linguagem de programação parte do princípio de que uma função é uma “caixa preta”: você coloca determinados parâmetros na entrada da mesma, ele realiza algum processamento (que o programador não precisa realmente saber do que se trata) e devolve ao usuário alguma saída. Porém, embora seja uma “caixa preta”, é sempre necessário a uma função indicar o que ela espera receber de parâmetros para trabalhar e o que o usuário irá receber de volta. No C isso é feito no momento em que se nomeia a função.
Como dissemos acima, podemos pensar em uma função como um “bloco de código com nome”. Em C, chamamos o “nome da função” de protótipo ou assinatura (esse último é mais usado em documentos focando C++ e tem a ver com certas propriedades da mesma). O protótipo de uma função costuma seguir o formato:

retorno nome (tipoPar1 par1[=init1],tipoPar2 par2[=init2],…,tipoParN parN[=initN])

Onde:

  • retorno indica o tipo de varíavel que a função retorna. Esse tipo pode ser qualquer tipo básico do C, ponteiros (veremos isso quando entrarmos nesse assunto) ou void: void pode ser entendido como um nulo, ou seja, quando C executar uma função cujo retorno seja void ele não deve esperar nenhum retorno do mesmo (na realidade, alguns compiladores costumam provocar erros de compilação);
  • nome é o nome pelo qual a função é chamada. Os nomes de função seguem as mesmas regras que vimos quanto aos nomes de variável que vimos quando falamos de Entrada de Dados e Variáveis.  Além disso, não podem ter o mesmo nome de funções que tenham sido importadas por meio de #includes. Uma coisa: mesmo para as funções da biblioteca padrão vale a mesma regra. Por exemplo: se eu não importar a stdio.h, posso incluir minha própria versão de printf sem problemas. Veremos o motivo disso adiante;
  • Dentro dos parênteses incluímos uma série de indicações sobre os parâmetros da função, tipoPar1 par1[=init1],tipoPar2 par2[=init2],…,tipoParN parN[=initN]. Elas são distrinchadas assim: tipoPar é o tipo da variável em questão, que pode ser de qualquer tipo básico do C, tipos do usuário ou ponteiros (veremos os dois últimos no futuro). A ele pode ser agregado um modificador const, que indica que, não importa o que aconteça, esse valor não deve ser modificado (esse modificador só é útil quando usamos ponteiros – explicaremos o porque quando alcançarmos esse tópico); par é o nome do parâmetro. Opcionalmente, você tem init, que permite que você defina um valor default para inicialização, que será colocado caso esse parâmetro não seja passado (isso é feito ao deixar o espaço desse parâmetro vazio, sem nenhuma váriavel ou valor, mesmo null – para o compilador, passar null é passar um valor, ainda que nulo).
  • Uma ressalva sobre parâmetros: se você não esperar nenhum parâmetro em uma função, é uma boa prática inserir void dentro dos parênteses, ainda que parênteses em branco (()) seja igualmente suficiente para indicar uma função sem parâmetros. Essa boa prática ajuda na leitura do que a função faz e em muitas documentações você verá ela sendo adotada;

Bom, após vermos como é nomeada uma função, vamos ver o nome de nossa função e destrinchá-la:

float media(float nota)
  • Primeiro, indicamos que ela é uma função que irá devolver um valor de ponto flutuante (float);
  • Depois, informamos que o seu nome é media;
  • E na parte de parâmetros, indicamos que ela recebe apenas um parâmetro, do tipo flutuante e chamado nota (float nota). Também, pela ausência de um igual, indicamos que ela não tem um valor default. Portanto, a ausência desse parâmetro irá provocar um erro. Se tivéssemos indicado um default e não passássemos um valor, o compilador poderia devolver um alerta, mas ainda assim o programa iria compilar normalmente;

Escopo de varável e o modificador static:

Dito isso, vamos falar sobre o código. Na nossa primeira linha, temos uma declaração especial:
   static float mediaAtual=0.0;
Essa única linha vai nos levar a todo um tópico de explicações. Na realidade, para entendermos ela totalmente, precisaremos falar sobre escopo de variável e sobre como funciona a declaração de variáveis dentro de uma função.
Como já fizemos no main() quando falamos de Entrada de Dados e Variáveis, podemos declarar variáveis internas em uma função  Na realidade, você pode declarar variáveis dentro de qualquer bloco de código. Como uma função é um “bloco de código nomeado”, podemos definir variáveis dentro delas. Além dos parâmetros (que são variáveis dentro da função), podemos declarar quantas variáveis que acharmos necessárias. No caso, da mesma forma que uma variável no main() representa o local onde um conteúdo fica dentro do programa principal, uma variável dentro de uma função representa o local onde um conteúdo ficará armazenado dentro da função. Importante, porém, é notar que uma variável dentro de uma função pode ter um nome que já esteja sendo usado fora da função. Isso é possível pois, embora os nomes das variáveis sejam iguais, seus escopos são diferentes: uma vale para a função main() e outra para a função que o usuário criou, e o compilador, ao gerar o programa, tratará tais variáveis como variáveis diferentes e portanto com locais em memória diferentes. Existem algumas formas de mudar esse comportamento que veremos adiante.
Normalmente, ao chamar-se uma função, todas as suas variáveis são reinicializadas com os valores que o usuário definiu (ou com valores arbitrários, caso não o tenha feito), ou seja, podemos dizer que a cada chamada de uma função o valor de suas variáveis é resetado. Esse é o comportamento esperado, pois parte-se do princípio que cada chamada de uma função irá processaar valores não exatamente iguais.
Porém, existem casos onde podemos precisar que um ou mais valores permaneçam “salvos” entre chamadas de uma mesma função. Para garantir que isso aconteça, utilizamos um modificador na declaração da variável chamado static (estático). O que ele faz é garantir que a variável em questão tenha seu valor mantido entre as várias chamadas à função. Ou seja, após a função terminar sua execução, seu valor não é resetado como normalmente acontece. Uma situação onde isso pode ser útil é quando, por exemplo, desejamos saber o número de pessoas que executou uma determinada transação bancária: uma forma bruta seria colocar uma variável somando o número de requisições feitas à função de transação e depois definir uma forma de obter-se esse valor. (Embora normalmente só um valor possa ser retornado por função, existem “truques” que permitem obter-se mais de um valor – veremos tais “truques” quando falarmos sobre passagem de valor para funções, no próximo post).
No caso, voltando ao nosso programa, o que fazemos é declarar nossa variável mediaAtual como tipo flutuante (float) e estática (static), inicializando ela como 0.0 (zero flutuante). Essa inicialização será feita apenas na primeira chamada à função dentro do programa. Uma vez que essa chamada tenha se encerrado, na entrada seguinte o sistema irá manter o valor com o qual a variável encerrou a chamada anterior. Ou seja, caso o valor final de  mediaAtual seja 2, esse será o valor de mediaAtual na chamada seguinte.
Você deve estar se perguntando agora: “qual a diferença entre usar static e const em uma função?”. Aparentemente seria nenhuma, mas na verdade é ENORME:

  • const é usado quando você não quer que, durante a execução da função, o valor da varíavel seja alterado. Porém, uma vez que uma const é uma variável como outra qualquer até ser inicializada, ela também tem valores arbitrários preenchidos nela até ser inicializada (lembre-se que uma const só pode ser inicializada, ou seja, receber um valor, UMA ÚNICA VEZ). No caso de const, após o término da execução da função, o valor definido na inicialização é perdido e poderá receber um valor completamente diferente na próxima execução;
  • static deve ser usado quando você não quer que, entre execuções da função, o valor da variável em questão seja perdido. Dentro da função, durante a execução da mesma, você poderá alterar normalmente, como qualquer outra variável. Porém, seu valor não será “resetado” após o término da execução da função.
  • Aqui cabe dizer ainda que é possível criar-se uma variável static const. Esse “pequeno monstrinho” seria uma variável dentro de uma função cujo valor permaneceria sempre o mesmo entre todas as chamadas da mesma, definido na inicialização da variável na primeira chamada. CUIDADO: esse tipo de “monstrinho” pode gerar dor de cabeça séria na programação e normalmente não fará nada de mais interessante que não possa ser feito, por exemplo, com símbolos #define;

Bem, não temos muito o que dizer aqui mais sobre o escopo. Veremos um pouco mais sobre escopo de variáveis no futuro. Agora, vamos continuar analizando nosso código.

Retorno – a palavra chave return:

Continuando nosso código, após a declaração de variável, temos o seguinte código:

   mediaAtual=(mediaAtual==0)?nota:(mediaAtual+nota)/2;
   return mediaAtual;

A primeira linha representa uma atribuição condicional que vimos na última “aula” ao aprofundarmos operadores e lógica. No caso, quando mediaAtual for 0 (no caso, na primeira chamada de função), ele receberá o valor de nota (parâmetro passado pelo usuário). Caso contrário, irá manter em mediaAtual o valor médio entre mediaAtual e nota. Não há muito mistério aqui e, embora a construção possa ser confusa, é só ler com atenção que fica claro o que estamos fazendo. Em seguida, usamos a palavra chave return para devolver a main (que chamou essa função) o valor calculado.
Se lembrarmos bem, temos visto return desde nosso primeiro “Hello World”. Isso porque, como dissemos na época, mesmo main() é uma função para o C, ainda que especial. E a última coisa que qualquer função precisa fazer é devolver o controle da execução para a função que a chamou. Para isso, utilizamos return para indicar que terminamos de processar tudo o que devíamos e que o processador pode voltar para onde ele tinha parado antes de começar a executar nossa função.
“Como assim?”, você deve se perguntar. Bem, ao darmos início ao programa, o mesmo é carregado na memória por um processo de todo sistema operacional chamado loader e sua execução é iniciada. Conforme os comandos são executados, o sistema vai respondendo adequadamente, modificando espaços de memória (representados no programa pelas variáveis) e seguindo adiante de maneira sequencial (isso também considerando os comandos de controle de fluxo). Quando uma função é chamada, o programa principal passa o controle da execução para outro ponto completamente arbitrário dentro do espaço que o programa ocupa na memória e executa os comandos informados na função. Uma vez que termine, ele tem que devolver o controle para que o programa principal volte a ser executado, o que em C indicamos com return, e assim sucessivamente até que o programa chegue ao fim do programa principal e seja encerrado.
O que precisamos saber então sobre o funcionamento de uma função:

  1. Um programa pode requisitar a qualquer momento a execução de uma função;
  2. Funções são usadas para tornar o programa mais reutilizável, mais fácil de manter-se e mais legível;
  3. Ao requisitar a execução de uma função, o programa pode precisar passar parâmetros em uma ordem determinada, indicando o que a função precisa ter de entrada para trabalhar;
  4. A função irá trabalhar como foi estipulado pelo criador da função: para o programa, uma função atua como uma “caixa-preta”;
  5. A função devolve o controle de execução ao programa principal ao encerrar-se, devolvendo valores determinados pelo tipo de retorno da função como saída;

Uma coisa antes de encerrarmos: ao devolver dados para o programa principal, devolvemos ele segundo o tipo estipulado lá no protótipo da função. Se for necessário por qualquer motivo, a função pode fazer typecast do valor devolvido antes de o retornar. Porém, é adequado que não seja usado tal expediente: use uma varíavel do mesmo tipo de retorno estipulado no protótipo e faça as operações necessárias a usando e use ela como valor para return. Assim evitará dores de cabeças e bugs difíceis de depurar-se.
Bem, não temos mais o que falar sobre a função em questão, então vamos voltar ao programa principal.
No programa principal não há mistérios:

int main(void)
{
    float nota=0.0;
    int notas=0;

    do
    {
        printf(“Digite a próxima nota ou -1 para sair:”);
        scanf(“%f”,&nota);

        if (nota!=-1)
        {
             notas++;
            printf(“Com essa nota, a média total é de %9.2f\n”,media(nota));
        }
    } while (nota!=-1);

    printf(“Você inseriu %d notas.\n”, notas);
    return 0;
}

Inicializamos duas variáveis, uma float chamada nota (que receberá a nota a ser “adicionada” à média) e uma inteira chamada notas (que usamos como um contador do número de notas “adicionadas). Um laço do…while é usado para que novas notas sejam inseridas uma após a outra até que o usuário entre com -1 (que é considerado valor de saída).
A única coisa que precisamos aqui é a linha:
            printf(“Com essa nota, a média total é de %9.2f\n”,media(nota));
A pergunta é ‘essa é uma entrada válida’? A resposta é: SIM.
No nosso caso, o printf espera um valor de tipo flutuante (repare no formato %9.2f), o que é oferecido por nossa função media (que retorna um tipo flutuante). ‘E se o programa esperasse, por exemplo, um int, ou recebesse um int?’ No caso, ocorreriam typecasts, mas o C sempre fará o possível para oferecer um retorno ao usuário, não importa se os valores saiam espúrios (ele parte do princípio de que o programador mantenha seus dados de maneira correta, não fazendo muitas checagens).
Bem… Vamos encerrar por agora nessa semana. Na próxima, iremos falar mais sobre funções, incluindo uma aprofundada nos tipos de escopo de variáveis e algumas dicas úteis (e IMPORTANTÍSSIMAS) sobre funções e seus protótipos.
Para essa semana, umas brincadeiras:

  1. Renomeie mediaAtual para outros nomes de variáveis que ocorram e veja o que irá acontecer;
  2. Remova static da declaração de variável da função media;
  3. Tente reescrever media de modo que você não precise usar uma variável static nela;
  4. Escolha um segundo valor (por exemplo, -2) para “resetar” os valores de media;
  5. Tente imprimir o valor retornado por media como inteiro e veja o que acontecerá;
  6. Tente deslocar o código da função media para abaixo da função main e veja as mensagens de erro. Procure entender os motivos das mensagens;

Bem, até semana que vem, quando veremos mais sobre funções, praticamente “fechando” o assunto.

Uma resposta para “Funções – Parte 1

  1. Pingback: Sobre arquivos de código-fonte, arquivos de cabeçalhos e projetos « Aulas de C

Deixe uma resposta

Preencha os seus dados abaixo ou clique em um ícone para log in:

Logotipo do WordPress.com

Você está comentando utilizando sua conta WordPress.com. Sair / Alterar )

Imagem do Twitter

Você está comentando utilizando sua conta Twitter. Sair / Alterar )

Foto do Facebook

Você está comentando utilizando sua conta Facebook. Sair / Alterar )

Foto do Google+

Você está comentando utilizando sua conta Google+. Sair / Alterar )

Conectando a %s

%d blogueiros gostam disto: