Aula 14a — Categorias de Funções em C

void, com retorno, com parâmetros e com ponteiros — quando usar cada uma

As Quatro Categorias de Função

Na Aula 13 você aprendeu a estrutura básica de uma função: como declará-la, defini-la e chamá-la. Agora vamos entender as quatro categorias que existem em C, cada uma adequada para um tipo diferente de situação.

# Categoria Retorna algo? Recebe dados? Quando usar
I Função void ❌ Não ❌ Não Ação pura: imprimir, separador, menu
II Com valor de retorno ✅ Sim ❌ Não (ou usa globais) Cálculo que produz um resultado único
III Com parâmetros ✅ Sim (ou void) ✅ Sim — cópias dos valores Cálculo com dados fornecidos pelo chamador
IV Com ponteiros void (resultado via ponteiro) ✅ Sim — endereços de memória Modificar variáveis originais ou retornar múltiplos resultados
As categorias não são excludentes — uma função real pode combinar parâmetros por valor e por ponteiro ao mesmo tempo, como veremos na Categoria IV. A divisão aqui é didática, para construir o entendimento passo a passo.

Categoria I — Funções void

Uma função void realiza uma ação mas não devolve nenhum resultado. Ela é chamada por seus efeitos — imprimir algo na tela, modificar uma estrutura, tocar um alarme — e não pelo valor que produz (porque não produz nenhum).

Funções void têm void como tipo de retorno. Não possuem instrução return valor — apenas return; vazio (opcional) para sair antecipadamente, ou simplesmente fecham com }.

Exemplo 1 — sem parâmetros: separador visual

#include <stdio.h>

void imprimirSeparador() {
    printf("================================\n");
}

void imprimirCabecalho() {
    imprimirSeparador();
    printf("   SISTEMA DE NOTAS\n");
    imprimirSeparador();
}

int main() {
    imprimirCabecalho();
    printf("   Aluno: Ana Souza\n");
    imprimirSeparador();
    return 0;
}
================================
   SISTEMA DE NOTAS
================================
   Aluno: Ana Souza
================================

Exemplo 2 — com parâmetro: exibição formatada

Uma função void pode receber parâmetros — ela só não retorna nada.

#include <stdio.h>

void exibirSituacao(char *nome, float media) {
    printf("%-20s | Media: %.1f | %s\n",
           nome,
           media,
           media >= 7.0f ? "Aprovado" : "Reprovado");
}

int main() {
    exibirSituacao("Carlos Mota",   8.5f);
    exibirSituacao("Beatriz Lima",  5.0f);
    exibirSituacao("Diego Farias",  7.0f);
    return 0;
}
Carlos Mota | Media: 8.5 | Aprovado
Beatriz Lima | Media: 5.0 | Reprovado
Diego Farias | Media: 7.0 | Aprovado
Quando usar void: Sempre que o propósito da função for um efeito (imprimir, gravar, atualizar tela) e não um cálculo com resultado. Separar a apresentação dos dados da lógica de cálculo é uma das bases da modularização.

Categoria II — Funções com Valor de Retorno

Funções com valor de retorno calculam um resultado e o devolvem ao chamador com a instrução return. O chamador pode usar esse valor em expressões, atribuições ou diretamente em outros printf.

O tipo de retorno é declarado antes do nome da função. A instrução return expressão; termina a execução da função e entrega o valor ao ponto da chamada. Uma função pode ter múltiplos return em caminhos diferentes — mas sempre retorna um único valor.

Exemplo 1 — sucessor e predecessor

#include <stdio.h>

int sucessor(int n) {
    return n + 1;
}

int predecessor(int n) {
    return n - 1;
}

int main() {
    int a = 10;
    printf("Número:      %d\n", a);
    printf("Sucessor:    %d\n", sucessor(a));
    printf("Predecessor: %d\n", predecessor(a));
    return 0;
}
Número: 10
Sucessor: 11
Predecessor: 9

Exemplo 2 — múltiplos return e lógica de decisão

#include <stdio.h>

char classificarNota(float nota) {
    if (nota >= 9.0f) return 'A';
    if (nota >= 7.0f) return 'B';
    if (nota >= 5.0f) return 'C';
    return 'D';                    // abaixo de 5
}

int ehPrimo(int n) {
    if (n < 2) return 0;
    for (int i = 2; i * i <= n; i++)
        if (n % i == 0) return 0;
    return 1;                      // 1 = verdadeiro em C
}

