#+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 ... #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 #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 #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 #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 #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 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 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=. |