From 699cf4bd1c3e4af7e1b582b133b33d3e5965159a Mon Sep 17 00:00:00 2001 From: Blau Araujo Date: Fri, 25 Apr 2025 09:00:50 -0300 Subject: [PATCH] =?UTF-8?q?anota=C3=A7=C3=B5es=20da=20aula=2011?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- aulas/11-scanf/README.org | 483 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 483 insertions(+) create mode 100644 aulas/11-scanf/README.org diff --git a/aulas/11-scanf/README.org b/aulas/11-scanf/README.org new file mode 100644 index 0000000..14a6301 --- /dev/null +++ b/aulas/11-scanf/README.org @@ -0,0 +1,483 @@ +#+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 menos 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 + +