#+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 (ex: =0= ou =STDIN_FILENO=, numa leitura da entrada padrão). 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=. |