Entrada e saída

[monitor-2.png]

[keyboard-3.png]

Este capítulo descreve, superficialmente, as funções de entrada (= input) e de saída (= output) mais importantes da linguagem C.  Todas estão na biblioteca stdio. Para ter acesso a essa biblioteca, seu programa deve incluir a interface da biblioteca por meio de

#include <stdio.h>

Sumário:

Teclado e tela do terminal

A função  printf  (abreviatura de print formatted) exibe na tela do terminal uma lista formatada de números, caracteres, strings, etc.  O primeiro argumento da função é uma string que especifica o formato da impressão.

A função  scanf  (abreviatura de scan formatted) recebe do teclado uma lista de números, caracteres, strings, etc.  O primeiro argumento da função é uma string que especifica o formato da lista. Os demais argumentos são os endereços das variáveis onde os valores recebidos devem ser armazenados.  A função trata todos os brancos como se fossem espaços (caracteres ' ').  Veja um exemplo:

#include <stdio.h>
#include <stdlib.h>

int main (void) {
   int a, b;
   scanf ("%d %d", &a, &b);
   double media;
   media = (a + b)/2.0;
   printf ("A média de %d e %d é %f\n", a, b, media);
   return EXIT_SUCCESS;
}

Se o nome do programa for average, veremos o seguinte resultado na tela (o computador escreve em vermelho):

~$ ./average
222 333
A média de 222 e 333 é 277.500000
~$

Para exibir o caractere ASCII representado por um char ou unsigned char, use a função printf com especificação de formato  %c.  Por exemplo, o fragmento de programa

char a = 65; // equivalente: a = 'A';
printf ("%d %c", a, a);

exibe

65 A

Para ler um caractere ASCII do teclado, use a função scanf com especificação de formato %c. A função lê o primeiro caractere não-branco (os brancos anteriores são desprezados), converte o caractere lido no correspondente byte, e armazena o byte em a:

char a;
scanf ("%c", &a);

Para ler uma cadeia de caracteres do teclado, use scanf com especificação de formato %s. A função interrompe a leitura ao encontrar o primeiro caractere branco. (Para ler uma cadeia de caracteres que contém brancos, use a função fgets.)  Por exemplo, o fragmento de programa abaixo lê uma cadeia de caracteres, converte a cadeia numa string e armazena a string no vetor str:

char str[100];
scanf ("%s", str); 

Arquivos

Um arquivo (= file) é uma sequência de bytes que reside na memória lenta do computador (um disco magnético, por exemplo). Os bytes de um arquivo não têm endereços individuais. Assim, o acesso ao arquivo é estritamente sequencial: para chegar ao 5º byte é preciso passar pelo 1º, 2º, 3º e 4º bytes.

Para que um programa possa manipular um arquivo, é preciso associá-lo a uma variável do tipo  FILE  (esse tipo está definido na interface stdio.h).  A operação de associação é conhecida como abertura do arquivo e é executada pela função fopen (= file open). O primeiro argumento da função é o nome do arquivo e o segundo argumento é "r" ou "w" para indicar se o arquivo deve ser aberto para leitura (= read) ou para escrita (= write). A função fopen devolve o endereço de um FILE (ou NULL, se não encontrar o arquivo especificado).  Depois de usar o arquivo, é preciso fechá-lo com a função fclose (= file close).

Digamos, por exemplo, que o arquivo de texto dados.txt contém uma sequência de números inteiros (em notação decimal) separados por brancos. O programa abaixo calcula a média desses números. Para ler o arquivo, o programa usa a função fscanf (o nome é uma abreviatura de file scanf), que generaliza a função scanf:

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main (void) {
   FILE *entrada;
   entrada = fopen ("dados.txt", "r");
   if (entrada == NULL) {
      printf ("\nNão encontrei o arquivo!\n");
      exit (EXIT_FAILURE);
   }  
   double soma = 0.0;
   int n = 0;
   while (true) {
      int x;
      int k = fscanf (entrada, "%d", &x);
      if (k != 1) break;
      soma += x;
      n += 1;
   }
   fclose (entrada);
   printf ("A média dos números é %f\n", soma/n);
   return EXIT_SUCCESS;
}

A função fscanf, tal como a função scanf, devolve o número de objetos efetivamente lidos. Se não houver um número a ser lido, devolve 0.  O programa acima usa isso para detectar o fim do arquivo. (O programa supõe que o arquivo contém pelo menos um número.)

Stdin e stdout.  O arquivo padrão de entrada (= standard input) é o teclado.  Ele está permanente aberto e é representado pela constante stdin. Portanto fscanf (stdin, ...) equivale a scanf (...).

Algo análogo acontece com as funções printf, fprintf e o arquivo padrão de saída stdout, que corresponde à tela do terminal.

As funções putc e getc

A função mais básica de saída — mais básica que fprintf — é  putc  (o nome é uma abreviatura de put character).  A função recebe um byte e grava-o no arquivo especificado.  (Muitas vezes, o byte tem valor entre 0 e 127 e portanto representa um caractere ASCII.)  Se c é um char e arq é um FILE então putc (c, arq) grava c em arq.  Por exemplo, putc ('?', stdout) exibe o caractere ? na tela.