int main() {
    printf("Nota 8.5 -> conceito %c\n", classificarNota(8.5f));
    printf("Nota 5.0 -> conceito %c\n", classificarNota(5.0f));
    printf("Nota 3.0 -> conceito %c\n", classificarNota(3.0f));

    for (int i = 1; i <= 15; i++)
        if (ehPrimo(i))
            printf("%d ", i);
    printf("\n");
    return 0;
}
Nota 8.5 -> conceito B
Nota 5.0 -> conceito C
Nota 3.0 -> conceito D
2 3 5 7 11 13

Exemplo 3 — soma e exibição: dois estilos

#include <stdio.h>

/* Exibe o resultado — void, efeito colateral */
void mostrarSoma(int a, int b) {
    printf("A soma de %d e %d é %d\n", a, b, a + b);
}

/* Retorna o resultado — reutilizável em expressões */
int calcularSoma(int a, int b) {
    return a + b;
}

int main() {
    mostrarSoma(4, 7);

    int resultado = calcularSoma(4, 7);
    printf("Resultado guardado: %d\n", resultado);

    /* A versão com retorno pode ser usada diretamente em expressões: */
    printf("Dobro da soma: %d\n", calcularSoma(4, 7) * 2);
    return 0;
}
A soma de 4 e 7 é 11
Resultado guardado: 11
Dobro da soma: 22
void vs. retorno — qual escolher? Se o chamador precisa usar o resultado em cálculos, condições ou guardá-lo numa variável, use retorno. Se o único propósito é um efeito visível (imprimir, tocar som, salvar arquivo), use void. Prefira retorno: funções com retorno são mais fáceis de testar e reutilizar.

Categoria III — Funções com Parâmetros

Parâmetros são a forma de alimentar a função com dados externos. Sem parâmetros, uma função só pode trabalhar com constantes ou variáveis globais — o que a torna pouco reutilizável. Com parâmetros, a mesma função serve para qualquer conjunto de dados.

Cada parâmetro é declarado com tipo e nome na assinatura da função: float calcularArea(float base, float altura). Ao chamar a função, o chamador fornece os argumentos — os valores concretos. A função recebe cópias desses valores e trabalha com elas internamente.

Exemplo 1 — área de figuras geométricas

#include <stdio.h>
#define PI 3.14159f

float areaRetangulo(float base, float altura) {
    return base * altura;
}

float areaTriangulo(float base, float altura) {
    return (base * altura) / 2.0f;
}

float areaCirculo(float raio) {
    return PI * raio * raio;
}

int main() {
    printf("Retangulo 5x3:    %.2f\n", areaRetangulo(5.0f, 3.0f));
    printf("Triangulo 6x4:    %.2f\n", areaTriangulo(6.0f, 4.0f));
    printf("Circulo r=7:      %.2f\n", areaCirculo(7.0f));
    return 0;
}
Retangulo 5x3: 15.00
Triangulo 6x4: 12.00
Circulo r=7: 153.94

Exemplo 2 — parâmetros de tipos diferentes

#include <stdio.h>

float calcularSalario(float horasTrabalhadas, float valorHora, float bonus) {
    float bruto = horasTrabalhadas * valorHora + bonus;
    return bruto;
}

void imprimirHolerite(char *nome, float horas, float valorHora, float bonus) {
    float salario = calcularSalario(horas, valorHora, bonus);
    printf("Funcionario: %-15s | Horas: %.0f | Salario: R$%.2f\n",
           nome, horas, salario);
}

int main() {
    imprimirHolerite("Ana Oliveira",  160.0f, 25.50f, 300.0f);
    imprimirHolerite("Bruno Santos",  120.0f, 30.00f, 0.0f);
    imprimirHolerite("Carla Mendes",  176.0f, 22.00f, 500.0f);
    return 0;
}
Funcionario: Ana Oliveira | Horas: 160 | Salario: R$4380.00
Funcionario: Bruno Santos | Horas: 120 | Salario: R$3600.00
Funcionario: Carla Mendes | Horas: 176 | Salario: R$4372.00

Exemplo 3 — parâmetros e composição de funções

Uma função pode chamar outras funções, passando seus parâmetros adiante ou usando seus retornos como argumentos:

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

float calcularHipotenusa(float cateto1, float cateto2) {
    return sqrt(cateto1 * cateto1 + cateto2 * cateto2);
}

