Projeto de Calculadora

Vamos usar os conhecimentos adquiridos até aqui para desenvolver uma calculadora simples. Nosso projeto será composto pelo arquivo de cabeçalho (calculator.h), a implementação (calculator.c) e o programa que usa nossa calculadora (main_calculator.c).

Até aqui, usamos muitas palavras em português, visto que os ponteiros para função podem causar confusão no início. Agora, vamos usar mais o inglês nas implementações, pois o ecossistema de programação C (bibliotecas, documentação, e frameworks) utiliza predominantemente o inglês. Adotar o inglês em nomes de variáveis, funções e estruturas é crucial para aderir às boas práticas da indústria e facilitar a colaboração e a leitura global do código.

Nota

Se você possui um pouco de dificuldade com o inglês, mantenha um glóssario com as palavras mais usadas em programação. Consumir conteúdo de programação em inglês ajudará bastante nesta empreitada também.

Interface da API

A interface da calculadora contém um novo tipo Operation que representa uma função binária. Como já mencionamos é um ponteiro para uma função genérica que recebe dois paramêtros. Definimos a estrtura calculadora que contém os campos data_size para o tamanho em bytes do tipo de dados que iremos trabalhar, n_operations para o número de operações que a calculadora suporta e um ponteiro operations que contém as operações da calculadora.

calculator.h
#include <stddef.h> // Necessário para o tipo size_t

typedef void* (*Operation)(const void*, const void*);

typedef struct calculator {
    size_t data_size;
    size_t n_operations;
    Operation *operations; 
} Calculator;

Se liga! A palavra chave const indica que os valores passados para a operação não podem ser modificados internamente.

Em seguida, temos protótipos das funções para criar e destruir uma calculadora. A função evaluation já é conhecida e ela coordenda qual callback usar para calcular. Além dela, temos quatro funções de callback para adicionar e multiplicar inteiros ou números em ponto flutuante.

calculator.h
Calculator *calculator_create(size_t data_size, 
                              size_t n_operations, 
                              const Operation *operations);
void calculator_destroy(Calculator *calc);

void* evaluation(const void *a, const void *b, Operation operation); 

void* add_int (const void *a, const void *b);
void* add_double (const void *a, const void *b);
void* multiply_int (const void *a, const void *b);
void* multiply_double (const void *a, const void *b);

Operation select_operation_int();
Operation select_operation_double();

Em resumo:

Componente Função Design é Sólido
typedef Operation O contrato binário de todas as funções de cálculo. O uso de const void* protege os dados de entrada, garantindo que os callbacks apenas leiam.
struct Calculator O contêiner para metadados e operações. Os campos data_size e n_operations (ambos size_t) tornam a estrutura robusta e configurável para qualquer tipo de dado.
calculator_create/destroy Gerenciamento de ciclo de vida. Define claramente como a memória é alocada e, mais importante, como deve ser liberada.
evaluation A função coordenadora. Mantém o código principal limpo, delegando a lógica complexa (o callback).
Callbacks As funções de soma e multiplicação. Estão prontas para serem implementadas com a lógica de alocação de memória e casting de tipos.
select_operation_int Função de fábrica/seleção de callback. Permite ao usuário selecionar dinamicamente a operação para inteiros em tempo de execução, desacoplando a escolha da lógica de execução.
select_operation_double Função de fábrica/seleção de callback. Permite ao usuário selecionar dinamicamente a operação para reais em tempo de execução, desacoplando a escolha da lógica de execução.

Implementação da API

Agora partiremos para a implementação das funções da nossa interface. Primeiramente, vamos estabelecer quais bibliotecas padrão C e quais arquivos de interface precisamos importar.

calculator.c
#include "calculator.h"
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <stdio.h>

Na primeira linha, estamos importando nossa interface calculator.h. Como ela não faz parte da biblioteca padrão de C, usamos aspas duplas ("...") designando o caminho (basta o nome, pois ela está na mesma pasta).

