Projeto: calculadora
LidoNesta aula, você construirá uma calculadora completa em C, cobrindo desde o parsing de entrada até o tratamento de erros, com uma estrutura modular e boas práticas. O projeto inclui suporte a operações básicas, precedência de operadores e funções avançadas, além de testes.
Nesta aula, vamos colocar em prática tudo o que aprendemos ao longo do curso, construindo uma calculadora completa em C. Este projeto integra conceitos de parsing, manipulação de strings, estruturas de dados, funções e tratamento de erros. Ao final, você terá uma calculadora funcional que aceita expressões como 2 + 3 * 4, resolve com a precedência correta e trata entradas inválidas com elegância.
O objetivo é criar um programa modular, legível e extensível. Começaremos pelo parsing da entrada, depois implementaremos as operações, o tratamento de erros e, por fim, organizaremos tudo em uma estrutura limpa. Vamos lá!
Parsing de entrada
O parsing é o processo de analisar uma string de entrada e convertê-la em uma estrutura que o computador possa processar. Para nossa calculadora, precisamos interpretar números, operadores e parênteses. Uma abordagem comum é usar uma pilha para avaliar expressões no formato pós-fixo (notação polonesa reversa), mas aqui adotaremos uma técnica mais simples: converter a expressão infixa (como a escrevemos normalmente) para pós-fixa usando o algoritmo shunting-yard de Edsger Dijkstra.
O algoritmo shunting-yard usa duas pilhas: uma para operadores e outra para valores (ou saída). Percorremos a entrada caractere por caractere, ignorando espaços. Quando encontramos um número, o empurramos para a pilha de valores. Quando encontramos um operador, comparamos sua precedência com o topo da pilha de operadores e, se a precedência for menor ou igual, desempilhamos operadores para a pilha de valores até que a condição seja satisfeita. Parênteses são tratados separadamente: ( é empurrado, e ) desempilha até encontrar o (.
Vejamos um exemplo simples: 3 + 4 * 2. O algoritmo produzirá a sequência 3 4 2 * +, que pode ser avaliada facilmente com uma pilha.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#define MAX_EXPR 100
#define MAX_STACK 100
typedef struct {
double valores[MAX_STACK];
int topo;
} PilhaValores;
typedef struct {
char operadores[MAX_STACK];
int topo;
} PilhaOperadores;
void empilhaValor(PilhaValores *p, double v) {
if (p->topo < MAX_STACK) {
p->valores[p->topo++] = v;
} else {
fprintf(stderr, "Erro: pilha de valores cheia\n");
exit(1);
}
}
double desempilhaValor(PilhaValores *p) {
if (p->topo > 0) {
return p->valores[--p->topo];
} else {
fprintf(stderr, "Erro: pilha de valores vazia\n");
exit(1);
}
}
void empilhaOperador(PilhaOperadores *p, char op) {
if (p->topo < MAX_STACK) {
p->operadores[p->topo++] = op;
} else {
fprintf(stderr, "Erro: pilha de operadores cheia\n");
exit(1);
}
}
char desempilhaOperador(PilhaOperadores *p) {
if (p->topo > 0) {
return p->operadores[--p->topo];
} else {
fprintf(stderr, "Erro: pilha de operadores vazia\n");
exit(1);
}
}
int precedencia(char op) {
switch (op) {
case '+': case '-': return 1;
case '*': case '/': return 2;
default: return 0;
}
}
// Função que converte infixa para pós-fixa (RPN) e retorna a expressão em string
void infixaParaPosfixa(const char *infixa, char *posfixa) {
PilhaOperadores ops = { .topo = 0 };
int i, j = 0;
for (i = 0; infixa[i] != '\0'; i++) {
char c = infixa[i];
if (isspace(c)) continue;
if (isdigit(c) || c == '.') {
while (isdigit(infixa[i]) || infixa[i] == '.') {
posfixa[j++] = infixa[i++];
}
posfixa[j++] = ' ';
i--; // ajusta para o for incrementar
} else if (c == '(') {
empilhaOperador(&ops, c);
} else if (c == ')') {
while (ops.topo > 0 && ops.operadores[ops.topo-1] != '(') {
posfixa[j++] = desempilhaOperador(&ops);
posfixa[j++] = ' ';
}
if (ops.topo > 0) desempilhaOperador(&ops); // remove '('
} else if (strchr("+-*/", c)) {
while (ops.topo > 0 && precedencia(ops.operadores[ops.topo-1]) >= precedencia(c)) {
posfixa[j++] = desempilhaOperador(&ops);
posfixa[j++] = ' ';
}
empilhaOperador(&ops, c);
} else {
fprintf(stderr, "Caractere inválido: %c\n", c);
exit(1);
}
}
while (ops.topo > 0) {
posfixa[j++] = desempilhaOperador(&ops);
posfixa[j++] = ' ';
}
posfixa[j] = '\0';
}
Operações
Com a expressão em pós-fixa, a avaliação é direta: percorremos os tokens e, quando encontramos um número, empilhamos; quando encontramos um operador, desempilhamos dois valores, aplicamos a operação e empilhamos o resultado. Ao final, o único valor na pilha é o resultado.
Implementaremos funções para cada operação básica (soma, subtração, multiplicação, divisão) e também suporte a potência e raiz quadrada como extras, mostrando como estender o projeto.
double avaliarPosfixa(const char *posfixa) {
PilhaValores vals = { .topo = 0 };
const char *p = posfixa;
while (*p) {
if (isspace(*p)) { p++; continue; }
if (isdigit(*p) || *p == '.') {
char *end;
double num = strtod(p, &end);
empilhaValor(&vals, num);
p = end;
} else {
double b = desempilhaValor(&vals);
double a = desempilhaValor(&vals);
switch (*p) {
case '+': empilhaValor(&vals, a + b); break;
case '-': empilhaValor(&vals, a - b); break;
case '*': empilhaValor(&vals, a * b); break;
case '/':
if (b == 0) {
fprintf(stderr, "Erro: divisão por zero\n");
exit(1);
}
empilhaValor(&vals, a / b); break;
default:
fprintf(stderr, "Operador desconhecido: %c\n", *p);
exit(1);
}
p++;
}
}
if (vals.topo != 1) {
fprintf(stderr, "Erro: expressão inválida\n");
exit(1);
}
return vals.valores[0];
}
Para estender com mais operações, basta adicionar novos casos no switch e, se necessário, ajustar a precedência no parsing. Por exemplo, para potência (^), podemos definir precedência 3 e implementar pow(a,b).
Tratamento de erros
Um bom programa deve lidar com entradas inválidas de forma amigável, sem encerrar abruptamente. Vamos criar uma função de validação que verifica se a expressão contém apenas caracteres permitidos, se os parênteses estão balanceados e se não há operadores consecutivos inválidos (como 2 + * 3).
Além disso, durante a avaliação, capturamos erros como divisão por zero e pilhas vazias. Em vez de usar exit, podemos retornar um código de erro ou usar setjmp/longjmp para um tratamento mais sofisticado, mas aqui manteremos simples com mensagens de erro e retorno de NAN.
int validarExpressao(const char *expr) {
int balance = 0;
char ultimo = '\0';
for (int i = 0; expr[i] != '\0'; i++) {
char c = expr[i];
if (isspace(c)) continue;
if (isdigit(c) || c == '.') {
ultimo = 'n';
} else if (strchr("+-*/", c)) {
if (ultimo == '\0' || ultimo == '(' || strchr("+-*/", ultimo)) {
return 0; // operador sem operando
}
ultimo = c;
} else if (c == '(') {
balance++;
ultimo = c;
} else if (c == ')') {
balance--;
if (balance < 0) return 0;
ultimo = c;
} else {
return 0; // caractere inválido
}
}
return balance == 0 && (ultimo == 'n' || ultimo == ')');
}
Na função principal, chamamos a validação antes de processar. Se falhar, exibimos uma mensagem e pedimos nova entrada. Também tratamos o caso de divisão por zero na avaliação, retornando um valor especial ou imprimindo erro.
Estrutura
Para manter o código organizado, vamos dividir em módulos: calculadora.h (declarações), calculadora.c (implementação) e main.c (interface com o usuário). Isso facilita a manutenção e reutilização.
No cabeçalho, declaramos as funções públicas: double calcular(const char *expr), que faz todo o processamento e retorna o resultado, e int validarExpressao(const char *expr). O arquivo de implementação contém as funções internas de pilha, parsing e avaliação.
// calculadora.h
#ifndef CALCULADORA_H
#define CALCULADORA_H
double calcular(const char *expr);
int validarExpressao(const char *expr);
#endif
// calculadora.c
#include "calculadora.h"
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <ctype.h>
#include <math.h>
// ... (implementação das pilhas e funções) ...
double calcular(const char *expr) {
if (!validarExpressao(expr)) {
fprintf(stderr, "Expressão inválida\n");
return NAN;
}
char posfixa[MAX_EXPR * 2];
infixaParaPosfixa(expr, posfixa);
return avaliarPosfixa(posfixa);
}
No main.c, criamos um loop que lê expressões do usuário, chama calcular e imprime o resultado. Podemos também adicionar um comando de saída, como sair.
// main.c
#include <stdio.h>
#include <string.h>
#include "calculadora.h"
int main() {
char expr[MAX_EXPR];
printf("Calculadora em C\n");
printf("Digite uma expressão (ou 'sair' para encerrar):\n");
while (1) {
printf("> ");
fgets(expr, sizeof(expr), stdin);
expr[strcspn(expr, "\n")] = '\0'; // remove newline
if (strcmp(expr, "sair") == 0) break;
double resultado = calcular(expr);
if (!isnan(resultado)) {
printf("= %g\n", resultado);
}
}
return 0;
}
Essa estrutura modular permite que você adicione novas funcionalidades (como funções trigonométricas) sem bagunçar o código principal.
Boas práticas e observações finais
Ao desenvolver projetos em C, lembre-se de: usar const para parâmetros que não serão modificados, verificar limites de arrays, liberar memória alocada dinamicamente (se usar), e compilar com flags de warning (-Wall -Wextra).
Este projeto pode ser expandido para suportar variáveis, funções matemáticas avançadas, ou até mesmo uma interface gráfica. O importante é ter uma base sólida de parsing e avaliação.
Referências
- Shunting-yard algorithm - Wikipedia
- Reverse Polish notation - Wikipedia
- GNU C Library Manual: String Input Conversion
- strtod - cppreference.com
- C Operator Precedence - cppreference.com
- C Error Handling - TutorialsPoint
Exercícios
- Implemente a função
infixaParaPosfixapara suportar o operador de potência (^) com precedência maior que multiplicação. Teste com2 ^ 3 ^ 2e verifique se o resultado é 512 (associatividade à direita). - Modifique o programa para aceitar números negativos, como
-3 + 5. Dica: trate o sinal de menos como unário quando aparecer no início ou após um operador ou parêntese. - Adicione suporte à função raiz quadrada: o usuário pode digitar
sqrt(9). Dica: reconheça a palavra "sqrt" e trate como uma função unária. - Escreva uma função
avaliarPosfixaque retorne um código de erro em vez de usarexit. Por exemplo, retorne0em caso de sucesso e preencha um ponteiro com o resultado. - Refatore o código para usar alocação dinâmica nas pilhas, permitindo expressões de tamanho arbitrário.
'^' na função precedencia retornando 3. No parsing, quando encontrar ^, empilhe-o sem desempilhar operadores de mesma precedência (para associatividade à direita). Na avaliação, use pow(a,b). Exemplo: int precedencia(char op) { ... case '^': return 3; }- e o token anterior for um operador, início ou (, trate como unário: insira um 0 na saída e empilhe o operador. Ou use um flag. Implementação simples: na função infixaParaPosfixa, verifique se c == '-' e (i == 0 || strchr("+*/(", infixa[i-1])); se sim, adicione 0 à saída e depois o operador.sqrt.int avaliarPosfixa(const char *posfixa, double *resultado) { ... if (erro) return 0; *resultado = valor; return 1; } E em calcular, verifique o retorno.malloc/realloc. Por exemplo: typedef struct { double *valores; int topo; int capacidade; } PilhaValores; E implemente funções de redimensionamento.