cblc/aulas/11-scanf/README.org

14 KiB

Curso Básico da Linguagem C

Aula 11: Leitura da entrada padrão com 'scanf'

Vídeo desta aula

A entrada padrão (stream stdin ou descritor de arquivos 0) pode estar ligada…

  • A um terminal para leitura interativa (digitação);
  • A um arquivo via redirecionamento de leitura;
  • À saída de um processo via pipe ou arquivo FIFO;
  • A um arquivo de forma programática (ex: funções freopen, dup2).

Para verificar se o fluxo stdin está ligado a um terminal, nós podemos utilizar a função isatty (unistd.h):

int isatty(int fd);

Onde int fd é o descritor de arquivos testado. O retorno será o inteiro 1, se fd estiver ligado a um terminal, ou 0, se estiver ligado a um pipe ou redirecionado para um arquivo.

Exemplo (test-tty.c):

#include <stdio.h>
#include <unistd.h>

int main(void) {
    if (isatty(0)) {
        puts("STDIN ligada ao terminal.");
    } else {
        puts("STDIN ligada a um arquivo ou pipe.");
    }

    return 0;
}

Compilação e testes:

:~$ gcc -Wall test-tty.c
:~$ ./a.out
STDIN ligada ao terminal.
:~$ ./a.out < /dev/null
STDIN ligada a um arquivo ou pipe.
:~$ : | ./a.out
STDIN ligada a um arquivo ou pipe.

O comando interno do shell : é o comando nulo, que não faz nada e sempre termina com sucesso.

Para associar programaticamente a entrada padrão a um arquivo (de nome recebido como argumento, por exemplo), nós podemos utilizar a função freopen para abrir um arquivo para leitura através de um fluxo (stream) previamente aberto: no caso o stream stdin

FILE *freopen(const char *caminho, const char *modo, FILE *stream);

Onde:

  • caminho: string do nome do arquivo;
  • modo: string de modo de abertura ("r", "w", "rw");
  • stream: um fluxo de dados previamente aberto.

Retorna um ponteiro para o tipo FILE ou NULL, em caso de erro.

O propósito original desta função é alterar o arquivo associado a um dos fluxos padrão (stdin, stdout ou stderr).

Exemplo (redir-stdin.c):

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

int main(int argc, char **argv) {

    // Diretório 'fd' do processo de 'a.out'...
    puts("Antes do redirecionamento...");
    system("ls -l /proc/$(pidof a.out)/fd");

    // Nome do arquivo...
    char *filename;
    if (argc > 1) {
        // Primeiro argumento...
        filename = argv[1];
    } else {
        // Dispositivo nulo...
        filename = "/dev/null";
    }

    // Redirecionamento...
    FILE* stream = freopen(filename, "r", stdin);
    if (!stream) {
        perror("freopen");
        return EXIT_FAILURE;
    }

    // Diretório 'fd' do processo de 'a.out'...
    puts("Depois do redirecionamento...");
    system("ls -l /proc/$(pidof a.out)/fd");

    return EXIT_SUCCESS;
}

Compilação…

:~$ gcc -Wall redir-stdin.c

Executando sem argumentos (redireciona para /dev/null):

:~$ ./a.out
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Depois do redirecionamento...
total 0
lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /dev/null
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0

Executando com argumentos (nome do arquivo no primeiro argumento):

:~$ ./a.out /etc/shells
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:16 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0
Depois do redirecionamento...
total 0
lr-x------ 1 blau blau 64 abr 24 09:16 0 -> /etc/shells
lrwx------ 1 blau blau 64 abr 24 09:16 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:16 2 -> /dev/pts/0

No caso de um erro de abertura (ex: arquivo "banana" não existe):

:~$ ./a.out banana
Antes do redirecionamento...
total 0
lrwx------ 1 blau blau 64 abr 24 09:21 0 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:21 1 -> /dev/pts/0
lrwx------ 1 blau blau 64 abr 24 09:21 2 -> /dev/pts/0
freopen: No such file or directory

Leitura interativa da entrada padrão

Seja o conteúdo de um arquivo ou uma entrada digitada no terminal, a leitura da entrada padrão pode ser feita com várias formas e com diversas funções da biblioteca padrão:

  • Leitura de caracteres: getchar, getc(stdin) e fgetc(stdin);
  • Leitura de linhas (até \n): fgets(buf, size, stdin), getline(&buf, &size, stdin);
  • Leitura formatada de linhas: scanf, fscanf(stdin, ...);

