SOP-funcoes

De MediaWiki do Campus São José
Ir para navegação Ir para pesquisar

Linguagem C: funções

Mas afinal para que servem funções ? Para propósitos como:

  1. Facilitar a reutilização de um algoritmo em diferentes partes de um programa: um exemplo são as funções printf e scanf, que fazem parte da biblioteca C padrão.
  2. Reduzir a complexidade de um programa, ao dividi-lo em algoritmos menores e mais simples: a divisão de um programa em pequenos algoritmos especializados o torna mais fácil de ser escrito, de ser lido e entendido, de ser depurado (debug) e mantido, e também de ser modificado.

ola, mundo !

Uma forma simples de entender funções é vê-las como algoritmos, que podem ser usados em diferentes partes de um programa, e mesmo em diferentes programas. Um primeiro exemplo é mostrado a seguir:

#include <stdio.h>

void ola() {
  printf("Ola, mundo !\n");
}

int main() {
  ola();
}

Ao se executar o programa acima, o resultado é aparecer na tela a mensagem "Ola, mundo !":

> gcc -o ola ola.c
> ./ola
Ola, mundo !

Se o exemplo for modificado da seguinte forma:

#include <stdio.h>

void ola() {
  printf("Ola, mundo !\n");
}

int main() {
  ola();
  ola();
  ola();
}

... sua execução dará como resultado:

> gcc -o ola ola.c
> ./ola
Ola, mundo !
Ola, mundo !
Ola, mundo !

Funções com parâmetros e declaração de funções

Mas e se fosse desejável criar uma função que pudesse mostrar diferentes mensagens na tela ? O exemplo abaixo ilustra uma forma de fazer isto:

#include <stdio.h>

void mostra_mensagem(char * mensagem) {
  printf("Sua mensagem: %s\n", mensagem);
}

int main() {
  mostra_mensagem("Ola, mundo");
  mostra_mensagem("testando ...");
  mostra_mensagem("");
  mostra_mensagem("testando de novo ...");
}

Se o programa acima for executado, dará este resultado:

> gcc -o mostra mostra.c
> ./mostra
Sua mensagem: Ola, mundo
Sua mensagem: testando ...
Sua mensagem:
Sua mensagem: testando de novo ...

A função mostra_mensagem apresenta na tela uma string que é especificada no momento em que essa função é chamada. A isto se chama parametrização: tornar um algoritmo independente de valores específicos de entrada, de forma que possa funcionar com qualquer valor de entrada compatível. O próximo exemplo aprofunda este conceito por meio de uma função que calcula o quadrado de um número:

#include <stdio.h>

int quadrado(int x) {
  int y;
 
  y = x*x;
  return y;
}

int main() {
  printf("O quadrado de 2 é %d\n", quadrado(2));
  printf("O quadrado de 3 é %d\n", quadrado(3));
}

Este segundo exemplo é mais elaborado do que os anteriores, pois a função quadrado possui um dado de entrada (o parâmetro int x, também chamado de argumento da função), tem uma variável local (int y, que existe somente dentro da função) e devolve um valor do tipo int como resultado. A declaração da função revela esses detalhes:

Quadrado.png

Esses são os elementos básicos para declarar, escrever e usar funções.

Escopo de variáveis

Por escopo de uma variável entende-se o bloco de código onde esta variável é válida. Com base nisto, temos as seguintes afirmações:

  • As variáveis valem no bloco que são definidas;
  • As variáveis definidas dentro de uma função recebem o nome de variáveis locais
  • As variáveis declaradas fora de qualquer função são chamadas de variáveis globais
  • Os parâmetros formais de uma função (também conhecidos como argumentos da função) valem também somente dentro da função;
  • Uma variável definida dentro de uma função não é acessível em outras funções, MESMO QUE ESSAS VARIÁVEIS TENHAM NOMES IDÊNTICOS.

Variáveis locais

