cblc/aulas/13-read/README.org

557 lines
17 KiB
Org Mode
Raw Normal View History

2025-04-30 14:45:24 -03:00
#+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
2025-05-01 11:36:05 -03:00
a serviços e funcionalidades do sistema disponíveis no /espaço do kernel/.
Entre essas funcionalidades, estão o acesso a arquivos para leitura e escrita, a
alocação de espaço em memória, a criação de processos, o término de programas e
muitas outras. Portanto, 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 eles.
Isso também significa que todas as funções da biblioteca C padrão que nós
utilizamos até agora, em algum momento, também fazem chamadas de sistema. Por
exemplo, a função =fgets= utiliza a chamada de sistema =read= para preencher o
buffer de entrada do programa e, em seguida, copiar parte desse conteúdo para um
determinado endereço na memória: o que também envolve algumas outras chamadas de
sistema.
Se quisermos utilizar a /chamada de sistema/ =read= para escrever bytes diretamente
no endereço de destino, evitando o buffer de entrada do processo, nós podemos
utilizar a /função/ =read= da =glibc=, que é uma interface para a chamada de sistema
=read= propriamente dita.
2025-04-30 14:45:24 -03:00
#+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
2025-05-01 11:36:05 -03:00
=scanf=, não haverá a inclusão do terminador nulo (='\0'=). Se quisermos
garantir um byte para receber o terminador, nós precisamos fazer como na
função =fgets=: ler =count - 1= bytes...
#+begin_src c
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
read(STDIN_FILENO, buf, BUFMAX - 1);
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
...
return 0;
}
#+end_src
** Conversão para string incluindo o terminador nulo
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
Para converter o conteúdo do buffer em uma string, nós temos que
2025-04-30 14:45:24 -03:00
implementar uma forma de acrescentar o terminador nulo -- por exemplo,
2025-05-01 11:36:05 -03:00
utilizando a quantidade de bytes efetivamente lidos, que é retornada
pela função =read= em caso de sucesso:
2025-04-30 14:45:24 -03:00
#+begin_src c
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
2025-05-01 11:36:05 -03:00
ssize_t bytes = 0;
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
2025-04-30 14:45:24 -03:00
}
2025-05-01 11:36:05 -03:00
// Tratamento do terminador nulo...
buf[bytes] = '\0';
2025-04-30 14:45:24 -03:00
/* ... */
return 0;
}
#+end_src
#+begin_quote
2025-05-01 11:36:05 -03:00
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=.
2025-04-30 14:45:24 -03:00
#+end_quote
2025-05-01 11:36:05 -03:00
A solução do exemplo faz com que =read= tenha um comportamento semelhante ao
de =fgets=:
2025-04-30 14:45:24 -03:00
- A leitura é feita até =BUFMAX - 1=;
2025-05-01 11:36:05 -03:00
- O caractere seguinte no buffer (=buf[bytes]=) recebe ='\0'=;
2025-04-30 14:45:24 -03:00
- 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
2025-05-01 11:36:05 -03:00
de =bytes= será =6= e os bytes em =buf= serão:
2025-04-30 14:45:24 -03:00
#+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
2025-05-01 11:36:05 -03:00
Depois de alterado o byte em =buf[bytes]= (=buf[6]=), nós teremos:
2025-04-30 14:45:24 -03:00
#+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
2025-05-01 11:36:05 -03:00
Os caracteres =0x30= (=0=) e =0x0a= (='\n'=) não serão lidos, o valor de =bytes= será =9=
2025-04-30 14:45:24 -03:00
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...
2025-05-01 11:36:05 -03:00
Entre outras tantas situações.
2025-04-30 14:45:24 -03:00
** O buffer do terminal
2025-05-01 11:36:05 -03:00
Quando um programa com a função =read= é executado para receber dados digitados
em um terminal, a digitação é acumulada em um buffer gerenciado pelo terminal
e só será enviada para o processo do programa quando o usuário teclar =Enter=.
No programa, a função =read= consome parte desses dados e os bytes que não
forem consumidos permanecerão no buffer do terminal.
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
Os dados residuais não são descartados e permanecem disponíveis para futuras
chamadas de leitura (inclusive por processos subsequentes, caso o processo
do programa termine) ou até o terminal ser fechado. Isso pode causar efeitos
inesperados em programas interativos executados na sequência (como o próprio
shell, por exemplo), pois o conteúdo remanescente no buffer do terminal será
lido automaticamente.
2025-04-30 14:45:24 -03:00
Veja este exemplo:
#+begin_src c
#include <stdio.h>
#include <unistd.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
2025-05-01 11:36:05 -03:00
ssize_t bytes = 0;
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
2025-04-30 14:45:24 -03:00
}
// Tratamento do terminador nulo...
2025-05-01 11:36:05 -03:00
buf[bytes] = '\0';
2025-04-30 14:45:24 -03:00
printf("%s\n", buf);
return 0;
}
#+end_src
Compilando e executando:
#+begin_example
:~$ gcc -Wall teste.c
:~$ ./a.out
123456789012345
123456789
2025-05-01 11:36:05 -03:00
:~$ 012345 <-- O Bash tentou interpretar e executar o conteúdo do buffer!
2025-04-30 14:45:24 -03:00
bash: 012345: comando não encontrado
#+end_example
2025-05-01 11:36:05 -03:00
A função =read= consumiu apenas os 9 primeiros caracteres digitados, mas os 6
caracteres restantes continuaram no buffer do terminal e foram lidos e
interpretados pelo processo do shell (interativo) como a linha de um comando.
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
*** Descarga do buffer do terminal
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
A solução para isso é simples e já é velha conhecida: consumir todos os
bytes restantes na entrada padrão, por exemplo, com a função que nós
criamos em outras aulas...
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_src c
void flush_stdin(void) {
2025-04-30 14:45:24 -03:00
char c;
while((c = getchar()) != '\n' && c != EOF);
}
#+end_src
2025-05-01 11:36:05 -03:00
Mas esta função utiliza =getchar= que, assim como =fgets= e =scanf=, fará a
leitura do buffer de entrada criado no processo pela =glibc=. Certamente,
o conteúdo do buffer do terminal será copiado para o buffer de entrada do
programa. Porém, sem garantias de que haverá algo no buffer do terminal,
pode acontecer que a nossa função fique presa em =getchar= até que o usuário
tecle =Enter= novamente, enviando a quebra de linha que o loop =while= espera
para terminar.
Por exemplo, utilizando a função =flush_stdin=, o nosso exemplo funcionaria
corretamente quando houvesse mais caracteres digitados do que o =read=
pode consumir:
2025-04-30 14:45:24 -03:00
#+begin_example
:~$ gcc -Wall teste.c
:~$ ./a.out
123456789012345
123456789
:~$
#+end_example
2025-05-01 11:36:05 -03:00
Contudo, digitando menos caracteres do que o limite, nós teríamos que
teclar =Enter= duas vezes: a primeira para a leitura de =read= e a segunda
para =getchar=...
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_example
:~$ gcc -Wall teste.c
:~$ ./a.out
12345
<--- Aguardando um segundo ENTER
12345
<--- Quebra de linha na string impressa
:~$
#+end_example
A segunda linha em branco era esperada porque nós não fizemos nada para
remover a quebra de linha lida por =read=, mas a primeira aconteceu porque
eu tive que teclar =Enter= mais uma vez para que =getchar= tivesse algo para
ler.
*** Implementação de =flush_stdin= com a chamada =read=
Antes de passarmos à solução da descarga do buffer do terminal, vamos
aproveitar que estamos lidando com chamadas de sistema para implementar
uma versão da função =flush_stdin= com =read=, em vez de =getchar=:
2025-04-30 14:45:24 -03:00
#+begin_src c
2025-05-01 11:36:05 -03:00
void flush_tty_buffer(void) {
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n');
}
#+end_src
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_quote
*Importante!* Tanto =read= quanto =STDIN_FILENO= dependem da inclusão de =unistd.h=.
#+end_quote
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
O princípio é o mesmo, só que, desta vez, o /loop/ vai continuar enquanto
=read= retornar que leu 1 byte e este byte for diferente de ='\n'=. A outra
diferença é que, em vez de ler o buffer do programa, a nossa nova função
lerá diretamente o buffer do terminal.
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_quote
Esta função não poderia ser utilizada se os bytes remanescentes estivessem
no buffer de entrada do programa!
#+end_quote
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
Mesmo assim, o problema continua: se o buffer do terminal estiver vazio,
=read= ficará esperando o recebimento de uma linha, o que só acontecerá
quando nós digitarmos novamente um =Enter=.
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
*** Descarga condicional do buffer do terminal
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
Se a causa do problema é a possibilidade do buffer do terminal estar vazio,
basta condicionar a chamada de =flush_stdin=, ou de =flush_tty_buffer=, à
verificação desta condição, o que pode ser avaliado de, pelo menos, duas
formas: utilizando os bytes lidos e a sua quantidade ou verificando o
estado do buffer do terminal.
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
*Solução 1: Teste da presença de ='\n'=*
#+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 | 0x0A | 0X00 |
+------+------+------+------+------+------+------+------+------+------+
#+end_example
Aqui, a digitação foi =12345678\n= (exatamente =BUFMAX - 1= bytes) e, mesmo que
fosse digitada qualquer quantidade de bytes menor que essa, o buffer do
terminal estaria vazio e o caractere ='\n'= estaria presente na string. Logo,
nós poderíamos utilizar a seguinte condição:
#+begin_src c
if (buf[bytes - 1] != '\n') {
flush_stdout(); // Ou flush_tty_buffer
2025-04-30 14:45:24 -03:00
}
#+end_src
2025-05-01 11:36:05 -03:00
É uma solução interessante porque, além de simples, ela já verifica a
existência da quebra de linha na string e nós poderíamos aproveitar, se
fosse o caso, para removê-la:
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_src c
if (buf[bytes - 1] == '\n') {
buf[bytes - 1] = '\0'; // Remoção da quabra de linha
} else {
flush_stdout(); // Ou flush_tty_buffer
}
#+end_src
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
*Solução 2: verificação do estado do buffer do terminal*
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
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:
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
- 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.
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
Sendo assim, nós poderíamos criar uma função para retornar =0=, no caso de
buffer vazio, ou a quantidade de bytes remanescentes no buffer do terminal:
#+begin_src c
#include <sys/ioctl.h>
...
int is_tty_empty(void) {
int r = 0;
return (ioctl(STDIN_FILENO, FIONREAD, &r) == 0);
}
#+end_src
Portanto, nossa função retornaria =1= (verdadeiro), se o buffer do terminal
estivesse vazio, ou =0= (falso), se não estivesse.
#+begin_quote
A ideia de criar uma função, em vez de utilizar =ioctl= diretamente, tem a
ver com a atribuição de um valor semântico ao código.
#+end_quote
No nosso exemplo, ela poderia ser utilizada desta maneira:
#+begin_src c
if (!is_tty_empty()) {
flush_stdout(); // Ou flush_tty_buffer
}
#+end_src
*Qual solução utilizar?*
Não existe uma resposta certa neste caso: tudo depende do contexto do
programa. Se eu estivesse buscando uma solução mais robusta e aplicável
em diversas situações, eu escolheria a segunda (=ioctl=), mas escolheria
a primeira em contextos menos rigorosos, onde a quebra de linha tivesse
que ser detectada e removida da string de qualquer forma.
Eu vou optar pela primeira solução desta vez, então o nosso código ficará
assim:
2025-04-30 14:45:24 -03:00
#+begin_src c
#include <stdio.h>
#include <unistd.h>
#define BUFMAX 10
2025-05-01 11:36:05 -03:00
void flush_tty_buffer(void) {
char c;
while (read(STDIN_FILENO, &c, 1) == 1 && c != '\n');
}
2025-04-30 14:45:24 -03:00
int main(void) {
char buf[BUFMAX];
2025-05-01 11:36:05 -03:00
ssize_t bytes = 0;
if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
2025-04-30 14:45:24 -03:00
}
2025-05-01 11:36:05 -03:00
2025-04-30 14:45:24 -03:00
// Tratamento do terminador nulo...
2025-05-01 11:36:05 -03:00
buf[bytes] = '\0';
if (buf[bytes - 1] == '\n') {
// Remoção condicional da quebra de linha...
buf[bytes - 1] = '\0';
} else {
// Esvaziamento condicional do buffer do terminal...
flush_tty_buffer(); // Ou flush_stdin
}
2025-04-30 14:45:24 -03:00
printf("%s\n", buf);
return 0;
}
#+end_src
2025-05-01 11:36:05 -03:00
Compilando e testando...
2025-04-30 14:45:24 -03:00
#+begin_example
2025-05-01 11:36:05 -03:00
:~$ gcc -Wall teste.c
:~$ ./a.out
12345 <-- 6 bytes digitados (buffer tty vazio)
12345 <-- Quebra de linha removida
:~$ ./a.out
12345678 <-- 9 bytes digitados (buffer vazio)
12345678 <-- Quebra de linha removida
:~$ ./a.out
123456789012345 <-- 16 bytes digitados (7 bytes no buffer tty)
123456789 <-- Não havia quebra de linha para remover
:~$
2025-04-30 14:45:24 -03:00
#+end_example
2025-05-01 11:36:05 -03:00
** Definindo um prompt
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
O nosso programa precisa de um prompt, então vamos implementá-lo:
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_src c
...
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
char buf[BUFMAX];
ssize_t bytes = 0;
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
printf("Digite algo: "); // Nosso prompt!
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
if((bytes = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1; // Erro ou nada foi lido
}
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
...
2025-04-30 14:45:24 -03:00
#+end_src
2025-05-01 11:36:05 -03:00
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=:
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_example
:~$ gcc -Wall teste.c
:~$ ./a.out
1234567890
Digite algo: 123456789 <-- O prompt só foi impresso aqui!
:~$
#+end_example
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
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:
2025-04-30 14:47:32 -03:00
2025-05-01 11:36:05 -03:00
- /Totalmente bufferizados/: quando o programa escreve em arquivos ou pipes;
- /Bufferizados por linha/: quando a escrita é em um terminal.
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
Como estamos escrevendo no terminal, a saída de =printf= é /bufferizada por
linha/ e só é liberada quando o buffer de saída fica cheio (geralmente,
=8192 bytes=), quando recebe o caractere de quebra de linha (='\n'=) ou quando
é esvaziada explicitamente.
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
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 com a função
=fflush=:
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_src c
...
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
char buf[BUFMAX];
ssize_t count = 0;
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
// Prompt...
printf("Digite algo: ");
// Descarga do buffer de saída...
fflush(stdout);
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
if((count = read(STDIN_FILENO, buf, BUFMAX - 1)) <= 0) {
return 1;
}
2025-04-30 14:45:24 -03:00
...
2025-05-01 11:36:05 -03:00
#+end_src
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
Compilando e testando:
2025-04-30 14:45:24 -03:00
2025-05-01 11:36:05 -03:00
#+begin_example
:~$ gcc -Wall teste.c
:~$ ./a.out
Digite algo: 1234567890
123456789
:~$
#+end_example
#+begin_quote
O problema da /bufferização por linha/ não é exatamente causado pelo uso da
chamada de sistema =read=. Na verdade, as funções de mais alto nível (como
=fgets= e =scanf=) é que são implementadas com mecanismos para descargar o buffer
de saída automaticamente.
#+end_quote
2025-04-30 14:45:24 -03:00