mirror of
https://gitlab.com/blau_araujo/cblc.git
synced 2025-05-10 02:26:36 -03:00
483 lines
14 KiB
Org Mode
483 lines
14 KiB
Org Mode
#+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 <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;
|
|
}
|
|
#+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 <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;
|
|
}
|
|
#+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
|
|
|
|
|