Projeto Calculadora

O primeiro projeto do guia: uma calculadora que roda no terminal. Ela pede um número, a operação e outro número, mostra o resultado e pergunta se você quer fazer outra conta. A conta em si é a parte fácil. O desafio é fazer o programa não se perder quando alguém divide por zero, escolhe uma operação que não existe ou digita "seis" no lugar de um número.

Você vai praticar

O que vamos construir

Esta é a calculadora pronta, rodando na máquina da Ana. Tudo o que aparece depois dos dois-pontos nas perguntas foi digitado por ela:

$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10
Operação (+ - * /): +
Segundo número: 5
10 + 5 = 15
Outra conta? (s/n): s
Primeiro número: seis
"seis" não é um número. Tente de novo.      « texto no lugar de número
Primeiro número: 6
Operação (+ - * /): %
Operação desconhecida: %                    « operação que não existe
Operação (+ - * /): *
Segundo número: 1.5
6 * 1.5 = 9
Outra conta? (s/n): s
Primeiro número: 2,5
"2,5" não é um número. Tente de novo.       « no C, o decimal é com ponto
Primeiro número: 2.5
Operação (+ - * /): /
Segundo número: 0
Não dá para dividir por zero.               « divisão por zero
Outra conta? (s/n): n
Até logo!

Vamos chegar lá em quatro versões. No fim de cada uma, o programa compila e roda:

  1. Dois números e um operador (scanf, double, char, else if).
  2. switch e divisão por zero.
  3. Repetir até a pessoa sair (do while).
  4. Validar a entrada (fgets, strtod e funções).

Antes de começar

Você precisa saber criar, compilar e rodar um programa (Olá, Mundo e Compilar com gcc), conhecer double e char e ter visto o básico do scanf. Como fazer cada versão no editor ou no terminal está em Sobre os Projetos. No seu computador, crie a pasta do projeto; o arquivo vai se chamar calculadora.c:

$ mkdir -p c/calculadora
$ cd c/calculadora

Versão 1 · Dois números e um operador

A calculadora pede o primeiro número, a operação e o segundo número. Os números são double (com casas decimais), porque a Ana quer fazer contas como 7.5 / 2. A operação é um caractere (+, -, * ou /), então cabe num char. Para escolher a conta, uma escada de else if.

#include <stdio.h>

int main(void) {
    double a = 0.0;
    double b = 0.0;
    char op = ' ';

    printf("=== Calculadora da Ana ===\n");
    printf("Primeiro número: ");
    scanf("%lf", &a);
    printf("Operação (+ - * /): ");
    scanf(" %c", &op);
    printf("Segundo número: ");
    scanf("%lf", &b);

    if (op == '+') {
        printf("%g + %g = %g\n", a, b, a + b);
    } else if (op == '-') {
        printf("%g - %g = %g\n", a, b, a - b);
    } else if (op == '*') {
        printf("%g * %g = %g\n", a, b, a * b);
    } else if (op == '/') {
        printf("%g / %g = %g\n", a, b, a / b);
    } else {
        printf("Operação desconhecida: %c\n", op);
    }
    return 0;
}
LinhaO que faz
double a = 0.0;Cria a variável do primeiro número, já com zero. Toda variável começa com um valor: em C, uma variável sem valor inicial guarda lixo (veja Variáveis).
char op = ' ';A operação, começando com um espaço. Caracteres vão entre aspas simples: '+'.
printf("Primeiro número: ");A pergunta sem \n: o cursor fica na mesma linha, e o que a pessoa digitar aparece logo depois dos dois-pontos.
scanf("%lf", &a);Espera a pessoa digitar e apertar Enter, converte o texto num double e guarda em a. No scanf, double é %lf ("long float"); o & entrega o endereço de a, para o scanf saber onde guardar (veja Memória e Endereços).
scanf(" %c", &op);Lê um caractere. O espaço antes do %c manda pular espaços e Enter que sobraram na entrada. Sem ele, dá errado (veja o cenário abaixo).
if (op == '+') { … } else if …Compara a operação com cada possibilidade, em ordem, e faz a primeira que combinar. O último else pega o que não combinou com nada.
printf("%g + %g = %g\n", a, b, a + b);%g mostra um double sem zeros inúteis: 15 em vez de 15.000000, 3.75 em vez de 3.750000 (veja Formatos do printf).
$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10
Operação (+ - * /): +
Segundo número: 5
10 + 5 = 15
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 7.5
Operação (+ - * /): /
Segundo número: 2
7.5 / 2 = 3.75
Dá para digitar tudo numa linha

O scanf separa os valores por espaços, não por linhas. Se a Ana digitar 10 * 3 na primeira pergunta, os três valores ficam esperando na entrada, e as perguntas seguintes nem param:

$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10 * 3
Operação (+ - * /): Segundo número: 10 * 3 = 30
No C, o decimal é com ponto, mesmo em português

O scanf lê números com ponto: 2.5. Veja o que acontece quando a Ana digita 2,5:

$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 2,5
Operação (+ - * /): Segundo número: Operação desconhecida: ,