Mas, como a leitura das linhas de um arquivo é mais controlada (o conteúdo é previamente conhecido), nós vamos concentrar nossa atenção na leitura de linhas digitadas no terminal por um usuário (leitura interativa). Para isso, nós estudaremos três funções (nesta e nas próximas aulas):

  • Leitura formatada de linhas: scanf;
  • Leitura de linhas: fgets;
  • Leitura de caracteres: chamada de sistema read.

Leitura formatada de linhas com 'scanf'

A função scanf lê os bytes de uma linha enquanto encontra casamentos com os especificadores definidos em uma string de formato (como no printf). A parte da linha que encontrar casamento é passada para um ou mais endereços de variáveis (ou endereços em ponteiros) e tudo que vier depois permanece no buffer de entrada do processo.

Outros pontos importantes:

  • Cada especificador de formato é casado com apenas uma palavra da linha lida (palavras são cadeias de caracteres delimitadas por espaços, tabulações e quebras de linha).
  • Retorna o número de casamentos com as especificações de formatos que foram encontrados e atribuídos (o que pode ser menor do que a quantidade de especificadores fornecidos).
  • Em caso de erro, retorna zero ou EOF (com vários significados possíveis).
  • Altera a variável errno (errno.h), o que possibilita obter descrições de erros, por exemplo, com as funções perror (stdio.h) ou strerror (string.h).
  • No caso da formatação como string, o caractere nulo ('\0') é inserido após o último byte do casamento encontrado.
  • Tudo que delimita a quantidade de bytes lidos é a forma como a string de formato é construída.
  • Uma string de formato mal construída pode levar a vulnerabilidades de memória.

A função scanf é considerada insegura por muitos programadores, mas a verdade é que ela é reconhecidamente difícil de ser utilizada corretamente.

De man 3 scanf:

It is very difficult to use these functions correctly, and it is preferable to read entire lines with fgets(3) or getline(3) and parse them later with sscanf(3) or more specialized functions such as strtol(3).

Ou seja, o próprio manual diz que é muito difícil utilizar corretamente as funções da família do scanf e pode ser mais fácil ler linhas inteiras com funções como fgets e getline e fazer os processamentos necessários com a função sscanf ou outras funções mais especializadas em conversões de cadeias de caracteres em valores numéricos.

Para reduzir as chances de erro

A maior parte dos problemas com scanf decorre da conversão de strings. Por isso, é sempre bom atentar para alguns detalhes, como:

  • Sempre validar o retorno de scanf e fazer o tratamento dos erros.
  • Na leitura de uma linha inteira como string (por exemplo, uma linha que contenha apenas uma palavra), sempre limitar a quantidade de caracteres lidos no especificador %s (ex: %9s para ler até 9 bytes).
  • Na leitura de caracteres únicos, o especificador %c deve ser precedido de um espaço.

Exemplos

Leitura de duas strings
char str1[10];
char str2[10];

// "%Ns" não deve incluir '\0'! 
scanf("%9s", str1);
scanf("%9s", str2);
Separando a entrada digitada em campos
char str1[10];
char str2[10];

scanf("%9s %9s", str1, str2);
    
printf("str1: %s\n", str1);
printf("str2: %s\n", str2);
Lendo caracteres

Neste caso, apenas o primeiro caractere será casado, seja ele qual for.

printf("Confirma (s/n)? ");
char reply;
scanf("%c", &reply);

Mas, se o usuário digitar espaços antes dos caracteres esperados, o primeiro espaço é que será consumido. Para evitar isso…

scanf(" %c", &reply);
Lendo vários caracteres
char a, b, c;
scanf(" %c %c %c", &a, &b, &c);
printf("A:%c B:%c C:%c\n", a, b, c);
Lendo e formatando números

Inteiros:

printf("Digite um inteiro: ");
int num;
scanf("%d", &num);

Ponto flutuante:

printf("Digite um float: ");
float f;
int c = scanf("%f", &f);

Descarregando o buffer de entrada

A função scanf não consome nada que não case com o formato especificado, o que inclui a quebra de linha. Todo o restante da linha permanece no buffer de entrada e, na ausência de uma função da biblioteca padrão para descarregá-lo, nós podemos criar uma função como esta:

void stdin_flush(void) {
    char ch;
    while ((ch = getchar()) != '\n' && ch != EOF);
}

Ela consumiria todos os caracteres restantes no buffer de entrada enquanto o caractere atribuído a ch fosse diferente da quebra de linha (\n) ou de -1, que é o valor retornado por getchar quando tenta ler caracteres após o fim de um arquivo (constante simbólica EOF).