Em seguida, importamos quatro bibliotecas padrão:

  • <stdlib.h>: Essencial para o gerenciamento de memória (malloc e free).

  • <string.h>: Necessária para o método memcpy, que utilizaremos para realizar a cópia segura de dados e dos ponteiros de função em um contexto genérico.

  • <assert.h>: Usada para a macro assert, que insere verificações de segurança no código e garante que as pré-condições das funções sejam atendidas.

  • <stdio.h>: Usada para imprimir mensagem na tela e receber dados do usuário.

Com as dependências definidas, podemos começar a implementação das funções, começando pelo construtor calculator_create.

calculator.c
Calculator *calculator_create(size_t data_size, 
                              size_t n_operations, 
                              const Operation *operations) {
    assert(data_size > 0);
    assert(n_operations > 0);   
    assert(operations != NULL); 
    
    Calculator *calc = (Calculator *)malloc(sizeof(Calculator));
    calc->data_size = data_size;
    calc->n_operations = n_operations;
    calc->operations = (Operation *)malloc(sizeof(Operation) * n_operations);

    if (calc->operations == NULL) {
        free(calc);
        return NULL;
    }

    memcpy(calc->operations, operations, sizeof(Operation) * n_operations);

    return calc;
}

O primeiro ato é verificar as pré-condições com assert. Caso a pré-condição seja falsa, o programa é abortado, especificando qual condição falhou e em qual linha ela se encontra.

  • assert(data_size > 0): Esta verificação garante que a calculadora tenha recebido um tamanho de dado válido. Não faz sentido alocar memória para um dado com 0 bytes, o que é fundamental para a função memcpy funcionar corretamente.

  • assert(n_operations > 0) e assert(operations != NULL): Esses asserts trabalham em conjunto para garantir que a calculadora possa de fato operar. Eles verificam que há pelo menos uma função de callback para a calculadora utilizar e que o ponteiro para o array de callbacks não é nulo.

O restante da função executa a alocação para Calculator, inicializa os campos e usa o tratamento de erro (rollback) para liberar corretamente a memória se algo falhar na alocação do array de operações. O uso de memcpy finaliza a função, garantindo que a estrutura Calculator é dona de sua própria tabela de callbacks. Ela copia o array de operações passado de forma segura, byte a byte.

Para cada construtor, implemente um destrutor (ou, mais precisamente, para cada malloc deve haver um free correspondente). Use esse princípio para programar em C e evitar vazamentos de memória (memory leaks). Uma vez que criamos o construtor da calculadora, vamos criar seu destrutor, calculator_destroy.

Primeiro, devemos liberar a memória alocada para as operações. Depois, a memória alocada para a estrutura principal da calculadora. É uma boa prática verificar se o ponteiro é válido antes de tentar liberar.

calculator.c
void calculator_destroy(Calculator *calc) {
    if (calc == NULL) return;

    // 1. Libera o array de operações (operations).
    if (calc->operations != NULL) {
        free(calc->operations);
        calc->operations = NULL; 
    }
    
    // 2. Libera a estrutura Calculator principal.
    free(calc);
}

A ordem de liberação é crucial: se a gente liberasse a estrutura principal free(calc) primeiro, o endereço de calc->operations (que está dentro de calc) seria perdido para sempre. Isso resultaria em um vazamento de memória (memory leak), pois você não conseguiria liberar o array de operações.

Depois de liberar a memória, é uma boa prática atribuir o valor NULL ao ponteiro a fim de evitar ponteiros pendentes (dangling pointers). No entanto, note que não fazemos isso com o ponteiro calc, porque ele foi passado por valor. A variável calc dentro desta função é apenas uma cópia do endereço; o ponteiro original (por exemplo, calculator_int) no main não é alterado.

A função coordenadora (evaluation) é a parte mais simples da nossa implementação. Ela não precisa saber qual operação está sendo executada, apenas delega a responsabilidade para a função de callback que foi passada:

calculator.c
void* evaluation(const void *a, const void *b, Operation operation){
  return operation(a, b);
} 

