Capítulo 11 - Entrada e Saída



Este capítulo focará algumas das muitas formas de Entrada/Saída (I/O) da Linguagem C. Algumas dessas formas foram já brevemente mencionadas em capítulos anteriores, mas tornaremos a estudá-las, com muito mais detalhe aqui. Praticamente todas as formas de I/O do C têm os seus tipos e funções declarados no arquivo de inclusão da biblioteca standard stdio.h.

Tópicos



Streams

Os streams constituem uma forma portável de ler e escrever informação nos programas escritos em C. São também uma forma bastante eficiente e flexível de efectuar essa transferência de informação.

Um stream é um arquivo ou outro dispositivo de entrada/saída (por exemplo: o terminal, o teclado, a impressora, etc) que é manipulado através de um apontador para o stream. Na biblioteca standard os streams são representados por uma estrutura definida em stdio.h e com o nome de FILE. Os programas que manipulam streams usam então um apontador para esta estrutura (FILE *).

Do ponto de vista do programa em C não é necessário conhecer mais nada acerca desta estrutura. Basta declarar um apontador para ela e usá-lo para efectuar operações de I/O (escrita e/ou leitura). Antes de se poderem efectuar propriamente as operações de transferência de informação para os streams é necessário executar uma operação de abertura (open). Quando já não houver mais necessidade de novas operações de transferência de informação podemos (e devemos) fechar o stream com uma operação de fecho (close).

As operações de I/O em streams são bufferizadas, isto é, existe sempre uma área de memória intermédia (o buffer) para e de onde são efectuadas as transferências de informação para os dispositivos que os streams representam. Na ilustração seguinte pode ver-se um esquema deste mecanismo.

A existência de um buffer leva a uma maior eficiência nas operações de I/O, no entanto os dados presentes no buffer não são imediatamente transferidos para o stream. Qualquer terminação anormal de um programa pode conduzir a perda de informação se esta ainda não tiver sido transferida.

Streams pré-definidos

No UNIX, e na maior parte doutros Sistemas Operativos definem-se à partida pelo menos 3 streams, já abertos e prontos a utilizar: Todos eles usam texto como modo de I/O.

Redireccionamento

É um processo de associar os streams pré-definidos stdin e stdout com arquivos ou outros dispositivos de I/O que não o terminal e o teclado. O redireccionamento não faz parte da linguagem C mas sim do sistema operativo. Em geral é efectuado a partir da linha de comando (p. ex. no UNIX).

O símbolo > é utilizado para redireccionar o stdout para um arquivo. Assim se tivermos um programa out que escreve normalmente no vídeo do terminal, podemos dirigir a sua saída directamente para um arquivo invocando o comando:

out > file1
O símbolo < utiliza-se para redireccionar um arquivo especificado para o stream pré-definido stdin de um programa. Assim, se tivermos um programa denominado in que espera dados provenientes do teclado, estes, através do redireccionamento, podem provir de um arquivo, como se mostra a seguir:
in < file2
Pode utilizar-se ainda o símbolo | (pipe), que faz uma ligação directa entre o stdout de um programa e o stdin de um outro programa:
prog1 | prog2
A saída de prog1, que normalmente seria escrita no terminal, funciona como entrada de prog2, que normalmente a esperaria vinda do teclado.
 
Up

Operações básicas de entrada/saída

As funções mais básicas que permitem efectuar operações de entrada/saída nos streams pré-definidos stdin e stdout são: Estas funções, bem como todas as outras que dizem respeito a operações de entrada/saída suportadas pela biblioteca standard do C, estão declaradas no arquivo de inclusão stdio.h.

Exemplo:

#include <stdio.h>
...
int ch;
...
ch = getchar();
putchar((char) ch);
...
Existem funções semelhantes para leitura e escrita de um carácter noutros streams que não o stdin e stdout:  
Up

Entrada/saída formatadas

Já temos visto, em exemplos anteriores, a escrita de texto e valores, com um formato especificado, no terminal utilizando a função printf(). Vamos ver a utilização desta função em maior detalhe a seguir. Veremos também a função inversa, que lê valores do teclado directamente para variáveis - a função scanf().