O scanf("%lf", &a) leu o 2 e parou na vírgula, que não faz parte de um número. A vírgula e o 5 ficaram esperando na entrada. O scanf(" %c", &op) pegou a vírgula como operação, e o segundo scanf pegou o 5, sem esperar a Ana digitar. Na versão 4 a calculadora vai recusar o 2,5.

Sem o espaço no " %c": a operação vira um Enter

Este código tem um erro de propósito: scanf("%c", &op);, sem o espaço.

$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10
Operação (+ - * /): Segundo número: +
Operação desconhecida:

O que aconteceu: quando a Ana digitou 10 e apertou Enter, a entrada recebeu 10\n. O %lf leu o 10 e deixou o \n lá. O %c lê qualquer caractere, inclusive o \n, então a operação virou a quebra de linha, sem a Ana digitar nada. Depois, o %lf do segundo número encontrou o +, que não é número, e desistiu. A mensagem mostra a "operação" invisível: uma linha em branco.

Como consertar: um espaço antes do %c: scanf(" %c", &op);. No scanf, um espaço no formato quer dizer "pule todos os espaços, tabs e Enter que vierem". O %d e o %lf já fazem isso sozinhos; o %c não. Mais em Entrada com scanf.

Missão (versão 1): o programa ainda não lê o segundo número e não sabe subtrair. Peça e leia b com scanf("%lf", &b) e acrescente o else if da subtração. A entrada da missão é 10, -, 4: o resultado deve ser 10 - 4 = 6.

#include <stdio.h>

int main(void) {
    double a = 0.0;
    double b = 0.0;
    char op = ' ';

    printf("=== Calculadora da Ana ===\n");
    printf("Primeiro número: ");
    scanf("%lf", &a);
    printf("Operação (+ - * /): ");
    scanf(" %c", &op);
    // peça e leia o segundo número


    if (op == '+') {
        printf("%g + %g = %g\n", a, b, a + b);
    } else if (op == '*') {
        printf("%g * %g = %g\n", a, b, a * b);
    } else if (op == '/') {
        printf("%g / %g = %g\n", a, b, a / b);
    } else {
        printf("Operação desconhecida: %c\n", op);
    }
    return 0;
}
#include <stdio.h>

int main(void) {
    double a = 0.0;
    double b = 0.0;
    char op = ' ';

    printf("=== Calculadora da Ana ===\n");
    printf("Primeiro número: ");
    scanf("%lf", &a);
    printf("Operação (+ - * /): ");
    scanf(" %c", &op);
    printf("Segundo número: ");
    scanf("%lf", &b);

    if (op == '+') {
        printf("%g + %g = %g\n", a, b, a + b);
    } else if (op == '-') {
        printf("%g - %g = %g\n", a, b, a - b);
    } else if (op == '*') {
        printf("%g * %g = %g\n", a, b, a * b);
    } else if (op == '/') {
        printf("%g / %g = %g\n", a, b, a / b);
    } else {
        printf("Operação desconhecida: %c\n", op);
    }
    return 0;
}

Versão 2 · switch e divisão por zero

O que acontece se a Ana dividir por zero na versão 1? Na matemática, não existe resposta. No C, com double, existe uma resposta estranha:

$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 1
Operação (+ - * /): /
Segundo número: 0
1 / 0 = inf
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: -1
Operação (+ - * /): /
Segundo número: 0
-1 / 0 = -inf

inf é "infinito", um valor especial que o double sabe guardar. O programa não quebrou, mas a resposta não serve para a Ana. (Com int seria pior: dividir um inteiro por zero derruba o programa; veja Operadores Aritméticos.) Então, antes de dividir, a calculadora confere se b é zero.

Aproveitamos para trocar a escada de else if por um switch, que foi feito para isso: comparar uma variável com vários valores fixos. A conta vai para a variável resultado, e um único printf mostra qualquer operação.

#include <stdio.h>

int main(void) {
    double a = 0.0;
    double b = 0.0;
    char op = ' ';

    printf("=== Calculadora da Ana ===\n");
    printf("Primeiro número: ");
    scanf("%lf", &a);
    printf("Operação (+ - * /): ");
    scanf(" %c", &op);
    printf("Segundo número: ");
    scanf("%lf", &b);

    double resultado = 0.0;
    switch (op) {
        case '+':
            resultado = a + b;
            break;
        case '-':
            resultado = a - b;
            break;
        case '*':
            resultado = a * b;
            break;
        case '/':
            if (b == 0) {
                printf("Não dá para dividir por zero.\n");
                return 1;
            }
            resultado = a / b;
            break;
        default:
            printf("Operação desconhecida: %c\n", op);
            return 1;
    }
    printf("%g %c %g = %g\n", a, op, b, resultado);
    return 0;
}
switch (op)
"Conforme o valor de op…". O switch só aceita valores inteiros, e um char é um inteiro pequeno (o código do caractere).
case '+':
"…se for '+', comece aqui."
break;
"Terminei, saia do switch." Sem ele, o programa continua no case seguinte (veja o cenário abaixo).
if (b == 0) { … return 1; }
Dentro do case '/': se o divisor é zero, avisa e encerra o programa com o código de saída 1, que diz ao sistema que algo deu errado (0 é "tudo certo").
default:
"Se não for nenhum dos anteriores." Mostra o erro e também sai com 1.
printf("%g %c %g = %g\n", a, op, b, resultado);
Mostra a conta com a operação escolhida: %c é o formato de um char.
$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10
Operação (+ - * /): /
Segundo número: 4
10 / 4 = 2.5
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 1
Operação (+ - * /): /
Segundo número: 0
Não dá para dividir por zero.
$ echo $?
1                                « o código de saída do return 1
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 7
Operação (+ - * /): %
Segundo número: 2
Operação desconhecida: %
$ echo $?
1