No trecho de código a seguir tem-se um exemplo com funções e variáveis com nomes iguais.

#include <stdio.h>

void FUNC1() {
   int B;

   B = -100;
   printf("Valor de B dentro da função FUNC1: %d\n", B);
}

void FUNC2() {
   int B;

   B = -200;
   printf("Valor de B dentro da função FUNC2: %d\n", B);
}

void main() {
   int B;

   B = 10;
   printf("Valor de B: %d\n", B);
   B = 20;
   FUNC1();
   printf("Valor de B: %d\n", B);
   B = 30;
   FUNC2();
   printf("Valor de B: %d\n", B);
}

O resultado de sua execução deve ser:

Valor de B: 10
Valor de B dentro da função FUNC1: -100
Valor de B: 20
Valor de B dentro da função FUNC2: -200
Valor de B: 30

Variáveis globais

O seguinte exemplo mostra o mesmo programa, porém fazendo com que a variável B seja global:

#include <stdio.h>

// Declaração da variável global B
int B;

void FUNC1() {
   B = -100;
   printf("Valor de B dentro da função FUNC1: %d\n", B);
}

void FUNC2() {
   B = -200;
   printf("Valor de B dentro da função FUNC2: %d\n", B);
}

void main() {
   B = 10;
   printf("Valor de B: %d\n", B);
   B = 20;
   FUNC1();
   printf("Valor de B: %d\n", B);
   B = 30;
   FUNC2();
   printf("Valor de B: %d\n", B);
}

O resultado de sua execução deve ser:

Valor de B: 10
Valor de B dentro da função FUNC1: -100
Valor de B: -100
Valor de B dentro da função FUNC2: -200
Valor de B: -200

E se existir um variável local com mesmo nome que uma global ? nesse caso, a variável local vai esconder a sua homônima global:

#include <stdio.h>

// Declaração da variável global B
int B;

void FUNC1() {
   //  no escopo desta função, esta variável local vai esconder a variável global B! 
   int B; 

   B = -100;
   printf("Valor de B dentro da função FUNC1: %d\n", B);
}

void FUNC2() {
   B = -200;
   printf("Valor de B dentro da função FUNC2: %d\n", B);
}

void main() {
   B = 10;
   printf("Valor de B: %d\n", B);
   B = 20;
   FUNC1();
   printf("Valor de B: %d\n", B);
   B = 30;
   FUNC2();
   printf("Valor de B: %d\n", B);
}

O resultado de sua execução deve ser:

Valor de B: 10
Valor de B dentro da função FUNC1: -100
Valor de B: 20
Valor de B dentro da função FUNC2: -200
Valor de B: -200

Valor de retorno de função

Os próximos exemplos mostram outras situações em que se usam funções.

Função para testar se um número é par ou ímpar

#include <stdio.h>

int eh_par(int numero) {
  int resto;

  resto = numero % 2;
  if (resto == 0) return 1;
  else return 0;
}

int main() {
  int x;

  printf("Digite um numero: ");
  scanf("%d", &x);
  if (eh_par(x)) {
    printf("%d eh par\n", x);
  } else {
    printf("%d eh impar\n", x);
  }

}

O exemplo acima mostra se um número é par ou ímpar. A função int eh_par(int numero) retorna 1 se o valor contido em numero for par, e 0 caso contrário. Veja como essa função é chamada dentro da função main:

if (eh_par(x)) {

A função é usada diretamente como uma condição do if .. else (equivalente ao se .. então .. senão do Portugol). Isto é possível porque na linguagem C o valor 0 é equivalente ao valor booleano FALSO, e qualquer valor diferente de 0 é VERDADEIRO. Assim, ao retornar 0 ou 1, a função eh_par está tendo um resultado equivalente a FALSO ou VERDADEIRO. Tanto que o programa poderia ser escrito assim, tendo o mesmo resultado final:

int main() {
  int x;

  printf("Digite um numero: ");
  scanf("%d", &x);
  if (! eh_par(x)) {
    printf("%d eh impar\n", x);
  } else {
    printf("%d eh par\n", x);
  }

}

Lembre que o operador ! é equivalente a NÃO lógico (negação de uma condição). Então a condição ! eh_par(x) é verdadeira se x não for par.

Função para testar se um número é primo

Um exemplo semelhante ao anterior trata da verificação se um número é primo:

#include <stdio.h>

int primo(int n) {
  int x, ok;

  x = 2;
  ok = 1;
  while ((x <= (n/2)) && ok) {
    if ((n % x) == 0) {
      ok = 0;
    } else x++;
  }
  return ok;
}

int main() {
  int n;

  printf("Numero: ");
  scanf("%d", &n);
  if (primo(n)) {
    printf("%d eh primo\n", n);
  } else {
    printf("%d nao eh primo\n", n);
  }
}

A função int primo(int n) retorna 1 se o valor passado como parâmetro no argumento int nfor primo, e 0 caso contrário. Essa função possui um algoritmo mais elaborado que os exemplos anteriores, o qual utiliza uma estrutura de repetição com while (condição) (equivalente ao Enquanto condição do Portugol). Inclusive nessa função aparece um operador aritmético novo, usado para incrementar variáveis numéricas:

x++;

Isto é equivalente a:

x = x + 1;

De forma análoga, existe um operador parecido para decremento:

x--;

... equivalente a:

x = x - 1;

Função para fatorar um número

Esse exemplo para testar se números são primos pode ser aproveitado para criar um programa que fatore um número em seus divisores primos:

#include <stdio.h>

int primo(int n) {
  int x, ok;

  x = 2;
  ok = 1;
  while ((x <= (n/2)) && ok) {
    if ((n % x) == 0) {
      ok = 0;
    } else x++;
  }
  return ok;
}

int main() {
  int p, x;

  printf("Numero: ");
  scanf("%d", &x);
  p = 1;
  while (x != 1) {
    do {
      p ++;
    } while(!primo(p));

    while ((x % p) == 0) {
      x = x /p;
      printf("%d\n", p);
    }
  }
}

A função int primo(int n) foi reaproveitada para escrever esse novo programa, que mostra todos os fatores primos que dividem um número.

Passagem de parâmetros

O próximo exemplo mostra uma função que torna maiúscula a primeira letra de uma string:

#include <stdio.h>

void faz_primeira_letra_maiuscula(char * nome) {
  if ((nome[0] >= 'a') && (nome[0] <= 'z')) {
    nome[0] = nome[0] - 32;
  }
}

int main() {
  char nome[32];

  printf("Digite um nome: ");
  scanf("%s", nome);
  faz_primeira_letra_maiuscula(nome);
  printf("Nome: %s\n", nome);
}

A função void faz_primeira_letra_maiuscula(char * nome) converte a primeira letra do parâmetro char * nome para maiúscula. A conversão se baseia na tabela ASCII, que associa códigos numéricos a caracteres. Mas a novidade nessa função é a possibilidade de modificar um valor passado como parâmetro, o que não acontecia nas funções vistas até então. Dependendo de como foi passado um parâmetro, modificações em seu valor dentro de uma função podem ou não refletir fora dessa função.

    • Passagem por valor: o valor da variável passada como parâmetro é copiado para uma variável local da função. Com isto, qualquer modificação feita na variável local da função não afeta a variável que foi passada como parâmetro. Ex:
      int incrementa_e_mostra(int x) {
        x = x + 1;
        printf("x=%d\n", x);
      }
      
      int main() {
        int contador = 0;
      
        incrementa_e_mostra(contador);
        incrementa_e_mostra(contador);
        incrementa_e_mostra(contador);
        printf("Ao final, contador=%d\n", contador);
      }
      
      Ao executar este programa exemplo, o resultado na tela deve ser o seguinte:
      x=1
      x=1
      x=1
      Ao final, contador=0
      ... o que evidencia que o valor de contador não foi modificado dentro da função incrementa_e_mostra.
    • Passagem por referência: a própria variável é passada como parâmetro (quer dizer, a sua localização em memória, por isto se diz por referência). Assim, qualquer modificação feita dentro da função chamada refletirá na variável que foi passada como parâmetro. Ex:
      int incrementa_e_mostra(int * x) {
        *x = *x + 1;
        printf("x=%d\n", *x);
      }
      
      int main() {
        int contador = 0;
      
        incrementa_e_mostra(&contador);
        incrementa_e_mostra(&contador);
        incrementa_e_mostra(&contador);
        printf("Ao final, contador=%d\n", contador);
      }
      
      Ao se executar o exemplo acima, o resultado na tela deve ser:
      x=1
      x=2
      x=3
      Ao final, contador=3
      Fica evidente que o valor da variável contador foi modificado dentro da função incrementa_e_mostra. Repare que algumas diferenças de sintaxe existem quando se usa passagem por referência:
      • A declaração do parâmetro na função deve ter um * (asterisco) antes do nome do parâmetro (ex: incrementa_e_mostra(int * x) )
      • Dentro da função deve-se preceder com * (asterisco) o nome da variável do parâmetro (ex: *x = *x + 1;)
        • Quando a variável passada como parâmetro for do tipo struct, ao invés de precedê-la por * (asterisco), deve-se usar a seguinte sintaxe: nome_variável->campo ao invés de nome_variável.campo. Veja a função adiciona_pessoa do cadastro como exemplo.
      • Na chamada da função, deve-se preceder com & o nome da variável passada como parâmetro (ex: incrementa_e_mostra(&contador); )
      • As duas últimas regras de sintaxe acima não valem quando se passa uma variável string como parâmetro. Isto porque uma variável string por definição já é uma variável que contém um endereço de memória (quer dizer, ela já funciona como uma referência aos dados). Na verdade, isto vale para qualquer variável do tipo vetor (lembre que a string é um vetor de char). Ex:
        void faz_primeira_letra_maiuscula(char * nome) {
          if ((nome[0] >= 97) && (nome[0] <= 122)) {
            nome[0] = nome[0] - 32;
          }
        }
        
        int main() {
          char nome[32];
        
          printf("Digite um nome: ");
          scanf("%s", nome);
          faz_primeira_letra_maiuscula(nome);
          printf("Nome: %s\n", nome);
        }
        
    • No caso do exemplo do cadastro: para ver a diferença entre passagem de parâmetro por referência e por valor no caso do cadastro, veja esta versão do cadastro em que a função adiciona_pessoa faz passagem por valor.
    • Veja no curso de linguagem C da UFMG uma discussão mais aprofundada sobre passagem de parâmetros e escopo de variáveis.

Reaproveitamento e especialização de funções

Uma função pode ser especializada para fazer uma tarefa mais limitada. Veja o exemplo da inicial maíuscula:

#include <stdio.h>

void faz_letra_maiuscula(char * nome, int pos) {
  if ((nome[pos] >= 97) && (nome[pos] <= 122)) {
    nome[pos] = nome[pos] - 32;
  }
}

void faz_primeira_letra_maiuscula(char * nome) {
  faz_letra_maiuscula(nome, 0);
}

int main() {
  char nome[32];

  printf("Digite um nome: ");
  scanf("%s", nome);
  faz_primeira_letra_maiuscula(nome);
  printf("Nome: %s\n", nome);
}

Apesar de o resultado ser exatamente o mesmo do exemplo anterior, em que havia somente a função faz_primeira_letra_maiuscula, esse novo exemplo mostra como especializar uma função mais geral. A função void faz_letra_maiuscula(char * nome, int posicao) torna maiúscula a letra contida na posição pos da string 'nome. Quer dizer, faz_letra_maiuscula é capaz de tornar maiúscula qualquer letra da string contida em nome. A função void faz_primeira_letra_maiuscula(char * nome) torna maiúscula a letra inicial da string contida em nome, e para fazer isto agora foi usada a função faz_letra_maiuscula, indicando-se que se quer mudar a letra na posição 0.

Funções que usam tipos de dados definidos pelo usuário (ou struct)

Funções podem receber parâmetros de tipos struct, como retornar valores struct. Veja esse exemplo do cálculo da distância entre dois pontos bidimensionais:

#include <stdio.h>
#include <math.h>

struct Ponto {
  float x,y;
};

float quadrado(float x) {
  return x*x;
}

float distancia(struct Ponto p1, struct Ponto p2) {
  float d;

  d = sqrt(quadrado(p1.x - p2.x) + quadrado(p1.y - p2.y));
}

int main() {
  struct Ponto p1, p2;

  printf("Primeiro ponto:\n");
  printf("  x=");
  scanf("%f", &p1.x);
  printf("  y=");
  scanf("%f", &p1.y);

  printf("Segundo ponto:\n");
  printf("  x=");
  scanf("%f", &p2.x);
  printf("  y=");
  scanf("%f", &p2.y);

  printf("\nDistancia entre (%f, %f) e (%f, %f): %f\n", p1.x, p1.y, p2.x, p2.y,
          distancia(p1, p2));
}

Sua compilação e execução seguem abaixo:

> gcc -o distancia -lm distancia1.c  
> ./distancia
Primeiro ponto:
 x=1
 y=5
Segundo ponto:
 x=4
 y=9
Distancia entre (1.000000, 5.000000) e (4.000000, 9.000000): 5.000000

Observe que a compilação tem uma diferença em relação aos outros exemplos: a opção -lm passada ao compilador. Essa opção informa ao compilador que o programa precisa da bilioteca de funções matemáticas (chamada de libm, que vem de library math). Uma biblioteca é um arquivo que contém funções prontas para serem usadas. No exemplo acima, foi usada a função sqrt (abreviação de square root, ou raiz quadrada), que está implementada nessa biblioteca.

A função float distancia(struct Ponto p1, struct Ponto p2) calcula a distância entre os pontos dados pelos parâmetros p1 e p2. Esses parâmetros são do tipo struct Ponto, que foi definido pelo usuário da seguinte forma:

struct Ponto {
  float x,y;
};

O programa mostrado acima pode ser melhorado, para pareça mais simples e fácil de ser entendido:

#include <stdio.h>
#include <math.h>

struct Ponto {
  float x,y;
};

float quadrado(float x) {
  return x*x;
}

float distancia(struct Ponto p1, struct Ponto p2) {
  float d;

  d = sqrt(quadrado(p1.x - p2.x) + quadrado(p1.y - p2.y));
}

void le_ponto(char * mensagem, struct Ponto * p) {
  printf("%s\n", mensagem);
  printf("  x=");
  scanf("%f", &p->x);
  printf("  y=");
  scanf("%f", &p->y);
}

int main() {
  struct Ponto p1, p2;

  le_ponto("Primeiro ponto:", &p1);
  le_ponto("Segundo ponto:", &p2);

  printf("\nDistancia entre (%f, %f) e (%f, %f): %f\n", p1.x, p1.y, p2.x, p2.y,
          distancia(p1, p2));
}

A diferença em relação ao exemplo anterior está no aparecimento da função void le_ponto(char * mensagem, struct Ponto * p), que lê do teclado as coordenadas X e Y de um ponto. Essa função precisa fazer passagem de parâmetro por referência, uma vez que os campos x e y do argumento da função struct Ponto p devem ser modificados dentro da função, e essa modificação deve ser refletida fora da função. Veja que o acesso a esses campos tem uma sintaxe diferente:

  printf("  x=");
  scanf("%f", &p->x);

Sempre que um argumento do tipo struct for passado por referência (ex: struct Ponto * p), o acesso a seus campos deve usar essa sintaxe.