mirror of
https://gitlab.com/blau_araujo/cblc.git
synced 2025-05-09 18:16:37 -03:00
conteúdo da aula 13
This commit is contained in:
parent
ab810cd0f4
commit
0fbb47a10a
1 changed files with 457 additions and 0 deletions
457
aulas/13-read/README.org
Normal file
457
aulas/13-read/README.org
Normal file
|
@ -0,0 +1,457 @@
|
|||
#+title: Curso Básico da Linguagem C
|
||||
#+subtitle: Aula 13: Leitura da entrada padrão com 'read'
|
||||
#+author: Blau Araujo
|
||||
#+startup: show2levels
|
||||
#+options: toc:3
|
||||
|
||||
* Aula 13: Leitura da entrada padrão com 'read'
|
||||
|
||||
[[https://youtu.be/bW3Xox6LP_U][Vídeo desta aula]]
|
||||
|
||||
A função =read= é um /wrapper/ (uma /"embalagem"/) para simplificar o uso da chamada
|
||||
de sistema =read=. Chamadas de sistema são funções internas que o kernel
|
||||
implementa para que os programas (executados no /espaço de usuário/) tenham acesso
|
||||
a serviços e funcionalidades do sistema (disponíveis no /espaço do kernel/).
|
||||
|
||||
Entre essas funcionalidades, estão:
|
||||
|
||||
- Acesso a arquivos para leitura e escrita;
|
||||
- Alocação de espaço em memória;
|
||||
- Criação de processos;
|
||||
- Término de programas.
|
||||
|
||||
Nossos programas em C não interagem diretamente com o hardware ou com recursos
|
||||
protegidos do sistema. Em vez disso, eles fazem /chamadas de sistema/ para
|
||||
"pedir" que o kernel realize essas operações por ele. Sendo assim, fica fácil
|
||||
concluir que todas as funções da biblioteca C padrão que nós utilizamos até
|
||||
agora, em algum momento, fazem chamadas de sistema.
|
||||
|
||||
Por exemplo, a função =fgets= utiliza a chamada de sistema =read= para preencher o
|
||||
buffer de entrada do processo e, em seguida, copiar parte desse conteúdo para o
|
||||
buffer definido no programa para receber uma string. Se quisermos utilizar
|
||||
a chamada de sistema =read= para escrever bytes diretamente no buffer de destino,
|
||||
evitando o buffer de entrada do processo, nós podemos utilizar a função =read=
|
||||
da =glibc=, que é uma interface (um /wrapper/, ou /"embalagem"/) para a chamada de
|
||||
sistema propriamente dita.
|
||||
|
||||
#+begin_quote
|
||||
As /man pages/ das interfaces para as chamadas de sistema implementadas na =glibc=
|
||||
podem ser encontradas na seção 2 (ex.: =man 2 read=).
|
||||
#+end_quote
|
||||
|
||||
** A função 'read'
|
||||
|
||||
A função =read= é declarada no cabeçalho =unistd.h= da seguinte forma:
|
||||
|
||||
#+begin_src c
|
||||
ssize_t read(int fd, void buf[.count], size_t count);
|
||||
#+end_src
|
||||
|
||||
Onde:
|
||||
|
||||
- =int fd= - O número de um descritor de arquivos aberto para leitura;
|
||||
- =void buf[.count]= - O endereço do buffer de destino dos bytes lidos;
|
||||
- =size_t count= - A quantidade máxima de bytes a serem lidos.
|
||||
|
||||
#+begin_quote
|
||||
O tipo =ssize_t= é um apelido para =long= e =size_t= para =unsigned long=.
|
||||
#+end_quote
|
||||
|
||||
Por exemplo:
|
||||
|
||||
#+begin_src c
|
||||
#include <unistd.h>
|
||||
|
||||
#define BUFMAX 10
|
||||
|
||||
int main(void) {
|
||||
|
||||
char buf[BUFMAX];
|
||||
read(STDIN_FILENO, buf, BUFMAX);
|
||||
|
||||
...
|
||||
|
||||
return 0;
|
||||
}
|
||||
#+end_src
|
||||
|
||||
Isso fará com que até 10 bytes (=BUFMAX=) recebidos pela entrada padrão
|
||||
(descritor de arquivos =0=, valor expandido pela macro =STDIN_FILENO=) sejam
|
||||
escritos na memória a partir do endereço de =buf=. Mas, diferente de =fgets= e
|
||||
=scanf=, não haverá a inclusão do terminador nulo (='\0'=).
|
||||
|
||||
*** Incluindo o terminador nulo
|
||||
|
||||
Se quisermos que o conteúdo do buffer seja uma string, nós teremos que
|
||||
implementar uma forma de acrescentar o terminador nulo -- por exemplo,
|
||||
utilizando a quantidade de bytes lidos, que é retornada pela função =read=
|
||||
em caso de sucesso:
|
||||
|
||||
#+begin_src c
|
||||
#include <unistd.h>
|
||||
|
||||
#define BUFMAX 10
|
||||
|
||||
int main(void) {
|
||||
|
||||
char buf[BUFMAX];
|
||||
ssize_t count = 0;
|
||||
|
||||
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) > 0) {
|
||||
buf[count] = '\0';
|
||||
}
|
||||
|
||||
/* ... */
|
||||
|
||||
return 0;
|
||||
}
|
||||
#+end_src
|
||||
|
||||
#+begin_quote
|
||||
O retorno =0= indica o fim do arquivo e =-1= indica um erro, cujo identificador é
|
||||
atribuído à variável =errno= e pode ser exibido com a função =perror=.
|
||||
#+end_quote
|
||||
|
||||
A solução do exemplo faz com que =read= se comporte quase como a função
|
||||
=fgets=, exceto por não criar um buffer de entrada no /heap/ do processo:
|
||||
|
||||
- A leitura é feita até =BUFMAX - 1=;
|
||||
- O caractere seguinte no buffer (=buf[count]=) recebe ='\0'=;
|
||||
- O caractere ='\n'= será preservado se estiver no limite de leitura.
|
||||
|
||||
Para demonstrar, digamos que o usuário digitou =12345= e teclou =Enter=. O valor
|
||||
de =count= será =6= e os bytes em =buf= serão:
|
||||
|
||||
#+begin_example
|
||||
buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
|
||||
+------+------+------+------+------+------+------+------+------+------+
|
||||
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | lixo | lixo | lixo | lixo |
|
||||
+------+------+------+------+------+------+------+------+------+------+
|
||||
#+end_example
|
||||
|
||||
Depois de alterado o byte em =buf[count]= (=buf[6]=), nós teremos:
|
||||
|
||||
#+begin_example
|
||||
buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
|
||||
+------+------+------+------+------+------+------+------+------+------+
|
||||
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | 0x00 | lixo | lixo | lixo |
|
||||
+------+------+------+------+------+------+------+------+------+------+
|
||||
#+end_example
|
||||
|
||||
Se for digitado =1234567890=, nós teremos inicialmente:
|
||||
|
||||
#+begin_example
|
||||
buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
|
||||
+------+------+------+------+------+------+------+------+------+------+
|
||||
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | lixo |
|
||||
+------+------+------+------+------+------+------+------+------+------+
|
||||
#+end_example
|
||||
|
||||
Os caracteres =0x30= (=0=) e =0x0a= (='\n'=) não serão lidos, o valor de =count= será =9=
|
||||
e =buf[9]= receberá o terminador nulo:
|
||||
|
||||
#+begin_example
|
||||
buf[0] buf[1] buf[2] buf[3] buf[4] buf[5] buf[6] buf[7] buf[8] buf[9]
|
||||
+------+------+------+------+------+------+------+------+------+------+
|
||||
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
|
||||
+------+------+------+------+------+------+------+------+------+------+
|
||||
#+end_example
|
||||
|
||||
*** Quando não converter os bytes lidos em string
|
||||
|
||||
O tratamento anterior só faz sentido quando queremos utilizar a entrada
|
||||
como uma string. Mas nada disso será necessário, nem será adequado, se
|
||||
estivermos lendo...
|
||||
|
||||
- Dados binários (arquivos binários em geral);
|
||||
- Dados em blocos fixos (setores de disco, pacotes de rede, etc);
|
||||
- Transferências de dados entre processos;
|
||||
- Dados para cálculos de /checksum/ e /hash/;
|
||||
- Dados para compressão ou criptografia...
|
||||
|
||||
Entre outras tantas situações.
|
||||
|
||||
** O buffer do terminal
|
||||
|
||||
Quando um programa com a função =read= é executado interativamente, os dados
|
||||
digitados são inicialmente armazenados em um buffer gerenciado pelo terminal.
|
||||
Ao chamar a função =read=, o programa consome parte desses dados da entrada padrão
|
||||
e os bytes que não forem consumidos permanecerão no buffer do terminal.
|
||||
|
||||
Esses dados residuais não são descartados e permanecem disponíveis para futuras
|
||||
chamadas de leitura -- inclusive por processos subsequentes, caso o processo
|
||||
atual termine. Isso pode causar efeitos inesperados em programas interativos
|
||||
executados na sequência (como o próprio shell, por exemplo), pois os caracteres
|
||||
digitados anteriormente, mas ainda não consumidos, serão lidos automaticamente.
|
||||
|
||||
Veja este exemplo:
|
||||
|
||||
#+begin_src c
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define BUFMAX 10
|
||||
|
||||
int main(void) {
|
||||
|
||||
char buf[BUFMAX];
|
||||
ssize_t count = 0;
|
||||
|
||||
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
|
||||
return 1;
|
||||
}
|
||||
// Tratamento do terminador nulo...
|
||||
buf[count] = '\0';
|
||||
|
||||
printf("%s\n", buf);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#+end_src
|
||||
|
||||
Compilando e executando:
|
||||
|
||||
#+begin_example
|
||||
:~$ gcc -Wall teste.c
|
||||
:~$ ./a.out
|
||||
123456789012345
|
||||
123456789
|
||||
:~$ 012345
|
||||
bash: 012345: comando não encontrado
|
||||
#+end_example
|
||||
|
||||
A função =read= consumiu apenas os 9 primeiros caracteres digitados, os 6
|
||||
caracteres restantes continuaram no buffer do terminal e foram lidos pelo
|
||||
processo do shell (interativo) que tentou executá-los como um comando.
|
||||
|
||||
A solução para isso é simples (e já conhecida por nós): consumir todos os
|
||||
bytes restantes na entrada padrão:
|
||||
|
||||
#+begin_src c
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define BUFMAX 10
|
||||
|
||||
int main(void) {
|
||||
|
||||
char buf[BUFMAX];
|
||||
ssize_t count = 0;
|
||||
|
||||
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
|
||||
return 1;
|
||||
}
|
||||
// Tratamento do terminador nulo...
|
||||
buf[count] = '\0';
|
||||
|
||||
// Tratamento dos caracteres restantes no buffer do terminal...
|
||||
char c;
|
||||
while((c = getchar()) != '\n' && c != EOF);
|
||||
|
||||
printf("%s\n", buf);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#+end_src
|
||||
|
||||
Compilando e executando novamente:
|
||||
|
||||
#+begin_example
|
||||
:~$ gcc -Wall teste.c
|
||||
:~$ ./a.out
|
||||
123456789012345
|
||||
123456789
|
||||
:~$
|
||||
#+end_example
|
||||
|
||||
** Definindo um prompt
|
||||
|
||||
O nosso programa precisa de um prompt, então vamos implementá-lo:
|
||||
|
||||
#+begin_src c
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define BUFMAX 10
|
||||
|
||||
int main(void) {
|
||||
|
||||
char buf[BUFMAX];
|
||||
ssize_t count = 0;
|
||||
|
||||
// Prompt...
|
||||
printf("Digite algo: ");
|
||||
|
||||
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
|
||||
return 1;
|
||||
}
|
||||
// Tratamento do terminador nulo...
|
||||
buf[count] = '\0';
|
||||
|
||||
// Tratamento dos caracteres restantes no buffer do terminal...
|
||||
char c;
|
||||
while((c = getchar()) != '\n' && c != EOF);
|
||||
|
||||
printf("%s\n", buf);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#+end_src
|
||||
|
||||
Agora, nós temos a impressão da string ="Digite algo: "= antes da leitura
|
||||
do terminal. Entretanto, ao compilar e executar o programa, nada é exibido
|
||||
até nós teclarmos =Enter=:
|
||||
|
||||
#+begin_example
|
||||
:~$ gcc -Wall teste.c
|
||||
:~$ ./a.out
|
||||
1234567890
|
||||
Digite algo: 123456789
|
||||
:~$
|
||||
#+end_example
|
||||
|
||||
Isso acontece porque =printf= utiliza o buffer de saída do programa (criado
|
||||
pela biblioteca padrão) e, por padrão, os dados na saída podem ser:
|
||||
|
||||
- Totalmente bufferizados: quando o programa escreve em arquivos ou pipes;
|
||||
- Bufferizados por linha: quando a escrita é em um terminal.
|
||||
|
||||
Como estamos escrevendo no terminal, a saída de =printf= é bufferizada por
|
||||
linha e só é liberada quando o buffer de saída fica cheio (~BUFSIZ = 8192 bytes~),
|
||||
quando recebe o caractere de quebra de linha (='\n'=) ou quando é esvaziada
|
||||
explicitamente (por exemplo, com =fflush(stdout)=).
|
||||
|
||||
No caso do exemplo, a saída não foi descarregada porque a string na chamada
|
||||
de =printf= não tem uma quebra de linha (exatamente como nós queremos). Uma
|
||||
solução simples, porém, é descarregar o buffer explicitamente:
|
||||
|
||||
#+begin_src c
|
||||
#include <stdio.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#define BUFMAX 10
|
||||
|
||||
int main(void) {
|
||||
|
||||
char buf[BUFMAX];
|
||||
ssize_t count = 0;
|
||||
|
||||
// Prompt...
|
||||
printf("Digite algo: ");
|
||||
// Descarga do buffer de saída...
|
||||
fflush(stdout);
|
||||
|
||||
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
|
||||
return 1;
|
||||
}
|
||||
// Tratamento do terminador nulo...
|
||||
buf[count] = '\0';
|
||||
|
||||
// Tratamento dos caracteres restantes no buffer do terminal...
|
||||
char c;
|
||||
while((c = getchar()) != '\n' && c != EOF);
|
||||
|
||||
printf("%s\n", buf);
|
||||
|
||||
return 0;
|
||||
}
|
||||
#+end_src
|
||||
|
||||
Compilando e testando:
|
||||
|
||||
#+begin_example
|
||||
:~$ gcc -Wall teste.c
|
||||
:~$ ./a.out
|
||||
Digite algo: 1234567890
|
||||
123456789
|
||||
:~$
|
||||
#+end_example
|
||||
|
||||
** Ainda temos um problema!
|
||||
|
||||
Acontece, porém, que os nossos testes estão mascarando outro problema.
|
||||
Observe o que acontece quando digitamos menos caracteres do que a função
|
||||
=read= espera ler:
|
||||
|
||||
#+begin_example
|
||||
:~$ gcc -Wall teste.c
|
||||
:~$ ./a.out
|
||||
Digite algo: 12345
|
||||
|
||||
12345
|
||||
|
||||
:~$
|
||||
#+end_example
|
||||
|
||||
A segunda linha em branco era esperada porque a quebra de linha da minha
|
||||
digitação foi preservada, mas a primeira linha em branco aconteceu
|
||||
porque eu tive que teclar =Enter= duas vezes!
|
||||
|
||||
Isso aconteceu por causa da forma como estamos descarregando o restante do
|
||||
buffer de entrada do terminal:
|
||||
|
||||
#+begin_src c
|
||||
// Tratamento dos caracteres restantes no buffer do terminal...
|
||||
char c;
|
||||
while((c = getchar()) != '\n' && c != EOF);
|
||||
#+end_src
|
||||
|
||||
Quando digitamos menos caracteres do que o =read= espera ler, não há sobras
|
||||
no buffer do terminal e não há quebras de linha nem uma indicação de fim de
|
||||
arquivo (a menos que o usuário tecle =Ctrl+D=). Por isso, a limpeza do buffer
|
||||
deve ser condicionada de uma das seguintes formas:
|
||||
|
||||
- Ou verificamos se o penúltimo caractere no buffer (=buf[count - 1]=) é
|
||||
diferente de ='\n'=, o que indicaria que o buffer não foi consumido
|
||||
completamente;
|
||||
- Ou verificamos se o buffer está vazio ou não com a chamada de sistema
|
||||
=ioctl=.
|
||||
|
||||
**** Solução com o conteúdo de =buf[count - 1]=
|
||||
|
||||
#+begin_src c
|
||||
// Verificar se '\n' faz parte da string...
|
||||
if (buf[count - 1] != '\n') {
|
||||
// Tratamento dos caracteres restantes no buffer do terminal...
|
||||
char c;
|
||||
while((c = getchar()) != '\n' && c != EOF);
|
||||
}
|
||||
#+end_src
|
||||
|
||||
**** Solução com =ioctl=
|
||||
|
||||
A chamada de sistema =ioctl=, no cabeçalho =sys/ioctl.h=, manipula parâmetros
|
||||
de dispositivos especiais (como terminais, por exemplo). Seus argumentos
|
||||
são:
|
||||
|
||||
- O descritor de arquivos do dispositivo (ex.: =0= ou =STDIN_FILENO=);
|
||||
- O número da operação com o dispositivo (ex.: =FIONREAD=, quantidade de bytes
|
||||
disponíveis para leitura imediata);
|
||||
- Um ponteiro para o endereço que vai receber a informação obtida.
|
||||
|
||||
Sendo assim, nós poderíamos utilizá-la desta forma para determinar se
|
||||
o buffer do terminal contém alguma coisa (=0= indica buffer vazio):
|
||||
|
||||
#+begin_src c
|
||||
#include <sys/ioctl.h>
|
||||
|
||||
...
|
||||
|
||||
int main(void) {
|
||||
...
|
||||
|
||||
// Verificar se o buffer está vazio...
|
||||
int r = 0;
|
||||
if (ioctl(STDIN_FILENO, FIONREAD, &r) != 0) {
|
||||
// Tratamento dos caracteres restantes no buffer do terminal...
|
||||
char c;
|
||||
while((c = getchar()) != '\n' && c != EOF);
|
||||
}
|
||||
...
|
||||
}
|
||||
#+end_src
|
||||
|
||||
#+begin_quote
|
||||
Experimente as duas soluções e escolha a sua preferida.
|
||||
#+end_quote
|
||||
|
Loading…
Add table
Reference in a new issue