O echo $? é um comando do terminal que mostra o código de saída do último programa. Não se preocupe com ele; é só para você ver o 1 do return 1 (mais em Encerrar o Programa).

Esqueceu o break: a conta "escorrega" para o próximo case

Este código tem um erro de propósito: o break depois da soma foi apagado.

        case '+':
            resultado = a + b;
        case '-':
            resultado = a - b;
            break;
$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora
calculadora.c: In function ‘main’:
calculadora.c:19:23: warning: this statement may fall through [-Wimplicit-fallthrough=]
   19 |             resultado = a + b;
      |             ~~~~~~~~~~^~~~~~~
calculadora.c:20:9: note: here
   20 |         case '-':
      |         ^~~~
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10
Operação (+ - * /): +
Segundo número: 5
10 + 5 = 5

O que significa: "esta instrução pode cair para o próximo" (fall through, "cair através"). Sem o break, depois de somar o programa continuou no case '-' e trocou o resultado por 10 - 5. A conta "10 + 5 = 5" está errada e o programa rodou sem reclamar. Só o aviso do -Wextra denunciou o problema: por isso o guia sempre compila com ele (veja Avisos).

Como consertar: um break; no fim de cada case. Veja mais em switch.

Missão (versão 2): esta versão divide por zero e mostra 1 / 0 = inf. Dentro do case '/', antes da conta, confira se b == 0: se for, mostre Não dá para dividir por zero. e saia com return 1;. A entrada da missão é 1, /, 0.

#include <stdio.h>

int main(void) {
    double a = 0.0;
    double b = 0.0;
    char op = ' ';

    printf("=== Calculadora da Ana ===\n");
    printf("Primeiro número: ");
    scanf("%lf", &a);
    printf("Operação (+ - * /): ");
    scanf(" %c", &op);
    printf("Segundo número: ");
    scanf("%lf", &b);

    double resultado = 0.0;
    switch (op) {
        case '+':
            resultado = a + b;
            break;
        case '-':
            resultado = a - b;
            break;
        case '*':
            resultado = a * b;
            break;
        case '/':
            // antes de dividir, confira se b é zero
            resultado = a / b;
            break;
        default:
            printf("Operação desconhecida: %c\n", op);
            return 1;
    }
    printf("%g %c %g = %g\n", a, op, b, resultado);
    return 0;
}
#include <stdio.h>

int main(void) {
    double a = 0.0;
    double b = 0.0;
    char op = ' ';

    printf("=== Calculadora da Ana ===\n");
    printf("Primeiro número: ");
    scanf("%lf", &a);
    printf("Operação (+ - * /): ");
    scanf(" %c", &op);
    printf("Segundo número: ");
    scanf("%lf", &b);

    double resultado = 0.0;
    switch (op) {
        case '+':
            resultado = a + b;
            break;
        case '-':
            resultado = a - b;
            break;
        case '*':
            resultado = a * b;
            break;
        case '/':
            if (b == 0) {
                printf("Não dá para dividir por zero.\n");
                return 1;
            }
            resultado = a / b;
            break;
        default:
            printf("Operação desconhecida: %c\n", op);
            return 1;
    }
    printf("%g %c %g = %g\n", a, op, b, resultado);
    return 0;
}

Versão 3 · Repetir até a pessoa sair

Até agora, a calculadora faz uma conta e termina. Para fazer outra, a Ana tem de rodar o programa de novo. A versão 3 pergunta "Outra conta?" e repete enquanto a resposta for s. Como a calculadora sempre faz pelo menos uma conta, o laço certo é o do while: ele roda o bloco primeiro e testa a condição depois.

Duas mudanças no switch: cada case mostra o seu resultado, e a divisão por zero não encerra mais o programa com return 1 (isso pularia o "Outra conta?"). Ela só avisa, e o laço continua.

#include <stdio.h>