int classificarTriangulo(float a, float b, float c) {
    /* retorna: 1=acutângulo  2=retângulo  3=obtusângulo  0=inválido */
    if (a <= 0 || b <= 0 || c <= 0)          return 0;
    if (a+b <= c || a+c <= b || b+c <= a)    return 0;
    float maior2 = (a>b && a>c) ? a*a : (b>c ? b*b : c*c);
    float soma   = a*a + b*b + c*c - maior2;
    if (maior2 < soma)  return 1;
    if (maior2 == soma) return 2;
    return 3;
}

int main() {
    float hip = calcularHipotenusa(3.0f, 4.0f);
    printf("Hipotenusa de (3, 4): %.2f\n", hip);

    const char *tipos[] = {"Invalido","Acutangulo","Retangulo","Obtusangulo"};
    printf("Triangulo 3-4-5: %s\n",  tipos[classificarTriangulo(3,4,5)]);
    printf("Triangulo 5-5-5: %s\n",  tipos[classificarTriangulo(5,5,5)]);
    printf("Triangulo 2-2-9: %s\n",  tipos[classificarTriangulo(2,2,9)]);
    return 0;
}
Hipotenusa de (3, 4): 5.00
Triangulo 3-4-5: Retangulo
Triangulo 5-5-5: Acutangulo
Triangulo 2-2-9: Invalido
Parâmetros são cópias. Modificar um parâmetro dentro da função não altera a variável original no chamador. Isso é previsível e seguro — a função não tem como "estragar" os dados de fora por acidente. Na Aula 14b veremos quando isso é insuficiente e como os ponteiros resolvem.

Categoria IV — Funções com Ponteiros

Parâmetros normais são cópias — a função não pode alterar a variável original. Às vezes isso é exatamente o que precisamos. Outras vezes, precisamos que a função modifique a variável original ou devolva mais de um resultado. Para isso, usamos ponteiros como parâmetros.

Um ponteiro é uma variável que armazena o endereço de memória de outra variável. Ao passar o endereço de uma variável para uma função (&x), a função pode ler e modificar diretamente o valor original, sem precisar de return para isso.

Exemplo 1 — incrementar o original (impossível sem ponteiro)

#include <stdio.h>

/* Sem ponteiro: modifica apenas a cópia — original intocado */
void tentarIncrementar(int n) {
    n++;                          // afeta só a cópia local
}

/* Com ponteiro: acessa e modifica o original */
void incrementar(int *n) {
    (*n)++;                       // modifica no endereço apontado
}

int main() {
    int pontos = 10;

    tentarIncrementar(pontos);
    printf("Após tentativa: %d\n", pontos);   // ainda 10

    incrementar(&pontos);
    printf("Após ponteiro:  %d\n", pontos);   // agora 11
    return 0;
}
Após tentativa: 10
Após ponteiro: 11

Exemplo 2 — retornar dois resultados ao mesmo tempo

Uma função só pode ter um return. Com ponteiros, podemos "retornar" vários resultados escrevendo diretamente nos endereços fornecidos pelo chamador:

#include <stdio.h>

/* Calcula mínimo e máximo de um vetor — dois resultados via ponteiro */
void minMax(int v[], int tam, int *minimo, int *maximo) {
    *minimo = *maximo = v[0];
    for (int i = 1; i < tam; i++) {
        if (v[i] < *minimo) *minimo = v[i];
        if (v[i] > *maximo) *maximo = v[i];
    }
}

/* Calcula média e desvio (simplificado) — dois resultados via ponteiro */
void estatisticas(float v[], int tam, float *media, float *amplitude) {
    float soma = 0, mn = v[0], mx = v[0];
    for (int i = 0; i < tam; i++) {
        soma += v[i];
        if (v[i] < mn) mn = v[i];
        if (v[i] > mx) mx = v[i];
    }
    *media     = soma / tam;
    *amplitude = mx - mn;
}

int main() {
    int notas[] = {7, 3, 9, 5, 8, 2, 10};
    int mn, mx;
    minMax(notas, 7, &mn, &mx);
    printf("Notas — min: %d | max: %d\n", mn, mx);

    float precos[] = {12.5f, 8.0f, 25.0f, 5.5f, 18.0f};
    float med, amp;
    estatisticas(precos, 5, &med, &amp);
    printf("Precos — media: %.1f | amplitude: %.1f\n", med, amp);
    return 0;
}
Notas — min: 2 | max: 10
Precos — media: 13.8 | amplitude: 19.5

Exemplo 3 — imprimir e modificar um array via funções

#include <stdio.h>

