====== Acesso a arquivos ======
{{ progc_arquivos.mkv |Video desta aula}}
Aqui serão descritas algumas das funções mais usuais para operações de entrada/saída em arquivos na linguagem C. A maioria das funções aqui descritas está declarada no arquivo de cabeçalho ''stdio.h'':
#include
===== Conceitos básicos =====
==== Tipos de arquivos ====
Um arquivo armazena uma sequência de bytes, cuja interpretação fica a cargo da aplicação. Contudo, para facilitar a manipulação de arquivos, a linguagem C considera dois tipos de arquivos:
* **arquivos de texto**: contém sequências de bytes representando caracteres de texto, separadas por caracteres de controle como ''\n'' ou ''\r'' e usando uma codificação como ASCII, ISO-8859-1 ou UTF-8. São usados para armazenar informação textual, como código-fonte, páginas Web, arquivos de configuração, etc.
* **arquivos binários**: contém sequências de bytes, cuja interpretação fica totalmente a cargo da aplicação. São usualmente empregados para armazenar imagens, vídeos, músicas, dados compactados, etc.
==== Streams e descritores ====
As operações sobre arquivos em C podem ser feitas de duas formas:
* por **streams**: acesso em um nível mais elevado, independente de sistema operacional e portanto portável. Permite entrada/saída formatada, mas pode ter um desempenho inferior ao acesso de baixo nível. //Streams// são acessadas através de variáveis do tipo ''FILE*''.
* por **descritores**: acesso através dos descritores de arquivo fornecidos pelo sistema operacional, pouco portável mas com melhor desempenho.
Na sequência deste texto serão apresentadas as funções de acesso usando //streams//, que são o padrão da linguagem C e valem para qualquer sistema operacional. Explicações sobre entrada/saída usando descritores UNIX podem ser encontradas [[pua:operacoes_usando_descritores|nesta página]].
==== Entradas e saídas padrão ====
Cada programa em execução tem acesso a três arquivos padrão definidos no arquivo de cabeçalho ''stdio.h'', que são:
* ''FILE* stdin'': a entrada padrão, normalmente associada ao teclado do terminal, usada para a entrada de dados do programa (''scanf'', por exemplo).
* ''FILE* stdout'': a saída padrão, normalmente associada à tela do terminal, usada para as saídas normais do programa (''printf'', por exemplo).
* ''FILE* stderr'': a saída de erro, normalmente associada à tela do terminal, usada para mensagens de erro.
Esses três arquivos não precisam ser abertos, eles estão prontos para uso quando o programa inicia. Geralmente eles estão associados ao terminal onde o programa foi lançado, mas podem ser redirecionados pelo //shell// ([[unix:shell_avancado|mais detalhes aqui]]).
===== Abrindo e fechando arquivos =====
Antes de ser usado, um arquivo precisa ser "aberto" pela aplicação (com exceção dos arquivos padrão descritos acima, que são abertos automaticamente). Isso é realizado através da chamada ''fopen'':
FILE* fopen (const char *filename, const char *mode)
Abre um arquivo indicado por ''filename'' e retorna um ponteiro para o //stream//. A //string// ''mode'' define o modo de abertura do arquivo:
* ''r'' : abre um arquivo existente para leitura (//read//).
* ''w'' : abre um arquivo para escrita (//write//). Se o arquivo já existe, seu conteúdo é descartado. Senão, um novo arquivo vazio é criado.
* ''a'' : abre um arquivo para concatenação (//append//). Se o arquivo já existe, seu conteúdo é preservado e as escritas serão concatenadas no final do arquivo. Senão, um novo arquivo vazio é criado.
* ''r+'' : abre um arquivo existente para leitura e escrita. O conteúdo anterior do arquivo é preservado e o ponteiro é posicionado no início do arquivo.
* ''w+'' : abre um arquivo para leitura e escrita. Se o arquivo já existe, seu conteúdo é descartado. Senão, um novo arquivo vazio é criado.
* ''a+'' : abre um arquivo para escrita e concatenação. Se o arquivo já existe, seu conteúdo é preservado e as escritas serão concatenadas no final do arquivo. Senão, um novo arquivo vazio é criado. O ponteiro de leitura é posicionado no início do arquivo, enquanto as escritas são efetuadas no seu final.
Os modos ''a'' e ''a+'' **sempre** escreverão no final do arquivo, mesmo se o cursor do mesmo for movido para outra posição.
Fecha um //stream//. Os dados de saída em //buffer// são escritos, enquanto dados de entrada são descartados:
int fclose (FILE* stream)
Fecha e abre novamente um //stream//, permitindo alterar o arquivo e/ou modo de abertura:
FILE* freopen (const char *filename, const char *mode, FILE *stream)
Exemplo: abrindo o arquivo ''x'' em leitura:
#include
#include
int main ()
{
FILE* arq ;
arq = fopen ("x", "r") ;
if ( ! arq )
{
perror ("Erro ao abrir arquivo x") ;
exit (1) ; // encerra o programa com status 1
}
fclose (arq) ;
exit (0) ;
}
Exemplo: abre o arquivo ''x'' em leitura/escrita, criando-o se não existir:
#include
#include
int main (int argc, char *argv[])
{
FILE* arq ;
arq = fopen ("x", "w+") ;
if ( ! arq )
{
perror ("Erro ao abrir/criar arquivo x") ;
exit (1) ; // encerra o programa com status 1
}
fclose (arq) ;
exit (0) ;
}
===== Arquivos-texto =====
Arquivos-texto contêm sequências de bytes representando um texto simples (sem formatações especiais, como negrito, itálico, etc), como código-fonte ou uma página em HTML, por exemplo.
Em um arquivo-texto, os caracteres do texto são representados usando uma codificação padronizada, para que possam ser abertos por diferentes aplicações em vários sistemas operacionais. As codificações de caracteres mais usuais hoje são:
* [[https://en.wikipedia.org/wiki/ASCII|ASCII]]: criada em 1963 para representar os caracteres comuns da língua inglesa, usando 7 bits (valores entre 0 e 127).
* [[https://en.wikipedia.org/wiki/ISO/IEC_8859|ISO-8859]]: conjunto de extensões da codificação ASCII para suportar outras línguas com alfabeto latino, como o Português. Os caracteres entre 0 e 127 os mesmos da tabela ASCII, enquanto os caracteres entre 128 e 255 são específicos. Por exemplo, a extensão [[https://en.wikipedia.org/wiki/ISO/IEC_8859-1|ISO-8859-1]] contém os caracteres acentuados e cedilhas da maior parte das linguagens ocidentais (Português, Espanhol, Francês etc).
* [[https://en.wikipedia.org/wiki/UTF-8|UTF-8]]: codificação baseada no padrão Unicode, capaz de representar mais de um milhão de caracteres em todas as línguas conhecidas, além de sinais gráficos (como //emojis//). Os caracteres em UTF-8 podem usar entre 1 e 4 bytes para sua representação, o que torna sua manipulação mais complexa em programas.
Além dos caracteres em si, as codificações geralmente suportam **caracteres de controle**, que permitem representar algumas estruturas básicas de formatação do texto, como quebras de linha. Alguns caracteres de controle presentes nas codificações acima são:
^ nome ^ valor ^ representação ^
| null | 0 | ''NUL'' ''\0'' ''%%^%%@'' |
| bell | 7 | ''BEL'' ''\a'' ''%%^%%G'' |
| backspace | 8 | ''BS'' ''\b'' ''%%^%%H'' |
| tab | 9 | ''HT'' ''\t'' ''%%^%%I'' |
| line feed | 10 | ''LF'' ''\n'' ''%%^%%J'' |
| form feed | 12 | ''FF'' ''\f'' ''%%^%%L'' |
| carriage return | 13 | ''CR'' ''\r'' ''%%^%%M'' |
| escape | 27 | ''ESC'' ''%%^%%['' |
===== Escrita de arquivos =====
==== Escrita simples ====
Estas funções permitem gravar caracteres ou strings simples em //streams//.
int fputc (int c, FILE* stream) // escreve o caractere c no stream
int putc (int c, FILE* stream) // idem, implementada como macro
int putchar (int c) // idem, em "stdout"
int fputs (const char *s, FILE* stream) // escreve a string s no stream
int puts (const char *s) // idem, em "stdout"
Um exemplo de uso de operações de escrita simples em arquivo:
#include
#include
int main ()
{
FILE *arq ;
unsigned char c ;
// abre o arquivo em escrita
arq = fopen ("ascii.txt", "w+") ;
if ( ! arq )
{
perror ("Erro ao abrir/criar arquivo") ;
exit (1) ;
}
// escreve os caracteres ascii
fputs ("caracteres ASCII:", arq) ;
for (c = 32; c < 128; c++)
{
fputc (c, arq) ;
fputc (' ', arq) ;
}
fputc ('\n', arq) ;
// fecha o arquivo
fclose (arq) ;
}
==== Escrita formatada ====
As operações de entrada e saída formatada usam padrões para formatação dos diversos tipos de dados descritos em livros de programação em C e no manual da GLibC.
Escreve dados usando a formatação definida em ''format'' no //stream// de saída padrão ''stdout'':
int printf (const char* format, ...)
Idêntico a ''printf'', usando o stream indicado:
int fprintf (FILE* stream, const char* format, ...)
Similar a ''printf'', mas a saída é depositada na string ''str'':
int sprintf (char* str, const char* format, ...)
Atenção: o programador deve garantir que ''str'' tenha tamanho suficiente para receber a saída; caso contrário, pode ocorrer um //buffer overflow// com consequências imprevisíveis. As funções ''snprintf'' e ''asprintf'' são mais seguras e evitam esse problema.
#include
#include
int main ()
{
FILE *arq ;
int i, j ;
// abre o arquivo em escrita
arq = fopen ("tabuada.txt", "w+") ;
if ( ! arq )
{
perror ("Erro ao abrir/criar arquivo") ;
exit (1) ;
}
// escreve o cabeçalho
fprintf (arq, "Tabuada:\n") ;
fprintf (arq, " ") ;
for (j = 0; j <= 10; j++)
fprintf (arq, "%4d", j) ;
fprintf (arq, "\n") ;
fprintf (arq, " ") ;
for (j = 0; j <= 10; j++)
fprintf (arq, "----") ;
fprintf (arq, "\n") ;
// escreve as linhas de valores
for (i = 0; i <= 10; i++)
{
fprintf (arq, "%4i | ", i) ;
for (j = 0; j <= 10; j++)
fprintf (arq, "%4d", i*j) ;
fprintf (arq, "\n") ;
}
// fecha o arquivo
fclose (arq) ;
}
===== Leitura de arquivos =====
==== Leitura simples ====
Estas funções permitem ler caracteres isolados de um //stream//. O valor lido é um ''int'' indicando o caractere lido ou então o valor especial ''EOF'' (//End-Of-File//):
int fgetc (FILE* stream) // Lê o próximo caractere do stream
int getc (FILE* stream) // Idem, como macro (mais rápida)
int getchar () // Idem, sobre stdin
Para a leitura de strings :!::
char* gets (char *s)
Lê caracteres de ''stdin'' até encontrar um //newline// e os armazena na string ''s''. O caractere //newline// é descartado.
Atenção: a função ''gets'' é **perigosa**, pois não provê segurança contra //overflow// na string ''s''. Sempre que possível, deve ser usada a função ''fgets'' ou ''getline''.
Lê uma linha de caracteres do //stream// e a deposita na string ''s''. O tamanho da linha é limitado em ''count-1'' caracteres, aos quais é adicionado o '''\0''' que marca o fim da string. O //newline// é incluso.
char* fgets (char *s, int count, FILE *stream)
O exemplo a seguir lê e numera as 10 primeiras linhas de um arquivo:
#include
#include
#define LINESIZE 1024
int main ()
{
FILE *arq ;
int i ;
char line[LINESIZE+1] ;
// abre o arquivo em leitura
arq = fopen ("dados.txt", "r") ;
if ( ! arq )
{
perror ("Erro ao abrir arquivo") ;
exit (1) ;
}
// lê as 10 primeiras linhas do arquivo
for (i = 0; i < 10; i++)
{
fgets (line, LINESIZE, arq) ;
printf ("%d: %s", i, line) ;
}
// fecha o arquivo
fclose (arq) ;
}
==== Leitura formatada ====
Lê dados do //stream// ''stdin'' de acordo com a formatação definida na string ''format''. Os demais argumentos são ponteiros para as variáveis onde os dados lidos são depositados. Retorna o número de dados lidos ou ''EOF'':
int scanf (const char* format, ...)
Similar a ''scanf'', mas usando como entrada o //stream// indicado:
int fscanf (FILE* stream, const char* format, ...)
Similar a ''scanf'', mas usando como entrada a string ''s'':
int sscanf (const char* s, const char* format, ...)
O exemplo a seguir lê 10 valores reais de um arquivo:
#include
#include
int main ()
{
FILE *arq ;
int i ;
float value ;
// abre o arquivo em leitura
arq = fopen ("numeros.txt", "r") ;
if ( ! arq )
{
perror ("Erro ao abrir arquivo") ;
exit (1) ;
}
// lê os 10 primeiros valores do arquivo
for (i = 0; i < 10; i++)
{
fscanf (arq, "%f", &value) ;
printf ("%d: %f\n", i, value) ;
}
// fecha o arquivo
fclose (arq) ;
}
Experimente executá-lo com os dados de entrada abaixo. Pode explicar o que acontece?
10 21 4
23.7 55 -0.7
6 5723.8, 455
1, 2, 3, 4
A função ''scanf'' considera espaços (espaços em branco, tabulações e novas linhas) como separadores // default// dos campos de entrada. O arquivo ''numeros.txt'' contém uma vírgula, que não é um separador, então a leitura não pode prosseguir até que a vírgula seja lida.
Um bloco de leitura mais robusto, imune a esse problema, seria:
// lê os 10 primeiros valores do arquivo
i = 0 ;
while (i < 10)
{
ret = fscanf (arq, "%f", &value) ;
// fim de arquivo ou erro?
if (ret == EOF)
break ;
// houve leitura?
if (ret > 0)
{
printf ("%d: %f\n", i, value) ;
i++ ;
}
// não houve, tira um caractere e tenta novamente
else
fgetc (arq) ;
}
==== Fim de arquivo ====
Muitas vezes deseja-se ler os dados de um arquivo até o fim, mas não se conhece seu tamanho a priori. Para isso existem funções e macros que indicam se o final de um arquivo foi atingido.
A função recomendada para testar o final de um arquivo é ''feof(stream)''. Ela retorna 0 (zero) se o final do arquivo **não foi** atingido. Eis um exemplo de uso:
#include
#include
#define LINESIZE 1024
int main ()
{
FILE *arq ;
int i ;
char line[LINESIZE+1] ;
// abre o arquivo em leitura
arq = fopen ("dados.txt", "r") ;
if ( ! arq )
{
perror ("Erro ao abrir arquivo") ;
exit (1) ;
}
// lê TODAS as linhas do arquivo
i = 1 ;
fgets (line, LINESIZE, arq) ; // tenta ler uma linha
while (! feof (arq)) // testa depois de tentar ler!
{
printf ("%d: %s", i, line) ;
fgets (line, LINESIZE, arq) ; // tenta ler a próxima linha
i++ ;
}
// fecha o arquivo
fclose (arq) ;
}
Observe que a função ''feof()'' indica ''TRUE'' somente **após** o final do arquivo ter sido atingido, ou seja, após uma leitura falhar. Por isso, ''feof()'' deve ser testada **depois** da leitura e não antes.
A macro ''EOF'' representa o valor devolvido por funções de leitura de caracteres como ''getchar'' e ''fgetc'' quando o final do arquivo é atingido:
#include
#include
#define LINESIZE 1024
int main ()
{
FILE *arq ;
char c ;
// abre o arquivo em leitura
arq = fopen ("dados.txt", "r") ;
if ( ! arq )
{
perror ("Erro ao abrir arquivo") ;
exit (1) ;
}
// lê os caracteres até o fim do arquivo
c = fgetc (arq) ; // tenta ler um caractere
while (c != EOF) // não é o fim do arquivo
{
printf ("%c ", c) ; // tenta ler o próximo
c = fgetc (arq) ;
}
// fecha o arquivo
fclose (arq) ;
}
Por fim, esta função retorna um valor não nulo se ocorreu um erro no último acesso ao //stream//:
int ferror (FILE* stream)
Além de ajustar o indicador de erro do //stream//, as funções de acesso a //streams// também ajustam a variável ''errno''.
===== Exercícios =====
- Escreva um programa em C para informar o número de caracteres presentes em um arquivo de texto.
- Escreva um programa em C que leia um arquivo de texto com números reais (um número por linha) e informe a média dos valores lidos.
- Escreva um programa em C para ler um arquivo ''minusc.txt'' e escrever um arquivo ''maiusc.txt'' contendo o mesmo texto em maiúsculas.
- O arquivo {{ mapa.txt}} contém o mapa de um nível do jogo [[https://en.wikipedia.org/wiki/Boulder_Dash|Boulder Dash]]. Escreva um programa em C que carregue esse mapa em uma matriz de caracteres.
- Escreva um programa ''mycp'' para fazer a cópia de um arquivo em outro: ''mycp arq1 arq2''. Antes da cópia, ''arq1'' deve existir e ''arq2'' não deve existir. Mensagens de erro devem ser geradas caso essas condições não sejam atendidas ou o nome dado a ''arq2'' seja inválido. Para acessar os nomes dos arquivos na linha de comando use os parâmetros ''argc'' e ''argv'' ([[a função main|veja aqui]]).
- o comando ''grep'' do UNIX imprime na saída padrão (//stdout//) as linhas de um arquivo de entrada que contenham uma determinada string informada como parâmetro. Escreva esse programa em C (dica: use a função ''strstr'').
Mais exercícios no capítulo 11 da {{apostila_c_-_nce.pdf|apostila do NCE/UFRJ}}.