int main(void) {
    char continuar = 's';

    printf("=== Calculadora da Ana ===\n");
    do {
        double a = 0.0;
        double b = 0.0;
        char op = ' ';

        printf("Primeiro número: ");
        scanf("%lf", &a);
        printf("Operação (+ - * /): ");
        scanf(" %c", &op);
        printf("Segundo número: ");
        scanf("%lf", &b);

        switch (op) {
            case '+':
                printf("%g + %g = %g\n", a, b, a + b);
                break;
            case '-':
                printf("%g - %g = %g\n", a, b, a - b);
                break;
            case '*':
                printf("%g * %g = %g\n", a, b, a * b);
                break;
            case '/':
                if (b == 0) {
                    printf("Não dá para dividir por zero.\n");
                } else {
                    printf("%g / %g = %g\n", a, b, a / b);
                }
                break;
            default:
                printf("Operação desconhecida: %c\n", op);
        }

        printf("Outra conta? (s/n): ");
        if (scanf(" %c", &continuar) != 1) {
            continuar = 'n';  // a entrada acabou: sai do laço
        }
    } while (continuar == 's' || continuar == 'S');

    printf("Até logo!\n");
    return 0;
}
do { … } while (continuar == 's' || continuar == 'S');
"Faça isto; depois, se a resposta for s ou S, faça de novo." O || é o "ou" (veja Operadores Lógicos). Repare no ; depois do while (…): no do while ele é obrigatório.
double a = 0.0; (dentro do do)
As variáveis da conta agora nascem dentro do laço: cada conta começa do zero (veja Escopo). Só continuar fica fora, porque o while lá embaixo precisa dela.
if (scanf(" %c", &continuar) != 1)
O scanf devolve quantos valores conseguiu ler. Se a entrada acabou (a pessoa apertou Ctrl+D), ele devolve EOF, que não é 1: então o programa sai do laço em vez de repetir para sempre.
$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10
Operação (+ - * /): +
Segundo número: 5
10 + 5 = 15
Outra conta? (s/n): s
Primeiro número: 3
Operação (+ - * /): *
Segundo número: 4
3 * 4 = 12
Outra conta? (s/n): s
Primeiro número: 1
Operação (+ - * /): /
Segundo número: 0
Não dá para dividir por zero.
Outra conta? (s/n): n
Até logo!
Texto no lugar de número: as perguntas são "atropeladas"
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: dez
Operação (+ - * /): Segundo número: Operação desconhecida: d
Outra conta? (s/n): Até logo!

O que aconteceu: o %lf não conseguiu ler dez e deixou o texto na entrada; a ficou com 0. O " %c" da operação pegou o d. O %lf do segundo número esbarrou no e e também falhou. O " %c" do "Outra conta?" pegou o e, que não é s, e o programa terminou sem a Ana ter digitado nada além de dez.

Como consertar: o scanf lê "o que der" e deixa o resto. Para validar, é melhor ler a linha inteira e só depois ver se ela é um número. É a versão 4.

Missão (versão 3): o programa faz uma conta, pergunta "Outra conta?", mas não repete. Coloque tudo, de double a até a leitura de continuar, dentro de do { … } while (continuar == 's' || continuar == 'S');. A entrada da missão faz 10 + 5, responde s, faz 3 * 4 e responde n.

#include <stdio.h>

int main(void) {
    char continuar = 's';

    printf("=== Calculadora da Ana ===\n");

    double a = 0.0;
    double b = 0.0;
    char op = ' ';

    printf("Primeiro número: ");
    scanf("%lf", &a);
    printf("Operação (+ - * /): ");
    scanf(" %c", &op);
    printf("Segundo número: ");
    scanf("%lf", &b);

    switch (op) {
        case '+':
            printf("%g + %g = %g\n", a, b, a + b);
            break;
        case '-':
            printf("%g - %g = %g\n", a, b, a - b);
            break;
        case '*':
            printf("%g * %g = %g\n", a, b, a * b);
            break;
        case '/':
            if (b == 0) {
                printf("Não dá para dividir por zero.\n");
            } else {
                printf("%g / %g = %g\n", a, b, a / b);
            }
            break;
        default:
            printf("Operação desconhecida: %c\n", op);
    }

    printf("Outra conta? (s/n): ");
    if (scanf(" %c", &continuar) != 1) {
        continuar = 'n';  // a entrada acabou
    }
    // coloque tudo, de "double a" até aqui, dentro de um do { ... } while

    printf("Até logo!\n");
    return 0;
}
#include <stdio.h>

int main(void) {
    char continuar = 's';

    printf("=== Calculadora da Ana ===\n");
    do {
        double a = 0.0;
        double b = 0.0;
        char op = ' ';

        printf("Primeiro número: ");
        scanf("%lf", &a);
        printf("Operação (+ - * /): ");
        scanf(" %c", &op);
        printf("Segundo número: ");
        scanf("%lf", &b);

        switch (op) {
            case '+':
                printf("%g + %g = %g\n", a, b, a + b);
                break;
            case '-':
                printf("%g - %g = %g\n", a, b, a - b);
                break;
            case '*':
                printf("%g * %g = %g\n", a, b, a * b);
                break;
            case '/':
                if (b == 0) {
                    printf("Não dá para dividir por zero.\n");
                } else {
                    printf("%g / %g = %g\n", a, b, a / b);
                }
                break;
            default:
                printf("Operação desconhecida: %c\n", op);
        }

        printf("Outra conta? (s/n): ");
        if (scanf(" %c", &continuar) != 1) {
            continuar = 'n';  // a entrada acabou: sai do laço
        }
    } while (continuar == 's' || continuar == 'S');

    printf("Até logo!\n");
    return 0;
}

