#+title: Curso Básico da Linguagem C #+subtitle: Aula 11: Leitura da entrada padrão com 'scanf' #+author: Blau Araujo #+startup: show2levels #+options: toc:3 * Aula 11: Leitura da entrada padrão com 'scanf' [[https://youtu.be/MZiI95b2gdY][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=): #+begin_src c int isatty(int fd); #+end_src 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=): #+begin_src c #include #include int main(void) { if (isatty(0)) { puts("STDIN ligada ao terminal."); } else { puts("STDIN ligada a um arquivo ou pipe."); } return 0; } #+end_src Compilação e testes: #+begin_example :~$ 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. #+end_example #+begin_quote O comando interno do shell =:= é o comando nulo, que não faz nada e sempre termina com sucesso. #+end_quote 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=... #+begin_src c FILE *freopen(const char *caminho, const char *modo, FILE *stream); #+end_src 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. #+begin_quote O propósito original desta função é alterar o arquivo associado a um dos fluxos padrão (=stdin=, =stdout= ou =stderr=). #+end_quote Exemplo (=redir-stdin.c=): #+begin_src c #include #include 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; } #+end_src Compilação... #+begin_example :~$ gcc -Wall redir-stdin.c #+end_example Executando sem argumentos (redireciona para =/dev/null=): #+begin_example :~$ ./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 #+end_example Executando com argumentos (nome do arquivo no primeiro argumento): #+begin_example :~$ ./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 #+end_example No caso de um erro de abertura (ex: arquivo "banana" não existe): #+begin_example :~$ ./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 #+end_example ** 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=: #+begin_quote 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). #+end_quote 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 #+begin_src c char str1[10]; char str2[10]; // "%Ns" não deve incluir '\0'! scanf("%9s", str1); scanf("%9s", str2); #+end_src **** Separando a entrada digitada em campos #+begin_src c char str1[10]; char str2[10]; scanf("%9s %9s", str1, str2); printf("str1: %s\n", str1); printf("str2: %s\n", str2); #+end_src **** Lendo caracteres Neste caso, apenas o primeiro caractere será casado, seja ele qual for. #+begin_src c printf("Confirma (s/n)? "); char reply; scanf("%c", &reply); #+end_src Mas, se o usuário digitar espaços antes dos caracteres esperados, o primeiro espaço é que será consumido. Para evitar isso... #+begin_src c scanf(" %c", &reply); #+end_src **** Lendo vários caracteres #+begin_src c char a, b, c; scanf(" %c %c %c", &a, &b, &c); printf("A:%c B:%c C:%c\n", a, b, c); #+end_src **** Lendo e formatando números Inteiros: #+begin_src c printf("Digite um inteiro: "); int num; scanf("%d", &num); #+end_src Ponto flutuante: #+begin_src c printf("Digite um float: "); float f; int c = scanf("%f", &f); #+end_src *** 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: #+begin_src c void stdin_flush(void) { char ch; while ((ch = getchar()) != '\n' && ch != EOF); } #+end_src 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=... #+begin_src c int a; scanf("%d", &b); stdin_flush(); int b; scanf("%d", &b); stdin_flush(); /* ... */ #+end_src *** 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: #+begin_src c int a = 0; scanf("%d", &b); stdin_flush(); int b = 0; scanf("%d", &b); stdin_flush(); /* ... */ #+end_src 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. #+begin_quote 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/. #+end_quote Observe este exemplo: #+begin_src c 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); #+end_src 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: #+begin_example +------+------+------+------+------+------+------+------+------+------+ 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 #+end_example A impressão no terminal será: #+begin_example str: abc ref: 123456789 #+end_example Mas, se digitar =abcdefghjij=, este será o resultado: #+begin_example 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 #+end_example E isso vai imprimir: #+begin_example str: abcdefghij ref: #+end_example 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: #+begin_src c 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); #+end_src Executando e digitando =abcdefghij=, os dados na pilha seriam... #+begin_example +------+------+------+------+------+------+------+------+------+------+ 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 #+end_example E nós veríamos isso no terminal: #+begin_example str: abcdefghi <- O 'j' não foi consumido... ref: 123456789 #+end_example