As funções que realmente realizam o cálculo seguem um protocolo rigoroso para garantir a generalidade e o gerenciamento de memória. Cada callback deve:

  • Alocar Memória: Usar malloc para reservar espaço para o resultado na memória heap.
  • Fazer o Casting: Converter o ponteiro genérico de entrada (const void*) para o tipo de dado correto (ex: int* ou double*) para que a operação possa ser realizada.
  • Desreferenciar e Calcular: Acessar o valor real (*) do ponteiro para realizar a operação.
  • Retornar: Reverter o ponteiro do resultado para o tipo genérico (void*).

Observe nos exemplos de soma como o casting é usado para transformar o ponteiro genérico (a, b) no tipo esperado:

calculator.c
// Callback para Soma de Inteiros 
void* add_int (const void *a, const void *b){
    int *result = malloc(sizeof(int));
    
    // Casting de (const void*) para (int*), seguido pela desreferência (*)
    *result = *(int*)a + *(int*)b; 
    
    return (void*)result; 
}

// Callback para Soma de Ponto Flutuante
void* add_double (const void *a, const void *b){
    double *result = malloc(sizeof(double));
    
    // Casting de (const void*) para (double*), seguido pela desreferência (*)
    *result = *(double*)a + *(double*)b; 
    
    return (void*)result; 
}

As outras funções de callback para multiplicação (multiply_int e multiply_double) seguirão exatamente o mesmo padrão de alocação e casting.

Os ponteiros de função nos permitem criar interfaces dinâmicas, como um menu interativo, permitindo que o usuário selecione qual callback deve ser executado em tempo de execução. A função select_operation_int é o nosso exemplo de função de fábrica para callbacks de inteiros. Seu objetivo é mapear a entrada numérica do usuário para o endereço de memória da função de callback correspondente.

calculator.c
Operation select_operation_int() {
    int option = 0;
    printf("Escolha sua operação:\n");
    printf("1. Soma\n");
    printf("2. Multiplicação\n");
    printf("Digite: ");
    scanf("%d", &option);

    if (option == 1) return add_int;
    else if (option == 2) return multiply_int;
    else return NULL; // Retorna NULL como ponteiro de função inválido
}

O bloco de controle de fluxo if-else é a parte central da função. Ele mapeia o inteiro c diretamente para o endereço de memória da função de callback desejada. Dessa forma, a função está pronta para ser usada no nosso main para selecionar dinamicamente a operação!

Do mesmo modo, a função select_operation_double() é implementada.

Uso da API

Nossa calculadora simples está finalmente finalizada. Prosseguiremos exemplificando o uso da mesma.

main_calculator.c
#include "calculator.h"
#include <stdio.h>
#include <stdlib.h>

int main() {
    
    Operation operations[] = {add_int, multiply_int};
    size_t n_operations = sizeof(operations) / sizeof(operations[0]); 
    
    // 1. CRIAÇÃO E TRATAMENTO DE ERRO
    Calculator* calculator_int = calculator_create(sizeof(int), 
                                                   n_operations, 
                                                   operations);
    
    if (calculator_int == NULL) {
        fprintf(stderr, "Erro: Falha ao alocar a estrutura Calculator.\n");
        return 1;
    }
    
    // Dados para teste
    int a = 10;
    int b = 2;
    
    // 2. SELEÇÃO E EXECUÇÃO
    Operation selected_op = select_operation_int(); 
    
    if (selected_op == NULL) {
        fprintf(stderr, "Erro: Operação inválida selecionada.\n");
        calculator_destroy(calculator_int); // Limpa o que foi criado
        return 1;
    }

    // Chama evaluation, que aloca o resultado na heap
    void *result_ptr = evaluation(&a, &b, selected_op);
    
    if (result_ptr == NULL) {
        fprintf(stderr, "Erro: Falha ao alocar memória para o resultado.\n");
        calculator_destroy(calculator_int); // Limpa o que foi criado
        return 1;
    }
    
    printf("Resultado: %d\n", *(int*)result_ptr);

    // 3. LIMPEZA DA MEMÓRIA
    
    // 3.1 Libera a memória alocada DENTRO do callback
    free(result_ptr); 
    
    // 3.2 Libera a estrutura Calculator e suas operações internas
    calculator_destroy(calculator_int); 
    calculator_int = NULL; // Boa prática: anular o ponteiro
    
    return 0;
}

