Problema com calculadorea em C (Código simples)

Olá, fiz uma calculadora simples, mas o problema que se deu realmente me deixou confuso, no trecho de código a seguir o problema ocorre depois da leitura do primeiro scanf, após coletar o primeiro dado o programa pula o trecho que coleta o dado de tipo char, que seria a operação da calculadora. Na verdade o programa pula absolutamente tudo depois do primeiro scanf e vai direto para o “do while”, eu não vejo razão para isso acontecer. Eu inverti a ordem da coleta dos dados e funcionou. Mas por que não funcionou do jeito que eu apresento aqui? Testei no CodeBlocks e no Visual Studio, meu compilador é o Mingw. Se puderem me ajudar agradeço.

#include <stdio.h>

int main(void)
{
    int numero;
    int numerodeop;
    char operacao;

    printf("Digite o numero: ");
    scanf("%d", &numero);

    /*Problema*/
    printf("\n______________________________\n");
    printf("Digite a operacao: %d ", numero);
    scanf("%c", &operacao);
    /*Problema*/  

    do
    {

        switch (operacao)
        {
        case '+':
            printf("Digite o numero: %d %c ", numero, operacao);
            scanf("%d", &numerodeop);
            numero += numerodeop;
            break;

        case '-':
            printf("Digite o numero: %d %c ", numero, operacao);
            scanf("%d", &numerodeop);
            numero -= numerodeop;
            break;

        case 'x':
            printf("Digite o numero: %d %c ", numero, operacao);
            scanf("%d", &numerodeop);
            numero *= numerodeop;
            break;

        case '/':
            printf("Digite o numero: %d %c ", numero, operacao);
            scanf("%d", &numerodeop);
            numero /= numerodeop;
            break;

        default:
            printf("\ncheguei aqui\n");
            printf("Operacao invalida");
            operacao = 'o';
            break;
        }

        printf("\n______________________________\n");
        printf("Digite a operacao: %d ", numero);
        scanf("%c", &operacao);

        if (operacao == 'o')
            break;

    } while (operacao != '=');

    printf("O resultado de todas as operações feitas foi: ");
    return 0;
}

scanf tem uma série de problemas e muitos são contra-intuitivos.

O principal é que nem sempre ele lê a quebra de linha (que equivale ao ENTER). Então o que acontece é:

  • vc digita um número (um ou mais dígitos) e dá ENTER
  • o buffer de entrada recebe os dígitos e o \n (caractere de quebra de linha, que equivale ao ENTER)
  • scanf("%d", &numero); lê somente os dígitos e deixa a quebra de linha lá
  • a próxima chamada (scanf("%c", &operacao);) vai ler um caractere, então ele pega o próximo que está disponível no buffer (que no caso, é a quebra de linha)

Ou seja, ele leu a quebra de linha, sem nem esperar você digitar. Você pode alterar seu programa para imprimir o valor da operação, só pra ver:

default:
    printf("\ncheguei aqui\n");
    printf("Operacao invalida: [%c] [%d]", operacao, operacao);

O resultado será (no teste abaixo, digitei 1 no número):

Digite o numero: 1

______________________________
Digite a operacao: 1 
cheguei aqui
Operacao invalida: [
] [10]
_______

Repare que ele imprimiu [, pulou a linha e depois ]. Isso porque o caractere corresponde à quebra de linha, que ao ser impressa faz isso (pula para a próxima linha). Depois imprimi também como inteiro, que mostra o respectivo código da tabela ASCII (e 10 é o new line).


Existem vários “malabarismos” que dá para fazer com scanf, mas acaba ficando mais confuso ainda.

Uma solução melhor, porém que dá um pouco mais de trabalho, é usar fgets. O chato é que ela inclui a quebra de linha na string (então você mesmo tem que retirar), e depois ainda precisa converter para número, quando necessário. E depois de ler, você ainda pode limpar o buffer, ou seja, ir lendo e descartando os caracteres restantes que não te interessam (e faça isso até consumir a quebra de linha).

Mas em C não tem jeito, é uma linguagem mais “bruta” e exige que você faça tudo na mão. Uma forma de fazer seria:

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

// limpa o buffer (lê tudo até a quebra de linha, assim a próxima leitura começa "zerada")
void clearBuffer(FILE *fp) {
    int c;
    while ((c = fgetc(fp)) != EOF && c != '\n');
}