void exibir(const int *v, int tam) {          // const: só leitura
    for (int i = 0; i < tam; i++)
        printf("%d ", v[i]);
    printf("\n");
}

void aplicarDesconto(int *precos, int tam, int percentual) {
    for (int i = 0; i < tam; i++)
        precos[i] -= precos[i] * percentual / 100;
}

int main() {
    int tabela[] = {100, 250, 80, 320, 150};

    printf("Antes:  "); exibir(tabela, 5);
    aplicarDesconto(tabela, 5, 10);           // desconto de 10%
    printf("Depois: "); exibir(tabela, 5);
    return 0;
}
Antes: 100 250 80 320 150
Depois: 90 225 72 288 135
Arrays sempre são ponteiros. Ao passar tabela para uma função, C passa automaticamente o endereço do primeiro elemento — por isso aplicarDesconto modifica o array original sem precisar de & na chamada. Use const quando a função só lê o array, para deixar a intenção clara e o compilador vigilante.

Exemplo Final — As Quatro Categorias Juntas

Um programa de boletim escolar que usa as quatro categorias em conjunto:

#include <stdio.h>

/* CATEGORIA I — void sem retorno */
void imprimirLinha() {
    printf("------------------------------------------\n");
}

/* CATEGORIA II — com retorno */
char conceito(float media) {
    if (media >= 9.0f) return 'A';
    if (media >= 7.0f) return 'B';
    if (media >= 5.0f) return 'C';
    return 'D';
}

/* CATEGORIA III — com parâmetros por valor */
float calcularMedia(float n1, float n2, float n3) {
    return (n1 + n2 + n3) / 3.0f;
}

/* CATEGORIA IV — com ponteiros: calcula melhor e pior média */
void extremosTurma(float medias[], int tam,
                   float *melhor, float *pior) {
    *melhor = *pior = medias[0];
    for (int i = 1; i < tam; i++) {
        if (medias[i] > *melhor) *melhor = medias[i];
        if (medias[i] < *pior)   *pior   = medias[i];
    }
}

int main() {
    const char *nomes[]  = {"Ana", "Bruno", "Carla", "Diego"};
    float notas[][3] = {
        {8.0f, 9.0f, 7.5f},
        {5.0f, 6.0f, 4.5f},
        {10.0f, 9.5f, 9.0f},
        {6.5f, 7.0f, 6.0f}
    };
    float medias[4];
    int n = 4;

    imprimirLinha();
    printf("%-10s  %5s  %5s  %5s  %6s  %s\n",
           "Nome","N1","N2","N3","Media","Conceito");
    imprimirLinha();

    for (int i = 0; i < n; i++) {
        medias[i] = calcularMedia(notas[i][0], notas[i][1], notas[i][2]);
        printf("%-10s  %5.1f  %5.1f  %5.1f  %6.1f  %c\n",
               nomes[i],
               notas[i][0], notas[i][1], notas[i][2],
               medias[i], conceito(medias[i]));
    }

    imprimirLinha();
    float melhor, pior;
    extremosTurma(medias, n, &melhor, &pior);
    printf("Melhor media: %.1f | Pior media: %.1f\n", melhor, pior);
    imprimirLinha();

    return 0;
}
------------------------------------------
Nome    N1     N2     N3   Media Conceito
------------------------------------------
Ana    8.0    9.0    7.5     8.2 B
Bruno    5.0    6.0    4.5     5.2 C
Carla   10.0    9.5    9.0     9.5 A
Diego    6.5    7.0    6.0     6.5 C
------------------------------------------
Melhor media: 9.5 | Pior media: 5.2
------------------------------------------

Quiz — Aula 14a

1. Uma função que imprime um relatório na tela e não devolve nenhum resultado deve ser declarada com qual tipo de retorno?

2. Qual é a saída do código abaixo?

int dobrar(int n) { return n * 2; }
int main() {
    int x = 5;
    dobrar(x);
    printf("%d", x);
}

3. Para que a função abaixo consiga alterar a variável original, o parâmetro deve ser declarado como:

void zerar(??? n) { *n = 0; }

4. Por que a função minMax usa ponteiros em vez de retornar os valores?

5. Qual é a saída?

void f(int n) { n = 99; }
int main() {
    int x = 1;
    f(x);
    printf("%d", x);
}

6. O que o parâmetro const int *v garante em uma função?

7. Uma função void calcularMedia(float n1, float n2) que calcula internamente a média mas não retorna e não usa ponteiros:

8. Ao passar um array int tabela[] para uma função sem usar &, o que é passado?