A correspondente função de entrada é  getc  (o nome é uma abreviatura de get character).  Cada chamada da função lê um byte do arquivo especificado.  (Muitas vezes, o byte lido representa um caractere ASCII.)  Por exemplo, getc (stdin) lê o próximo byte do teclado e devolve o byte lido.

As expressões putchar (x) e getchar () são abreviaturas de putc (x, stdout) e getc (stdin) respectivamente.

Exemplo.  O programa abaixo lê do teclado uma sequência de bytes que termina com \n (como uma linha de caracteres ASCII, por exemplo), armazena essa sequência em um vetor, e exibe os correspondentes caracteres na tela.  O programa supõe que a sequência tem no máximo 100 bytes (incluindo o \n final):

#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

int main (void) {
   char linha[100];
   int n = 0;
   while (true) {
      linha[n] = getchar ();
      if (linha[n] == '\n') break;
      n = n + 1;
   }
   for (int i = 0; i <= n; i += 1)
      putchar (linha[i]);
   return EXIT_SUCCESS;
}

Outro exemplo.  O programa abaixo deveria ler o primeiro byte do arquivo dados.txt e exibir o correspondente caractere (supõe-se que seja um caractere ASCII) na tela:

#include <stdio.h>
#include <stdlib.h>

int main (void) {
   FILE *entrada;
   entrada = fopen ("dados.txt", "r");
   if (entrada == NULL) exit (EXIT_FAILURE);
   char c; // erro!
   c = getc (stdin);
   fclose (entrada);
   putc (c, stdout);
   return EXIT_SUCCESS;
}

O programa ter um defeito a ser discutido na próxima seção.

Que tipo de objeto getc devolve?

Que acontece se getc tenta ler o próximo byte de um arquivo que já se esgotou?  Seria preciso que getc devolvesse algum tipo de byte inválido. Mas todos os 256 bytes são válidos!

Para resolver esse impasse, a função getc sempre devolve um int, não um byte. O conjunto de valores int contém todos os valores char e mais alguns.  Assim, se o arquivo tiver se esgotado, getc pode devolver um int distinto de qualquer char.  Mais especificamente,

  1. se houver um próximo byte no arquivo, getc lê o byte como se fosse unsigned char, converte-o em um int positivo, e devolve o resultado;
  2. se o arquivo não tiver mais bytes, getc devolve um int estritamente negativo.

Para ser mais exato, se o arquivo não tiver mais bytes, getc devolve a constante  EOF  (o nome é uma abreviatura de end of file), que está definida na interface stdio.h e vale −1 na maioria dos computadores.

Resumindo, a função getc devolve um elemento de um superconjunto do conjunto em que estamos realmente interessados. Assim, a resposta de getc é sempre do mesmo tipo (um int), até em situações excepcionais.  Esse truque é uma importante lição de projeto (design) de programas.

Exemplo.  O seguinte fragmento de código exibe o próximo byte do arquivo a menos que estejamos no fim do arquivo:

   int c;
   c = getc (entrada);
   if (c != EOF) 
      putc (c, stdout);
   else 
      printf ("\nO arquivo terminou!");

(A propósito, se o arquivo entrada for stdin, o fim do arquivo é produzido pela combinação de teclas Ctrl D, que gera o byte 4.)