Printf

A função, também declarada em stdio.h, é definida como se mostra a seguir:
int printf(char *format, ...); - Escreve em stdout a sua lista de argumentos (especificada em ...) de acordo com um formato especificado na string format; retorna o número de caracteres escrito.
A string format contém 2 tipos de especificações:  
Especificador de formato 
(a seguir a %)
Tipo Resultado
c char um único carácter
i ou d int número inteiro
o int número em octal
x ou X int número em hexadecimal 
(com letras minúsculas ou maiúsculas)
u unsigned int número inteiro sem sinal
s char * escreve o string terminado com \0
f double ou float número real com parte decimal
e ou E double ou float  número real escrito em notação científica 
g ou G double ou float equivalente a e ou f 
 (é escolhido o que ocupar menos espaço) 
hi ou hd short número inteiro curto
li ou ld long número inteiro longo
Lf long double número real com parte decimal
% - escreve o carácter %
 Em geral os prefixos h e l representam os modificadores short e long, respectivamente.

Entre o carácter % e o carácter especificador de formato podemos colocar ainda os seguintes indicadores:

Alguns exemplos:
printf("%-2.3f\n", 17.23478);
produz no terminal a saída:    17.234
printf("VAT = 17.5%%\n");
que escreve:   VAT = 17.5%

Scanf

A função scanf() é definida em stdio.h como se segue:
int scanf(char *format, ...); - lê caracteres de stdin e coloca os valores lidos e convertidos nas variáveis cujos endereços são passados na lista de argumentos a seguir à indicação do formato; retorna o número de caracteres lidos e convertidos.
A indicação do formato é muito semelhante à especificada para printf(). A única excepção diz respeito aos especificadores f, e ou g, que aqui se referem exclusivamente a valores do tipo float ( os valores lidos e convertidos deverão ser passados a apontadores para variáveis do tipo float). Para especificar valores do tipo double deverão ser usados os especificadores lf, le ou lg.

Como já se disse a lista de argumentos, que irão receber os valores lidos, deverá conter apenas apontadores ou endereços de variáveis. Por exemplo:

scanf("%d", &i);
para ler um valor inteiro do teclado e colocá-lo na variável de tipo int i.

No caso de arrays (strings, por exemplo) o próprio nome pode ser directamente usado na lista de argumentos, uma vez que representa o endereço da 1ª posição do array. Veja-se o exemplo:

char str[80];
...
scanf("%s", str);
 
Up

Arquivos

Os streams mais comuns são os que ficam associados a arquivos armazenados em disco. A primeira operação necessária para trabalhar com esse tipo de streams é uma operação de abertura, efectuada com a função fopen():
FILE *fopen(char *name, char *mode);
A função fopen() retorna um apontador para uma estrutura FILE. O parâmetro name é o nome do arquivo armazenado no disco que se pretende aceder. O parâmetro mode controla o tipo de acesso. Se o arquivo especificado não puder ser acedido, por qualquer razão, a função retornará um apontador nulo (NULL).

Os modos principais incluem:

É possível acrescentar aos designadores de modo as letras 't' ou 'b', que especificam o tipo de informação do arquivo: textual ou binária, respectivamente.

O apontador retornado pela função deve ser guardado, uma vez que é necessário como parâmetro para todas as funções de acesso ao stream assim aberto.

Exemplo de abertura de um arquivo chamado myfile.dat para leitura apenas:

#include <stdio.h>
...
FILE *stream;
...
stream = fopen("myfile.dat", "rb");
É sempre boa política verificar se os arquivos que se pretendem abrir, o foram efectivamente:
if ( (stream = fopen("myfile.dat", "rb")) == NULL) {
   printf("Can't open %s\n", "myfile.dat");
   exit(1);
}

Leitura e escrita de arquivos

As funções fprintf() e fscanf() são utilizadas para aceder a streams associados a arquivos de um modo idêntico às funções printf() e scanf(), que estão associadas à saída standard (stdout) e entrada standard (stdin) respectivamente. Incluem apenas mais um parâmetro que é o apontador para FILE obtido com a função fopen():
int fprintf(FILE *stream, char *format, ...);
int fscanf(FILE *stream, char *format, ...);
Exemplo:
#include <stdio.h>
...
char string[80];
FILE *stream;
...
if ( (stream = fopen(...)) != NULL)
   fscanf(stream, "%s", string);