Lembrando que para compilar nossa calculadora, garantindo que ambos os arquivos de implementação sejam processado, devemos executar o seguinte comando no terminal:

$ gcc calculator.c main_calculator.c -o calc

Isso irá gerar um executável chamado calc na sua pasta.

Finalizamos a implementação da nossa Calculadora Genérica. Ela pode não estar perfeita, mas conseguimos aplicar e solidificar conceitos avançados que dão um grande up em nossa jornada na programação C. Em particular, a manipulação de ponteiros genéricos (void*) e o uso de ponteiros para funções como callbacks são ferramentas poderosíssimas que devem ser exploradas com afinco.

Como sugestão de aprimoramento e prática adicional, você pode tentar modularizar o sistema e refinar a interface do usuário:

Nota🎯 Desafio de Código

Modularização

  • Mova as funções select_operation_int e select_operation_double para um novo par de arquivos de utilitário (por exemplo, ui.c/ui.h). Isso ajuda a manter o arquivo principal (calculator.c) focado apenas na lógica de cálculo, separando a interface do usuário da lógica de negócios.

Refinamento do Tipo

  • Refine a interface do usuário criando uma função inicial que permita ao usuário escolher entre os tipos (int ou double) antes de chamar as funções de seleção de operação.

Nesta unidade, você aprendeu

✅ a criar um projeto genérico com interface, implementação e uso

✅ a aplicar os conceitos de função de ordem superior e callback

✅ a gerenciar o ciclo de vida dos objetos na memória alocando e desalocando

✅ a compilar um projeto com mais de um arquivo .c