Exercícios 1

  1. Um programador propõe redefinir a função getc de modo que ela devolva um char (e não um int) e a expressão getc (entrada) tenha o seguinte efeito: se não houver um próximo byte no arquivo entrada, a função devolve o caractere \a e exibe a mensagem arquivo acabou na tela.  Critique essa proposta.
  2. Escreva um programa completo que faça uma cópia byte-a-byte do arquivo cujo nome é digitado pelo usuário.  [Solução: ./solucoes/io1.html]
  3. Escreva um programa que remova os comentários (do tipo /*...*/ e do tipo //...) do arquivo-fonte de um programa C.  O resultado deve ser gravado em um novo arquivo-fonte.

As funções fputs e fgets

A função fputs (o nome é uma abreviatura de file put string) da biblioteca stdio grava uma string num arquivo. Por exemplo,

fputs (str, arq);

grava a string str (exceto seu byte nulo final) no arquivo arq. (O efeito é essencialmente o mesmo de fprintf (arq, "%s", str).)

Se o segundo argumento de fputs for stdout, a string str é exibida na tela do terminal depois de ser convertida em uma cadeia de caracteres.

A função fgets (o nome é uma abreviatura de file get string) da biblioteca stdio lê uma linha de um arquivo e armazena a linha em uma string. Por exemplo, se str é um vetor de bytes então

fgets (str, 100, arq); 

armazena em str a linha corrente do arquivo arq (inclusive o \n que marca o fim da linha) e acrescenta um byte nulo final.

O segundo argumento de fgets é uma proteção contra linhas muito longas, mais longas que o espaço alocado para o primeiro argumento. No exemplo acima, se o número de bytes da linha corrente do arquivo (incluído o \n final) for maior que 99, somente os 99 primeiros bytes serão armazenados em str.

Se o último argumento de fgets for stdin, a função lê uma cadeia de caracteres do teclado até o fim da linha (tecla ), converte a cadeia numa string, e armazena a string no endereço indicado pelo primeiro agumento.

Argumentos na linha de comando

A execução de qualquer programa C consiste na execução da função main (que em geral invoca outras funções).  A função main admite dois parâmetros, que chamaremos numargs e arg.  O segundo parâmetro é um vetor de strings e o primeiro é o número de elementos do vetor.

int main (int numargs, char *arg[]) {
   . . . 
} 

Os argumentos são digitadas pelo usuário no teclado ao invocar o programa. (O usuário digita um ou mais espaços para separar cada argumento do anterior.)  A primeira string digitada é o nome do programa e passará a ser o valor de arg[0].  As strings seguintes são conhecidas como argumentos na linha de comando (= command line arguments) e passarão a ser os valores de arg[1], arg[2], … , arg[numargs-1].  O valor de numargs é definido implicitamente pelo número de strings digitadas.

No seguinte exemplo, estamos supondo que o nome do programa é prog.  Depois que o usuário digita a linha de comando

~$ ./prog -a  bb   ccc 2222

o valor de numargs será 5 e os valores de arg[0] a arg[4] serão as strings  "prog", "-a", "bb", "ccc" e "2222" respectivamente.

Exemplo.  O seguinte programa calcula a média dos números inteiros digitados na linha de comando.  O usuário precisa garantir que cada um dos números digitados esteja no intervalo INT_MIN..INT_MAX.

#include <stdio.h>
#include <stdlib.h>

int main (int numargs, char *arg[]) {
   int soma = 0;
   for (int i = 1; i < numargs; ++i) 
      soma += strtol (arg[i], NULL, 10);
   int n = numargs - 1;
   printf ("média = %.2f\n", (double) soma / n);
   return EXIT_SUCCESS;
} 

Se o nome do programa é prog, veremos a seguinte interação na tela:

~$ ./prog +22   33 -11      +44
média = 22.00
~$

Outro exemplo.  O seguinte programa exibe uma tabela de conversão de graus Celsius em graus Fahrenheit ou vice-versa. O usuário especifica a direção da conversão, bem como o início e o fim da tabela.

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Programa temperatura
// --------------------
// Para obter uma tabela de conversão de graus
// Celsius em graus Fahrenheit, digite
// 
//     ./temperatura c-f 10 40
//
// A primeira coluna começará com 10 graus 
// Celsius e andará em passos de 1 grau até 40
// graus Celsius. A segunda coluna trará a 
// correspondente temperatura em graus
// Fahrenheit. Troque "c-f" por "f-c" para
// obter a tabela de conversão de graus 
// Fahrenheit em graus Celsius.

int main (int numargs, char *arg[]) {
   if (numargs != 4) {
      printf ("Número de argumentos errado.\n");
      return EXIT_FAILURE;
   }
   int inf = strtol (arg[2], NULL, 10);
   int sup = strtol (arg[3], NULL, 10);
   // arg[2] e arg[3] devem representar inteiros 
   // no intervalo INT_MIN..INT_MAX
   if (strcmp (arg[1], "c-f") == 0) {
      printf ("Celsius Fahrenheit\n");
      for (int c = inf; c <= sup; c += 1) 
         printf ("%7d %10.2f\n", c, 9.0/5.0*c + 32);
      return EXIT_SUCCESS;
   }
   if (strcmp (arg[1], "f-c") == 0) {
      printf ("Fahrenheit  Celsius\n");
      for (int f = inf; f <= sup; f += 1) 
         printf ("%10d %8.2f\n", f, 5.0*(f-32)/9.0);
      return EXIT_SUCCESS;
   }
   return EXIT_FAILURE;
} 

Redirecionamento de entrada/saída.  Se um argumento na linha de comando for o nome de um arquivo precedido pelo caractere  <  então esse arquivo passa a fazer o papel da entrada padrão.  Analogamente, se um argumento na linha de comando for o nome de um arquivo precedido pelo caractere  >  então esse arquivo passa a fazer o papel da saída padrão.  Por exemplo, se o arquivo in.txt no diretório corrente contém

222  333

e average é o nome do programa no primeiro exemplo deste capítulo, então a invocação

~$ ./average  < in.txt > out.txt

produzirá um arquivo out.txt com o seguinte conteúdo:

A média de 222 e 333 é 277.500000

Exercícios 2

  1. Modifique o programa do exemplo acima de modo a interromper a execução se strtol devolver um número que não cabe em um int.
  2. Escreva e teste um programa que exiba uma tabela com os valores de todos os argumentos digitados pelo usuário na linha de comando (e pare em seguida).
  3. Word count.  Escreva um programa que conte o número de ocorrências de cada caractere em um arquivo de texto cujos bytes representam caracteres ASCII.  O programa deve receber o nome do arquivo pela linha de comando e exibir uma tabela com o número de ocorrências de cada caractere.  (Para ganhar inspiração, analise o comportamento do utilitário wc.)

Perguntas e respostas