...
Outras funções que trabalham com streams associados a arquivos:
int fgetc(FILE *stream);
int fputc(char ch, FILE *stream);
As duas funções anteriores são idênticas a getchar() e putchar(), já descritas.
size_t fread(void *ptr, size_t size, size_t nobj, FILE *stream); - lê do stream para o array apontado por ptr um número de bytes igual a size*nobj; retorna o número de objectos lidos, que pode ser menor do que nobj;
size_t fwrite(void *ptr, size_t size, size_t nobj, FILE *stream); - escreve no stream provenientes do array apontado por ptr um número de bytes igual a size*nobj; retorna o número de objectos escritos, que pode ser menor do que nobj;
int fflush(FILE *stream); - transfere qualquer informação que porventura ainda se encontre no buffer associado ao stream para o respectivo arquivo;
int fclose(FILE *stream); - fecha o stream desassociando-o do arquivo;
Os streams pré-definidos e abertos também podem ser acedidos com as funções próprias dos arquivos:
fprintf(stderr, "Cannot compute!!\n");
fscanf(stdin, "%s", string);
 
Up

As funções sprintf e sscanf

Estas funções são em tudo idênticas a fprintf() e fscanf(), excepto no facto de escreverem ou lerem para ou de strings em vez de arquivos:
int sprintf(char *string, char *format, ...);
int sscanf(char *string, char *format, ...);
Por exemplo:
#include <stdio.h>
...
float full_tank = 47.0;
float miles = 300;
char miles_per_litre[80];
...
sprintf(miles_per_litre, "Miles per litre = %2.3f", miles / full_tank);
...
 
Up

Passagem de parâmetros para programas

A linguagem C permite a passagem de argumentos, especificados na linha de comando que invoca um programa, para a função que começa a execução do programa. Estes argumentos aparecem na linha de comando logo após o nome do programa e são separados por um espaço. São passados à função main() como strings, através de um mecanismo especial.

Para receber estes argumentos a função main() tem de ser declarada como:

main(int argc, char **argv)
Os parâmetros argc e argv têm o seguinte significado: Um programa simples que utiliza este mecanismo é:
#include <stdio.h>

void main(int argc, char **argv)
{
   /* programa que imprime os seus argumentos */

   int i;

   printf("argc = %d\n\n", argc);
   for (i=0; i<argc; ++i)
      printf("argv[%d]: %s\n", i, argv[i]);
}

Se este programa for compilado como args.exe e se for invocado como:
args f1 f2 "f3 a" 4 stop!
a saída será:
argc = 6

argv[0]: args
argv[1]: f1
argv[2]: f2
argv[3]: f3 a
argv[4]: 4
argv[5]: stop!

Notas: argv[0] é o nome do programa; argc conta também o nome do programa; os argumentos são delimitados por espaço, excepto se o argumento for incluído entre aspas; as aspas não fazem parte do argumento.
 
Up

Exercícios

1. Escreva um programa que copie um arquivo para outro. Os nomes dos arquivos são passados como argumentos da invocação do programa. O arquivo deve ser copiado utilizando blocos de 512 bytes. Deverá verificar que foram efectivamente passados 2 argumentos ao programa, que o primeiro argumento pode ser aberto para leitura e ainda que o segundo argumento pode ser aberto para escrita.

2. Escreva um programa, denominado last, que escreve no vídeo as últimas n linhas de um arquivo de texto. Por defeito o valor de n é 5, podendo ser modificado através de um argumento passado na linha de comando (-n). O nome do arquivo de texto é também passado na linha de comando. A sintaxe para invocar este programa é então:    last [-n] file_name

3. Escreva um programa que compare 2 arquivos de texto, passados na linha de comando. Sempre que 2 linhas diferirem, estas deverão ser escritas no vídeo com a indicação do arquivo a que pertencem.
 
Up