diff --git a/aulas/12-fgets/README.org b/aulas/12-fgets/README.org new file mode 100644 index 0000000..6bddfd1 --- /dev/null +++ b/aulas/12-fgets/README.org @@ -0,0 +1,287 @@ +#+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=. |