// usa fgets para ler o que o usuário digitou, e remove o \n (ENTER) do final
char *fgets_wrapper(char *buffer, size_t buflen, FILE *fp) {
    if (fgets(buffer, buflen, fp) != 0) {
        size_t len = strlen(buffer);
        if (len > 0 && buffer[len - 1] == '\n') {
            // se tem \n, remove
            buffer[len - 1] = '\0';
        } else {
            // se não tem, é porque digitou coisas a mais, então limpa o buffer pra remover esse "lixo"
            clearBuffer(fp);
        }
        return buffer;
    }
    return 0;
}
/*
 * Lê um número e coloca o valor em "n"
 * maxLen indica o máximo de caracteres a serem lidos (incluindo o hífen para números negativos) - os excedentes serão descartados
 *
 * Retorna 1 se deu certo, 0 se deu erro
*/
int read_int(const char *prompt, int *n, int maxLen) {
    printf("%s", prompt);
    int maxBuffer = maxLen + 1; // +1 porque precisa de espaço para o terminador de string
    char *buffer = malloc(maxBuffer);
    // primeiro lê como string
    if (! fgets_wrapper(buffer, maxBuffer, stdin)) {
        printf("Erro ao ler\n");
        free(buffer);
        return 0;
    }
    // depois converte para número
    if (sscanf(buffer, "%d", n) != 1) {
        printf("Não é número: %s\n", buffer);
        free(buffer);
        return 0;
    }
    free(buffer);
    return 1;
}

/*
 * Lê a operação (apenas um caractere) e guarda em "operacao"
 * retorna 1 se conseguiu ler, 0 caso contrário
*/
int ler_operacao(char *operacao, int numero) {
    printf("Digite a operação: %d ", numero);
    // 2 porque precisa do terminador de string
    if (! fgets_wrapper(operacao, 2, stdin)) {
        return 0;
    }
    return 1;
}

int main(void) {
    int numerodeop;
    int numero;
    if( ! read_int("Digite o numero: ", &numero, 10)) {
        printf("Erro ao ler\n");
        return -1;
    }

    char operacao[2];
    while (1) {
        if (! ler_operacao(operacao, numero)) {
            printf("Erro ao ler\n");
            return -1;
        }
        if (*operacao == 'o')
            break;

        switch (*operacao) {
        case '+':
            if( ! read_int("Digite o numero: ", &numerodeop, 10)) {
                printf("Erro ao ler\n");
                return -1;
            }
            numero += numerodeop;
            break;

        case '-':
            if( ! read_int("Digite o numero: ", &numerodeop, 10)) {
                printf("Erro ao ler\n");
                return -1;
            }
            numero -= numerodeop;
            break;

        case 'x':
            if( ! read_int("Digite o numero: ", &numerodeop, 10)) {
                printf("Erro ao ler\n");
                return -1;
            }
            numero *= numerodeop;
            break;

        case '/':
            if( ! read_int("Digite o numero: ", &numerodeop, 10)) {
                printf("Erro ao ler\n");
                return -1;
            }
            numero /= numerodeop;
            break;

        default:
            printf("\ncheguei aqui\n");
            printf("Operacao invalida: [%s]\n", operacao);
            break;
        }
    }

    printf("O resultado de todas as operações feitas foi: ");
    return 0;
}

Muito obrigado pela resposta, aprendi um pouco mais sobre a relação de C com o buffer com a sua ajuda, mas eu percebi que existe um método mais simples, estava revisando uns exercícios e ví que esse problema nunca me ocorreu pois eu fazia a seguinte formatação do comando scanf:

scanf(" %c", &variável);

Ao colocar esse espaço todos os caracteres brancos são ignorados, meu código está funcionando da forma esperada agora.

Novamente obrigado, irei refatorar o código depois, ai irei aplicar o método de limpar o buffer manualmente sob um viés didático.

Usar scanf em exercícios é OK, mas em código sério geralmente usa-se outra coisa (como fgets, ou fgetc, ou uma combinação delas para criar uma função específica, pois depende muito do que vc quer fazer). Leia aqui para entender melhor (e siga também os links que tem no texto, para saber todos os problemas de usar scanf).

Meus códigos em C estão longe do profissional kkkkk. Estou utilizando uma apostila de iniciação em C para utilizar a linguagem para estudar temas como ponteiros, alocação dinâmica e listas encadeadas. Pois vou precisar desses conhecimentos no próximo período de minha faculdade. Obrigado pelas respostas!