Versão 4 · Validar a entrada com fgets e strtod

A ideia da versão final: nunca ler o número direto do teclado. Primeiro, a calculadora lê a linha inteira que a pessoa digitou, como texto, com fgets. Depois, tenta converter o texto em número com strtod ("string to double", texto para double). Se sobrar alguma coisa que não é número (seis, 2,5, 12abc), ela recusa e pergunta de novo.

Como agora há três perguntas com o mesmo jeito de ler (número, operação, "outra conta?"), o código ficou repetitivo. Hora de criar funções: cada uma faz uma coisa e tem um nome que diz o quê.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Mostra a pergunta e lê uma linha inteira em "linha".
// Se a entrada acabou (Ctrl+D), encerra o programa.
void ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        printf("\nAté logo!\n");
        exit(0);
    }
    linha[strcspn(linha, "\n")] = '\0';  // tira o Enter do fim
}

// Repete a pergunta até a pessoa digitar um número válido.
double ler_numero(const char *pergunta) {
    char linha[100];
    while (1) {
        ler_linha(pergunta, linha, sizeof linha);
        char *fim = NULL;
        double numero = strtod(linha, &fim);
        if (fim != linha && *fim == '\0') {
            return numero;  // converteu tudo: é um número
        }
        printf("\"%s\" não é um número. Tente de novo.\n", linha);
    }
}

// Repete a pergunta até a pessoa digitar + - * ou /.
char ler_operacao(void) {
    char linha[100];
    while (1) {
        ler_linha("Operação (+ - * /): ", linha, sizeof linha);
        if (strlen(linha) == 1 && strchr("+-*/", linha[0]) != NULL) {
            return linha[0];
        }
        printf("Operação desconhecida: %s\n", linha);
    }
}

int main(void) {
    char resposta[100] = "s";

    printf("=== Calculadora da Ana ===\n");
    while (resposta[0] == 's' || resposta[0] == 'S') {
        double a = ler_numero("Primeiro número: ");
        char op = ler_operacao();
        double b = ler_numero("Segundo número: ");

        if (op == '/' && b == 0) {
            printf("Não dá para dividir por zero.\n");
        } else {
            double r = 0.0;
            switch (op) {
                case '+': r = a + b; break;
                case '-': r = a - b; break;
                case '*': r = a * b; break;
                case '/': r = a / b; break;
            }
            printf("%g %c %g = %g\n", a, op, b, r);
        }
        ler_linha("Outra conta? (s/n): ", resposta, sizeof resposta);
    }
    printf("Até logo!\n");
    return 0;
}
TrechoO que faz
#include <stdlib.h> e <string.h>stdlib.h traz strtod e exit; string.h traz strcspn, strlen e strchr (veja Funções de String).
void ler_linha(const char *pergunta, char *linha, int tamanho)Mostra a pergunta e lê uma linha em linha. Recebe o tamanho do array para o fgets nunca escrever além dele.
fgets(linha, tamanho, stdin) == NULLO fgets devolve NULL quando a entrada acabou (Ctrl+D). Aí a calculadora se despede e encerra com exit(0).
linha[strcspn(linha, "\n")] = '\0';O fgets guarda o Enter (\n) no fim da linha. strcspn acha a posição do \n, e o '\0' corta a string ali.
double numero = strtod(linha, &fim);Converte o começo da linha num double e aponta fim para o primeiro caractere que não entrou no número. Em "12abc", fim aponta para o a.
fim != linha && *fim == '\0'Duas condições: fim != linha = "converteu pelo menos um caractere" (em "seis", fim fica no começo); *fim == '\0' = "não sobrou nada depois do número".
while (1) { … return numero; … }Um laço "para sempre", que só termina pelo return, quando o número é válido.
strlen(linha) == 1 && strchr("+-*/", linha[0]) != NULLA operação tem de ser exatamente um caractere, e esse caractere tem de estar na string "+-*/". O strchr procura um caractere numa string e devolve NULL se não achar.
char resposta[100] = "s";O "Outra conta?" também é lido com fgets. Começa com "s" para o while entrar na primeira vez.
Por que não misturar scanf e fgets?

O scanf("%lf") deixa o Enter na entrada. Se um fgets vier depois, ele lê esse Enter que sobrou e devolve uma linha vazia, sem esperar a pessoa. Por isso a versão 4 lê tudo com fgets: os números, a operação e o "Outra conta?". Mais em Ler uma Linha.