# Qual é o principal propósito do projeto da calculadora? > Pense na aplicação dos conceitos anteriores. 1. [ ] Criar uma calculadora apenas para inteiros. 1. [ ] Demonstrar herança e polimorfismo em C. 1. [x] Consolidar o uso de ponteiros genéricos e ponteiros para funções. 1. [ ] Implementar operações matemáticas complexas. # O que o tipo `Operation` representa no código? > Ele é um dos pilares da arquitetura da calculadora. 1. [ ] Um tipo genérico de variável numérica. 1. [x] Um ponteiro para função que recebe dois argumentos genéricos. 1. [ ] Uma estrutura auxiliar de armazenamento. 1. [ ] Um alias para `void*` usado para resultados. # Qual é a função do campo `data_size` na estrutura `Calculator`? > Lembre-se da importância do `size_t`. 1. [x] Indicar o tamanho em bytes do tipo de dado que será manipulado. 1. [ ] Controlar o número máximo de operações executadas. 1. [ ] Armazenar o resultado da última operação. 1. [ ] Determinar o número de callbacks disponíveis. # Sobre o uso de `const` em `Operation`, qual é a alternativa correta? > A palavra-chave tem papel importante na segurança do código. 1. [x] Garante que os parâmetros passados não serão modificados internamente. 1. [ ] Impede a função de retornar um valor. 1. [ ] Indica que a função deve ser inline. 1. [ ] Torna o ponteiro de função imutável. # Quais funções fazem parte do gerenciamento de ciclo de vida da calculadora? > Pense em alocação e liberação de memória. - [x] `calculator_create` - [x] `calculator_destroy` - [ ] `evaluation` - [ ] `select_operation_int` # Coloque na ordem as etapas executadas dentro de `calculator_create`. > Observe o processo de criação e inicialização. 1. Verificar pré-condições com `assert`. 2. Alocar memória para `Calculator`. 3. Alocar memória para o array `operations`. 4. Copiar o conteúdo das operações com `memcpy`. 5. Retornar o ponteiro da nova calculadora. # Qual é o papel da função `calculator_destroy`? > Pense na ordem correta da liberação. 1. [ ] Limpar apenas os dados dos resultados. 1. [x] Liberar o array de operações e depois a estrutura principal. 1. [ ] Encerrar o programa e exibir mensagem de sucesso. 1. [ ] Apenas definir todos os ponteiros como `NULL`. # Quais práticas de segurança de memória são usadas em `calculator_destroy`? > Observe o padrão de boas práticas. - [x] Verificação de ponteiro nulo antes de liberar. - [x] Liberação dos recursos na ordem inversa da alocação. - [x] Atribuição de `NULL` após `free` para evitar ponteiros pendentes. - [ ] Reutilização automática da estrutura após liberação. # O que a função `evaluation` faz? > Ela é o ponto de coordenação da execução. 1. [ ] Calcula e imprime o resultado diretamente. 1. [x] Chama a função de callback adequada, repassando os parâmetros. 1. [ ] Faz a seleção automática da operação. 1. [ ] Libera os ponteiros após a execução. # Qual é o comportamento comum às funções `add_int` e `add_double`? > Elas seguem o mesmo protocolo de implementação. - [x] Fazem cast dos ponteiros genéricos para o tipo correto. - [x] Alocam memória dinamicamente para o resultado. - [x] Retornam um `void*` apontando para o resultado. - [ ] Escrevem o resultado diretamente em `stdout`. # Qual seria o problema se as funções de callback não alocassem memória? > Pense no tempo de vida dos dados retornados. 1. [ ] A função não conseguiria imprimir o resultado. 1. [x] O ponteiro retornado apontaria para uma variável local inválida. 1. [ ] O compilador não aceitaria o retorno do tipo `void*`. 1. [ ] O programa entraria em loop infinito. # Sobre as funções `multiply_int` e `multiply_double`, assinale o correto. > Elas espelham a lógica das funções de soma. 1. [ ] Fazem casting incorreto para `float*`. 1. [x] Seguem o mesmo padrão de alocação, casting e retorno das funções de soma. 1. [ ] São implementadas com recursão. 1. [ ] Liberam automaticamente o resultado após a execução. # Qual é a função principal de `select_operation_int`? > Veja o papel do menu interativo. 1. [x] Retornar o endereço da função correspondente à operação escolhida. 1. [ ] Executar diretamente a operação selecionada. 1. [ ] Criar uma nova estrutura de calculadora. 1. [ ] Exibir o resultado na tela. # Coloque em ordem as etapas principais da execução em `main_calculator.c`. > Elas seguem o fluxo do programa principal. 1. Criar a calculadora com `calculator_create`. 2. Selecionar a operação via `select_operation_int`. 3. Chamar `evaluation` para obter o resultado. 4. Exibir o resultado na tela. 5. Liberar os recursos com `free` e `calculator_destroy`. # Quais verificações de erro são realizadas em `main_calculator.c`? > Segurança sempre vem primeiro. - [x] Verifica se `calculator_create` retornou `NULL`. - [x] Verifica se a operação selecionada é válida. - [x] Verifica se houve falha na alocação do resultado. - [ ] Verifica se o usuário digitou um número negativo. # Por que `calculator_int` é definido como `NULL` ao final do programa? > Uma boa prática de limpeza. 1. [x] Para evitar ponteiros pendentes após `free`. 1. [ ] Porque o compilador exige inicialização nula. 1. [ ] Para permitir o reuso automático da memória. 1. [ ] Porque `free` não libera o ponteiro corretamente. # Coloque em ordem o ciclo de vida da memória nesta aplicação. > Do início ao final da execução. 1. Alocação de `Calculator` com `malloc`. 2. Alocação de `operations`. 3. Alocação do resultado dentro do callback. 4. Liberação do resultado com `free`. 5. Liberação de `operations` e da estrutura principal. # Quais melhorias são sugeridas no desafio de código? > Elas visam modularização e clareza. - [x] Separar a interface do usuário em arquivos `ui.c` e `ui.h`. - [x] Permitir que o usuário escolha entre `int` e `double`. - [ ] Adicionar suporte a operações de subtração. - [ ] Mudar toda a estrutura para C++. # Quais conceitos fundamentais são reforçados neste projeto? > Ele consolida a base da programação modular em C. - [x] Uso de ponteiros genéricos (`void*`). - [x] Ponteiros para funções como callbacks. - [x] Modularização com múltiplos arquivos `.c`. - [ ] Programação orientada a objetos nativa.
De volta ao topo