cblc/aulas/12-fgets/README.org
2025-04-30 07:53:11 -03:00

14 KiB
Raw Permalink Blame History

Curso Básico da Linguagem C

Aula 12: Leitura da entrada padrão com 'fgets'

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:

char buf[10];
scanf("%9s", buf);

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:

#include <stdio.h>
...
#define BUFMAX 10
...
int main(...) {
    char buf[BUFMAX];
    ...
    scanf("%9s", buf);
    ...
}

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:

char *fgets(char s[restrict .size], int size, FILE *restrict stream);
  • 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:

#include <stdio.h>

#define BUFMAX 10

int main(void) {
    char buf[BUFMAX];
    fgets(buf, BUFMAX, stdin);

    ...
}

Aqui, stdin é uma macro que expande uma estrutura FILE predefinida com o descritor de arquivos 0 (entrada padrão).

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 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:

#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;
}

Executando o programa:

:~$ ./a.out
Digite até 9 caracteres: 123456789
123456789
:~$ ./a.out
Digite até 9 caracteres: 12345
12345

:~$

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.

        +------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x36 | 0x37 | 0x38 | 0x39 | 0x00 |
        +------+------+------+------+------+------+------+------+------+------+

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.

        +------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x0A | 0x00 | lixo | lixo | lixo |
        +------+------+------+------+------+------+------+------+------+------+

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:

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++;
}

Se estiver entrando com um buffer muito grande, o tipo de size pode ser alterado para size_t (unsigned long int).

No nosso exemplo, a função trim_string poderia ser utilizada assim:

# 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++;
}

Compilando e executando novamente:

:~$ ./a.out
Digite até 9 caracteres: 12345
12345
:~$

E o conteúdo do buffer seria:

        +------+------+------+------+------+------+------+------+------+------+
buf[10] | 0x31 | 0x32 | 0x33 | 0x34 | 0x35 | 0x00 | 0x00 | lixo | lixo | lixo |
        +------+------+------+------+------+------+------+------+------+------+

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:

#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;
}

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:

char buf[BUFMAX];
int i;

fgets(buf, BUFMAX, stdin);

sscanf(buf, "%d", &i);

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:

#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;
}

Neste exemplo, o comportamento de sscanf é indeterminado, mas compilaria sem erros:

:~$ gcc -Wall teste.c
:~$ ./a.out
Valor de INT_MAX : 2147483647
Dígitos na string: 9999999999
Valor convertido : 1410065407

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:

long strtol(char *str, char **endptr, int base);

Exemplo:

#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;
}

Compilando e executando:

:~$ gcc -Wall teste.c
:~$ ./a.out
Valor convertido: 1234567
Primeiro caractere inválido: 0x61

O caractere 0x61 é a, na tabela ASCII.

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.