$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10
Operação (+ - * /): +
Segundo número: 5
10 + 5 = 15
Outra conta? (s/n): s
Primeiro número: seis
"seis" não é um número. Tente de novo.
Primeiro número: 6
Operação (+ - * /): %
Operação desconhecida: %
Operação (+ - * /): *
Segundo número: 1.5
6 * 1.5 = 9
Outra conta? (s/n): s
Primeiro número: 12abc
"12abc" não é um número. Tente de novo.     « sobrou "abc" depois do 12
Primeiro número: 12
Operação (+ - * /): x
Operação desconhecida: x
Operação (+ - * /): /
Segundo número: 0
Não dá para dividir por zero.
Outra conta? (s/n): n
Até logo!

Missão (versão 4): a função ler_numero converte com strtod, mas aceita qualquer coisa: abc vira 0. Passe um char *fim para o strtod e só devolva o número se fim != linha && *fim == '\0'; senão, mostre "abc" não é um número. Tente de novo. (com o texto digitado) e deixe o laço repetir a pergunta. A entrada da missão é abc, 10, +, 5, n.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Mostra a pergunta e lê uma linha inteira em "linha".
// Se a entrada acabou (Ctrl+D), encerra o programa.
void ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        printf("\nAté logo!\n");
        exit(0);
    }
    linha[strcspn(linha, "\n")] = '\0';  // tira o Enter do fim
}

// Repete a pergunta até a pessoa digitar um número válido.
double ler_numero(const char *pergunta) {
    char linha[100];
    while (1) {
        ler_linha(pergunta, linha, sizeof linha);
        double numero = strtod(linha, NULL);
        // confira se a linha inteira era um número;
        // se não era, mostre a mensagem e repita a pergunta
        return numero;
    }
}

// Repete a pergunta até a pessoa digitar + - * ou /.
char ler_operacao(void) {
    char linha[100];
    while (1) {
        ler_linha("Operação (+ - * /): ", linha, sizeof linha);
        if (strlen(linha) == 1 && strchr("+-*/", linha[0]) != NULL) {
            return linha[0];
        }
        printf("Operação desconhecida: %s\n", linha);
    }
}

int main(void) {
    char resposta[100] = "s";

    printf("=== Calculadora da Ana ===\n");
    while (resposta[0] == 's' || resposta[0] == 'S') {
        double a = ler_numero("Primeiro número: ");
        char op = ler_operacao();
        double b = ler_numero("Segundo número: ");

        if (op == '/' && b == 0) {
            printf("Não dá para dividir por zero.\n");
        } else {
            double r = 0.0;
            switch (op) {
                case '+': r = a + b; break;
                case '-': r = a - b; break;
                case '*': r = a * b; break;
                case '/': r = a / b; break;
            }
            printf("%g %c %g = %g\n", a, op, b, r);
        }
        ler_linha("Outra conta? (s/n): ", resposta, sizeof resposta);
    }
    printf("Até logo!\n");
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Mostra a pergunta e lê uma linha inteira em "linha".
// Se a entrada acabou (Ctrl+D), encerra o programa.
void ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        printf("\nAté logo!\n");
        exit(0);
    }
    linha[strcspn(linha, "\n")] = '\0';  // tira o Enter do fim
}

// Repete a pergunta até a pessoa digitar um número válido.
double ler_numero(const char *pergunta) {
    char linha[100];
    while (1) {
        ler_linha(pergunta, linha, sizeof linha);
        char *fim = NULL;
        double numero = strtod(linha, &fim);
        if (fim != linha && *fim == '\0') {
            return numero;  // converteu tudo: é um número
        }
        printf("\"%s\" não é um número. Tente de novo.\n", linha);
    }
}

// Repete a pergunta até a pessoa digitar + - * ou /.
char ler_operacao(void) {
    char linha[100];
    while (1) {
        ler_linha("Operação (+ - * /): ", linha, sizeof linha);
        if (strlen(linha) == 1 && strchr("+-*/", linha[0]) != NULL) {
            return linha[0];
        }
        printf("Operação desconhecida: %s\n", linha);
    }
}

int main(void) {
    char resposta[100] = "s";

    printf("=== Calculadora da Ana ===\n");
    while (resposta[0] == 's' || resposta[0] == 'S') {
        double a = ler_numero("Primeiro número: ");
        char op = ler_operacao();
        double b = ler_numero("Segundo número: ");

        if (op == '/' && b == 0) {
            printf("Não dá para dividir por zero.\n");
        } else {
            double r = 0.0;
            switch (op) {
                case '+': r = a + b; break;
                case '-': r = a - b; break;
                case '*': r = a * b; break;
                case '/': r = a / b; break;
            }
            printf("%g %c %g = %g\n", a, op, b, r);
        }
        ler_linha("Outra conta? (s/n): ", resposta, sizeof resposta);
    }
    printf("Até logo!\n");
    return 0;
}

O programa completo

Esta é a calculadora final, a mesma da versão 4. Copie, compile com gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora e use à vontade:

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Mostra a pergunta e lê uma linha inteira em "linha".
// Se a entrada acabou (Ctrl+D), encerra o programa.
void ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        printf("\nAté logo!\n");
        exit(0);
    }
    linha[strcspn(linha, "\n")] = '\0';  // tira o Enter do fim
}