Isso é particularmente necessário quando houver leituras consecutivas da entrada padrão com scanf

int a;
scanf("%d", &b);
stdin_flush();

int b;
scanf("%d", &b);
stdin_flush();

/* ... */

Inicialização de valores numéricos

Nos exemplos anteriores, nós temos um problema que precisa ser considerado: a leitura pode falhar e, nesse caso, nada será escrito nos endereços das variáveis passados para scanf. Como as variáveis não foram inicializadas, seu conteúdo será lixo de memória. Portanto, é importante que essas variáveis sejam devidamente inicializadas com algum valor, por exemplo:

int a = 0;
scanf("%d", &b);
stdin_flush();

int b = 0;
scanf("%d", &b);
stdin_flush();

/* ... */

Este problema é relevante com variáveis que recebem valores numéricos (char, int, float, double, etc), porque, em geral, elas são utilizadas em cálculos e até o lixo de memória pode ser tratado como número válido, levando a resultados enganosos.

Buffer overflow

A leitura de strings com scanf é bastante suscetível a causar vulnerabilidades graves de memória, como o buffer overflow, que é quando a quantidade de dados escritos a partir do endereço de um buffer excedem seu limite máximo e continuam sendo escritos em endereços válidos subsequentes.

Quando o programa tentar escrever os bytes excedentes em uma região inválida da memória, nós temos uma falha de segmentação.

Observe este exemplo:

char ref[10] = "123456789"; // Uma string qualquer.
char str[10];               // Buffer para a stringa digitada no terminal.

scanf("%s", str);           // Especificador %s sem limite.

printf("str: %s\n", str);
printf("ref: %s\n", ref);

Ao ser compilado e executado, os 10 bytes de ref serão escritos na pilha e, acima deles (em um endereço 10 bytes mais baixo), serão reservados os 10 bytes para o buffer str.

Se o usuário digitar abc, esta parte da pilha ficará assim:

    +------+------+------+------+------+------+------+------+------+------+
ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
    +------+------+------+------+------+------+------+------+------+------+
      0x0A   0x0B   0x0C   0x0D   0x0E   0x0F   0x10   0x11   0x12   0x13
      
    +------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x00 | lixo | lixo | lixo | lixo | lixo | lixo |
    +------+------+------+------+------+------+------+------+------+------+
      0x00   0x01   0x02   0x03   0x04   0x05   0x06   0x07   0x08   0x09

A impressão no terminal será:

str: abc
ref: 123456789

Mas, se digitar abcdefghjij, este será o resultado:

    buffer overflow
       ↓
    +------+------+------+------+------+------+------+------+------+------+
ref | 0x00 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
    +------+------+------+------+------+------+------+------+------+------+
      0x0A   0x0B   0x0C   0x0D   0x0E   0x0F   0x10   0x11   0x12   0x13
      
    +------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x6A |
    +------+------+------+------+------+------+------+------+------+------+
      0x00   0x01   0x02   0x03   0x04   0x05   0x06   0x07   0x08   0x09

E isso vai imprimir:

str: abcdefghij
ref:

Porque o caractere nulo (0x00) foi escrito no décimo primeiro endereço após o endereço de str (endereço 0x0A), que é o primeiro byte de ref.

Esse problema poderia ser evitado especificando o limite do tamanho da string:

char ref[10] = "123456789"; // Uma string qualquer.
char str[10];               // Buffer para a stringa digitada no terminal.

scanf("%9s", str);           // Limite de 9 bytes para a string.

printf("str: %s\n", str);
printf("ref: %s\n", ref);

Executando e digitando abcdefghij, os dados na pilha seriam…

    +------+------+------+------+------+------+------+------+------+------+
ref | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
    +------+------+------+------+------+------+------+------+------+------+
      0x0A   0x0B   0x0C   0x0D   0x0E   0x0F   0x10   0x11   0x12   0x13
      
    +------+------+------+------+------+------+------+------+------+------+
str | 0x61 | 0x62 | 0x63 | 0x64 | 0x65 | 0x66 | 0x67 | 0x68 | 0x69 | 0x00 |
    +------+------+------+------+------+------+------+------+------+------+
      0x00   0x01   0x02   0x03   0x04   0x05   0x06   0x07   0x08   0x09

E nós veríamos isso no terminal:

str: abcdefghi    <- O 'j' não foi consumido...
ref: 123456789