cblc/aulas/12-fgets/README.org

294 lines
9 KiB
Org Mode
Raw Normal View History

2025-04-29 11:20:54 -03:00
#+title: Curso Básico da Linguagem C
#+subtitle: Aula 12: Leitura da entrada padrão com 'fgets'
#+author: Blau Araujo
#+startup: show2levels
#+options: toc:3
* Aula 12: Leitura da entrada padrão com 'fgets'
[[https://youtu.be/ZZr9HBPo0Oc][Vídeo desta aula]]
Se, por um lado, a função =scanf= é conveniente para ler e converter dados na
entrada padrão, também é preciso considerar os cuidados que ela exige,
especialmente quando lidamos com a passagem dos dados lidos para um buffer
de caracteres (geralmente, criado para receber uma string). A sua principal
causa de problemas é a falta de um controle simples e explícito da quantidade
de bytes que poderão ser escritos na memória. Na função =scanf=, todo controle
possível é aquele que escrevemos em sua string de formato, por exemplo:
#+begin_src c
char buf[10];
scanf("%9s", buf);
#+end_src
Aqui, 9 primeiros bytes lidos serão consumidos, acrescidos do terminador nulo
(totalizando 10 bytes) e copiados para o endereço expresso pelo vetor =buf=, que
foi declarado para receber até 10 bytes (incluindo o terminador nulo). Mas,
pode acontecer de, um dia, nós mudarmos de ideia e reduzirmos o limite de =buf=,
digamos, para 8 bytes. Isso exigiria uma alteração na chamada de =scanf= -- o que
pode ser facilmente esquecido!
Aliás, essa é uma das justificativas para o uso de constantes simbólicas em
vez dos chamados /números mágicos/. Então, é comum que vetores, como o nosso vetor
=buf=, sejam declarado assim:
#+begin_src c
#include <stdio.h>
...
#define BUFMAX 10
...
int main(...) {
char buf[BUFMAX];
...
scanf("%9s", buf);
...
}
#+end_src
E isso tornaria ainda mais arriscada qualquer modificação no tamanho definido
por =BUFMAX=.
** A função 'fgets'
Segundo sua /man page/, a função =fgets= recebe três argumentos:
#+begin_example
char *fgets(char s[restrict .size], int size, FILE *restrict stream);
#+end_example
- =char s[restrict .size]= - Um buffer para receber os dados lidos;
- =int size= - A quantidade máxima de bytes a serem escritos no buffer;
- =FILE *restrict stream= - Uma estrutura do tipo =FILE= representando o fluxo de
dados que será lido.
Quando chamada, a função =fgets= lê, no máximo, a quantidade de bytes expressa
por =size - 1=, inclui o terminador nulo (='\0'=) e escreve esses bytes no buffer.
Por exemplo:
#+begin_src c
#include <stdio.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
fgets(buf, BUFMAX, stdin);
...
}
#+end_src
#+begin_quote
Aqui, =stdin= é uma macro que expande uma estrutura =FILE= predefinida com o
descritor de arquivos =0= (entrada padrão).
#+end_quote
De pronto, fica evidente a facilidade que nós termos para alterar o tamanho do
vetor =buf= quando necessário: basta alterar a constante simbólica =BUFMAX=.
*** Retorno nulo ambíguo
No caso de sucesso, a função =fgets= retorna o endereço do buffer ou =NULL=,
no caso de um erro, ou se o fim do arquivo for alcançado sem que algo seja lido
(por exemplo, ao ler um arquivo vazio ou numa leitura interativa cancelada com a
combinação de teclas =Ctrl+D=).
Como você pode ver, o significado do retorno =NULL= é ambíguo, mas isso só será
uma preocupação se estivermos lendo arquivos. De todo modo, nós podemos testar
o que aconteceu com as funções =feof= e/ou =ferror=.
- =feof(FILE *stream)= - Testa o estado do indicador de fim de arquivo (~0 = não difinido~).
- =ferror(FILE *stream)= - Testa o estado do indicador de erros (~0 = não definido~).
Se isso for relevante, nós podemos chamar a função =fgets= desta forma:
#+begin_src c
#include <stdio.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
if (fgets(buf, BUFMAX, stdin) == NULL) {
// Só preciamos testar se o retorno for NULL...
if (feof(stdin)) {
// Se feof() retornar algo diferente de zero...
fprintf(stderr, "Fim de arquivo alcançado.\n");
} else if (ferror(stdin)) {
// Se ferror() retornar algo diferente de zero...
fprintf(stderr, "Erro de leitura.\n");
}
return 1;
}
...
return 0;
}
#+end_src
** O problema da quebra de linha
A função =fgets= lê, no máximo =size - 1= bytes, o que é ótimo, mas nem sempre
a linha lida terá tantos bytes. Nesses casos, a leitura será interrompida
depois de uma quebra de linha (caractere ='\n'=) ou depois de alcançado o fim
do arquivo (=EOF=). Ou seja, se a leitura terminar com uma quebra de linha,
o caractere ='\n'= continuará entre os bytes que serão escritos no buffer.
Observe:
#+begin_src c
#include <stdio.h>
#define BUFMAX 10
int main(void) {
char buf[BUFMAX];
printf("Digite até %d caracteres: ", BUFMAX - 1);
fgets(buf, BUFMAX, stdin);
printf("%s\n", buf);
return 0;
}
#+end_src
Executando o programa:
#+begin_example
:~$ ./a.out
Digite até 9 caracteres: 123456789
123456789
:~$ ./a.out
Digite até 9 caracteres: 12345
12345
:~$
#+end_example
Quando eu executei o programa pela primeira vez, eu digitei exatamente 9
caracteres e a quebra de linha (inserida com o =Enter=) não chegou a ser lida
por =fgets=.
#+begin_example
+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
+------+------+------+------+------+------+------+------+------+------+
#+end_example
Mas, na segunda vez, eu digitei apenas 5 caracteres e teclei =Enter=, totalizando
os 6 caracteres lidos por =fgets=. Sendo assim, a quebra de linha final também foi
escrita no buffer e impressa.
#+begin_example
+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | 0x00 | lixo | lixo | lixo |
+------+------+------+------+------+------+------+------+------+------+
#+end_example
Pode parecer um inconveniente se estivermos lidando com a digitação de linhas no
terminal, mas não remover o caractere ='\n'= faz todo sentido quando estamos lendo
linhas de arquivos!
De todo modo, nós podemos criar uma função para remover a quebra de linha, se for
o caso:
#+begin_src c
void trim_string(char *str, int size) {
int i = 0;
while (i < size && str[i] != '\0') {
if (str[i] == '\n') {
str[i] = '\0';
break;
}
}
i++;
}
#+end_src
#+begin_quote
Se estiver entrando com um buffer muito grande, o tipo de =size= pode ser
alterado para =size_t= (=unsigned long int=).
#+end_quote
No nosso exemplo, a função =trim_string= poderia ser utilizada assim:
#+begin_src c
# include <stdio.h>
#define BUFMAX 10;
// Remove a quebra de linha final do buffer...
void trim_string(char *str, int size);
int main(void) {
char buf[BUFMAX];
printf("Digite até %d caracteres: ", BUFMAX - 1);
fgets(buf, BUFMAX, stdin);
trim_string(buf, BUFMAX);
printf("%s\n", buf);
return 0;
}
// Remove a quebra de linha final do buffer...
void trim_string(char *str, int size) {
int i = 0;
while (i < size && str[i] != '\0') {
if (str[i] == '\n') {
str[i] = '\0';
break;
}
}
i++;
}
#+end_src
Compilando e executando novamente:
#+begin_example
:~$ ./a.out
Digite até 9 caracteres: 12345
12345
:~$
#+end_example
E o conteúdo do buffer seria:
#+begin_example
+------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x00 | 0x00 | lixo | lixo | lixo |
+------+------+------+------+------+------+------+------+------+------+
#+end_example
** Uma nota sobre o tipo ponteiro para FILE
No nível do sistema, o acesso a arquivos é sempre estabelecido através de uma
estrutura complexa identificada por um número inteiro: o descritor de arquivos,
assunto da [[../10-dataio/README.org#headline-2][aula 10]]. Então, quando utilizamos chamadas de sistema, como =read= e
=write=, o fluxo de dados (/stream/) é passado como o número de um descritor de
2025-04-29 11:24:58 -03:00
arquivos. Por isso, nós utilizamos as macros:
| Macro | Significado |
|---------------+----------------------------------------------------|
| =STDIN_FILENO= | Valor inteiro relativo ao descritor de arquivos =0=. |
| =STDOUT_FILENO= | Valor inteiro relativo ao descritor de arquivos =1=. |
| =STDERR_FILENO= | Valor inteiro relativo ao descritor de arquivos =2=. |
2025-04-29 11:20:54 -03:00
Nas funções da biblioteca padrão, que abstraem outras estruturas de controle
associadas aos descritores de arquivos, os fluxos de dados são passados como
ponteiros para essas estruturas através do tipo =FILE *= (ponteiro para =FILE=).
Os fluxos de dados padrão também são abstraídos como macros definidas na
biblioteca de funções:
| Macro | Significado |
|--------+----------------------------------------------------------|
| =stdin= | Ponteiro para =FILE= associado ao descritor de arquivos =0=. |
| =stdout= | Ponteiro para =FILE= associado ao descritor de arquivos =1=. |
| =stderr= | Ponteiro para =FILE= associado ao descritor de arquivos =2=. |