// Repete a pergunta até a pessoa digitar um número válido.
double ler_numero(const char *pergunta) {
    char linha[100];
    while (1) {
        ler_linha(pergunta, linha, sizeof linha);
        char *fim = NULL;
        double numero = strtod(linha, &fim);
        if (fim != linha && *fim == '\0') {
            return numero;  // converteu tudo: é um número
        }
        printf("\"%s\" não é um número. Tente de novo.\n", linha);
    }
}

// Repete a pergunta até a pessoa digitar + - * ou /.
char ler_operacao(void) {
    char linha[100];
    while (1) {
        ler_linha("Operação (+ - * /): ", linha, sizeof linha);
        if (strlen(linha) == 1 && strchr("+-*/", linha[0]) != NULL) {
            return linha[0];
        }
        printf("Operação desconhecida: %s\n", linha);
    }
}

int main(void) {
    char resposta[100] = "s";

    printf("=== Calculadora da Ana ===\n");
    while (resposta[0] == 's' || resposta[0] == 'S') {
        double a = ler_numero("Primeiro número: ");
        char op = ler_operacao();
        double b = ler_numero("Segundo número: ");

        if (op == '/' && b == 0) {
            printf("Não dá para dividir por zero.\n");
        } else {
            double r = 0.0;
            switch (op) {
                case '+': r = a + b; break;
                case '-': r = a - b; break;
                case '*': r = a * b; break;
                case '/': r = a / b; break;
            }
            printf("%g %c %g = %g\n", a, op, b, r);
        }
        ler_linha("Outra conta? (s/n): ", resposta, sizeof resposta);
    }
    printf("Até logo!\n");
    return 0;
}

Repare no que o projeto juntou: scanf, else if, switch, do while, while, fgets, strtod, funções com parâmetros e retorno. Nenhum desses assuntos é difícil sozinho; o projeto mostra como eles se encaixam.

Rodando no laboratório

A sessão do começo da página, na máquina da Ana, com a versão final:

$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 10
Operação (+ - * /): +
Segundo número: 5
10 + 5 = 15
Outra conta? (s/n): s
Primeiro número: seis
"seis" não é um número. Tente de novo.
Primeiro número: 6
Operação (+ - * /): %
Operação desconhecida: %
Operação (+ - * /): *
Segundo número: 1.5
6 * 1.5 = 9
Outra conta? (s/n): s
Primeiro número: 2,5
"2,5" não é um número. Tente de novo.
Primeiro número: 2.5
Operação (+ - * /): /
Segundo número: 0
Não dá para dividir por zero.
Outra conta? (s/n): n
Até logo!

Desafios extras

  1. Resto da divisão. Acrescente a operação %. Cuidado: o % do C só funciona com inteiros. Para double, use fmod(a, b), do <math.h> (veja Matemática). E o resto da divisão por zero também não existe.
  2. Potência. Acrescente ^ com pow(a, b), também do <math.h>.
  3. Histórico. Guarde os resultados num array double historico[100] e mostre todos antes do "Até logo!" (veja Arrays).
  4. Duas casas decimais. Mostre o resultado com %.2f no lugar de %g e compare 10 / 3 nos dois formatos.
  5. Números estranhos. O strtod aceita 1e3 (mil), inf e nan. Experimente na calculadora. Como você recusaria inf e nan? (Dica: isfinite, do <math.h>.)
O erro que aparece no desafio 1 (no terminal)
$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccua0ZF3.o: na função "main":
calculadora.c:(.text+0x39e): referência não definida para "fmod"
collect2: error: ld returned 1 exit status
$ gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora -lm
$ ./calculadora
=== Calculadora da Ana ===
Primeiro número: 7.5
Operação (+ - * /): %
Segundo número: 2
7.5 % 2 = 1.5
Outra conta? (s/n): n
Até logo!

O que significa: o código compilou, mas o ligador (ld, a etapa que junta o seu programa com as bibliotecas) não achou a função fmod: "referência não definida" (em inglês, undefined reference to). No Linux, as funções de math.h ficam numa biblioteca separada, a libm, que o gcc não junta sozinho.

Como consertar: acrescente -lm ("link math", junte a biblioteca de matemática) no fim do comando. O editor do guia já junta a libm sozinho, por isso a missão abaixo funciona sem ele. Veja Matemática e Bibliotecas.

Desafio 1 no editor: acrescente a operação % com fmod. Mude quatro lugares: o #include <math.h>, a string "+-*/" de ler_operacao, o switch e o if da divisão por zero (que agora vale para / e para %). Com a entrada da missão, a calculadora deve mostrar 10 % 3 = 1, 7.5 % 2 = 1.5 e recusar 7 % 0.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Mostra a pergunta e lê uma linha inteira em "linha".
// Se a entrada acabou (Ctrl+D), encerra o programa.
void ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        printf("\nAté logo!\n");
        exit(0);
    }
    linha[strcspn(linha, "\n")] = '\0';  // tira o Enter do fim
}

