mirror of
https://gitlab.com/blau_araujo/cblc.git
synced 2025-05-10 02:26:36 -03:00
447 lines
14 KiB
Org Mode
447 lines
14 KiB
Org Mode
#+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=.
|
|
|
|
*** 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
|
|
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=. |
|
|
|
|
Nas funções da biblioteca padrão, que abstrai 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=. |
|
|
|
|
** 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
|
|
|
|
** 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
|
|
|
|
** Conversão para números
|
|
|
|
Diferente da função =scanf=, =fgets= apenas lê caracteres e escreve num buffer
|
|
como uma string (caracteres terminados com ='\0'=). Isso significa que, se
|
|
estivermos interessados em valores numéricos, os bytes no buffer terão que
|
|
ser tratado, geralmente em três etapas:
|
|
|
|
- Remoção da quebra de linha, se houver uma;
|
|
- Conversão para o tipo desejado;
|
|
- Validação do resultado segundo o tipo;
|
|
|
|
Nós já falamos sobre a remoção da quebra de linha, então podemos nos concentrar
|
|
nas duas etapas seguintes. Porém, os métodos de conversão mais comuns já integram
|
|
alguma forma de validação (geralmente, como retornos). Sendo assim, vamos nos
|
|
concentrar no que pode ser feito para obter os tipos esperados.
|
|
|
|
*** Conversão para inteiros com 'sscanf'
|
|
|
|
Provavelmente, a forma mais simples e direta para converter o conteúdo
|
|
de um buffer para tipos numéricos é com a função =sscanf=:
|
|
|
|
#+begin_src c
|
|
char buf[BUFMAX];
|
|
int i;
|
|
|
|
fgets(buf, BUFMAX, stdin);
|
|
|
|
sscanf(buf, "%d", &i);
|
|
#+end_src
|
|
|
|
Aqui, se houver dígitos válidos no começo de =buf=, eles serão convertidos
|
|
em seu correspondente inteiro na base 10, exatamente como seria feito
|
|
lendo uma entrada digitada no terminal com a função =scanf= -- e com suas
|
|
mesmas limitações:
|
|
|
|
- A dificuldade de detectar entradas inválidas (por exemplo, =42abc= seria
|
|
casado e convertido para o inteiro =42=);
|
|
- O retorno será apenas a quantidade de casamentos com a string de formato
|
|
que forem convertidos e escritos nas variáveis, ou =EOF=, no caso do fim
|
|
do buffer ser alcançado antes de um primeiro casamento ou no caso de
|
|
um erro;
|
|
- Não oferece uma detecção de erros detalhada, como com as funções que
|
|
alteram a variável =errno=.
|
|
|
|
Além disso, =sscanf= não lida bem com valores fora dos limites do tipo de
|
|
destino, por exemplo:
|
|
|
|
#+begin_src c
|
|
#include <stdio.h>
|
|
#include <limits.h>
|
|
|
|
int main(void) {
|
|
char str[] = "9999999999";
|
|
int i = 0;
|
|
|
|
sscanf(str, "%d", &i);
|
|
|
|
printf("Valor de INT_MAX : %d\n", INT_MAX);
|
|
printf("Dígitos na string: %s\n", str);
|
|
printf("Valor convertido : %d\n", i);
|
|
|
|
return 0;
|
|
}
|
|
#+end_src
|
|
|
|
|
|
Neste exemplo, o comportamento de =sscanf= é indeterminado, mas compilaria
|
|
sem erros:
|
|
|
|
#+begin_example
|
|
:~$ gcc -Wall teste.c
|
|
:~$ ./a.out
|
|
Valor de INT_MAX : 2147483647
|
|
Dígitos na string: 9999999999
|
|
Valor convertido : 1410065407
|
|
#+end_example
|
|
|
|
Aqui, =9999999999= é convertido para um valor com 5 bytes (=0x02540be3ff=),
|
|
dos quais, apenas os 4 últimos cabem no espaço do tipo =int= (=0x540be3ff=),
|
|
resultando no valor =1410065407=.
|
|
|
|
*** Conversão para inteiros longos com 'strtol'
|
|
|
|
A função =strtol= converte a parte inicial de uma string para =long= de
|
|
acordo com a base de numeração indicada. O endereço do primeiro caractere
|
|
inválido é atribuído a um ponteiro -- e isso nos dá uma excelente forma
|
|
de controle da conversão.
|
|
|
|
Simplificadamente, este seria o protótipo da função:
|
|
|
|
#+begin_src c
|
|
long strtol(char *str, char **endptr, int base);
|
|
#+end_src
|
|
|
|
Exemplo:
|
|
|
|
#+begin_src c
|
|
#include <stdio.h>
|
|
#include <stdlib.h>
|
|
|
|
int main(void) {
|
|
char str[] = "1234567abc";
|
|
char *end;
|
|
|
|
long i = strtol(str, &end, 10);
|
|
|
|
printf("Valor convertido : %ld\n", i);
|
|
printf("Primeiro caractere inválido: 0x%02x\n", *end);
|
|
|
|
return 0;
|
|
}
|
|
#+end_src
|
|
|
|
Compilando e executando:
|
|
|
|
#+begin_example
|
|
:~$ gcc -Wall teste.c
|
|
:~$ ./a.out
|
|
Valor convertido: 1234567
|
|
Primeiro caractere inválido: 0x61
|
|
#+end_example
|
|
|
|
#+begin_quote
|
|
O caractere =0x61= é =a=, na tabela ASCII.
|
|
#+end_quote
|
|
|
|
Outros detalhes:
|
|
|
|
- No caso de valores maiores que o limite máximo de um inteiro longo,
|
|
o retorno será =LONG_MAX=.
|
|
- No caso de valores menores que o limite mínimo de um inteiro longo,
|
|
o retorno será =LONG_MIN=.
|
|
- Em ambos os casos, a variável =errno= recebe o valor de =ERANGE= (valor
|
|
fora da faixa do tipo).
|
|
- Se o valor for inválido para a base de numeração, =errno= recebe
|
|
o valor de =EINVAL= (valor inválido para a base).
|
|
- A descrição do erro em =errno= pode ser impressa com a função =perror=,
|
|
chamada logo após =strtol=.
|
|
- Se não houver dígitos e caracteres válidos no começo da string,
|
|
o endereço do primeiro caractere inválido será o mesmo da string
|
|
e a função retornará =0=.
|
|
- O ponto negativo de =strtol= é que o retorno é =long= e, se precisarmos
|
|
de um valor =int=, teremos que verificar os limites máximo e mínimo
|
|
do valor convertido.
|
|
|
|
*** Outras funções de conversão
|
|
|
|
- =strtoll= - Idêntica a =strtol=, mas converte para =long long int=.
|
|
- =strtod=, =strtof= e =strtold= - Funcionam como =strtol=, mas convertem
|
|
para =double=, =float= e =long double=, respectivamente, sem a passagem
|
|
de uma base de numeração.
|
|
- =atoi=, =atof= e =atol= - São muito simples, mas devem ser evitadas pela
|
|
falta de validação e de meios para tratamento de erros.
|