// Repete a pergunta até a pessoa digitar um número válido.
double ler_numero(const char *pergunta) {
    char linha[100];
    while (1) {
        ler_linha(pergunta, linha, sizeof linha);
        char *fim = NULL;
        double numero = strtod(linha, &fim);
        if (fim != linha && *fim == '\0') {
            return numero;  // converteu tudo: é um número
        }
        printf("\"%s\" não é um número. Tente de novo.\n", linha);
    }
}

// Repete a pergunta até a pessoa digitar + - * ou /.
char ler_operacao(void) {
    char linha[100];
    while (1) {
        ler_linha("Operação (+ - * /): ", linha, sizeof linha);
        if (strlen(linha) == 1 && strchr("+-*/", linha[0]) != NULL) {
            return linha[0];
        }
        printf("Operação desconhecida: %s\n", linha);
    }
}

int main(void) {
    char resposta[100] = "s";

    printf("=== Calculadora da Ana ===\n");
    while (resposta[0] == 's' || resposta[0] == 'S') {
        double a = ler_numero("Primeiro número: ");
        char op = ler_operacao();
        double b = ler_numero("Segundo número: ");

        if (op == '/' && b == 0) {
            printf("Não dá para dividir por zero.\n");
        } else {
            double r = 0.0;
            switch (op) {
                case '+': r = a + b; break;
                case '-': r = a - b; break;
                case '*': r = a * b; break;
                case '/': r = a / b; break;
            }
            printf("%g %c %g = %g\n", a, op, b, r);
        }
        ler_linha("Outra conta? (s/n): ", resposta, sizeof resposta);
    }
    printf("Até logo!\n");
    return 0;
}
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <math.h>

// Mostra a pergunta e lê uma linha inteira em "linha".
// Se a entrada acabou (Ctrl+D), encerra o programa.
void ler_linha(const char *pergunta, char *linha, int tamanho) {
    printf("%s", pergunta);
    if (fgets(linha, tamanho, stdin) == NULL) {
        printf("\nAté logo!\n");
        exit(0);
    }
    linha[strcspn(linha, "\n")] = '\0';  // tira o Enter do fim
}

// Repete a pergunta até a pessoa digitar um número válido.
double ler_numero(const char *pergunta) {
    char linha[100];
    while (1) {
        ler_linha(pergunta, linha, sizeof linha);
        char *fim = NULL;
        double numero = strtod(linha, &fim);
        if (fim != linha && *fim == '\0') {
            return numero;  // converteu tudo: é um número
        }
        printf("\"%s\" não é um número. Tente de novo.\n", linha);
    }
}

// Repete a pergunta até a pessoa digitar + - * ou /.
char ler_operacao(void) {
    char linha[100];
    while (1) {
        ler_linha("Operação (+ - * /): ", linha, sizeof linha);
        if (strlen(linha) == 1 && strchr("+-*/%", linha[0]) != NULL) {
            return linha[0];
        }
        printf("Operação desconhecida: %s\n", linha);
    }
}

int main(void) {
    char resposta[100] = "s";

    printf("=== Calculadora da Ana ===\n");
    while (resposta[0] == 's' || resposta[0] == 'S') {
        double a = ler_numero("Primeiro número: ");
        char op = ler_operacao();
        double b = ler_numero("Segundo número: ");

        if ((op == '/' || op == '%') && b == 0) {
            printf("Não dá para dividir por zero.\n");
        } else {
            double r = 0.0;
            switch (op) {
                case '+': r = a + b; break;
                case '-': r = a - b; break;
                case '*': r = a * b; break;
                case '/': r = a / b; break;
                case '%': r = fmod(a, b); break;
            }
            printf("%g %c %g = %g\n", a, op, b, r);
        }
        ler_linha("Outra conta? (s/n): ", resposta, sizeof resposta);
    }
    printf("Até logo!\n");
    return 0;
}

Exercícios

1. Por que a calculadora usa scanf(" %c", &op), com um espaço antes do %c?

2. Na versão 2, a Ana apagou o break depois de resultado = a * b;. O que acontece com 6 * 2?

3. Com strtod(linha, &fim) e linha igual a "12abc", para onde aponta fim?

4. Por que a versão 3 usa do while e não while?

5. Complete o formato do scanf que lê um double na variável a:

scanf("", &a);

6. No terminal, que opção você acrescenta ao fim do comando do gcc quando aparece referência não definida para "fmod"?

gcc -std=c23 -Wall -Wextra calculadora.c -o calculadora

Resumo

  • scanf("%lf", &a) lê um double; scanf(" %c", &op) lê um caractere, e o espaço pula o Enter que sobrou.
  • O switch compara uma variável com vários valores; cada case termina com break, e o -Wextra avisa quando falta.
  • Dividir um double por zero dá inf: confira o divisor antes.
  • do { … } while (…); repete pelo menos uma vez; confira o retorno do scanf para não repetir para sempre quando a entrada acaba.
  • Para validar: leia a linha com fgets, converta com strtod(linha, &fim) e aceite só se fim != linha && *fim == '\0'.
  • fmod, pow e as outras de math.h pedem -lm no gcc.

Próximo projeto: Jogo de Adivinhação.