sábado, julho 09, 2011

Aula 10 - Tipos de Dados Avançados


Já vimos que uma variável é declarada como
tipo_da_variável lista_de_variáveis;
Vimos também que existem modificadores de tipos. Estes modificam o tipo da variável declarada. Destes, já vimos os modificadores signed, unsigned, long, e short. Estes modificadores são incluídos na declaração da variável da seguinte maneira:
modificador_de_tipo tipo_da_variável lista_de_variáveis;
Vamos discutir agora outros modificadores de tipo.

Modificadores de Acesso

Estes modificadores, como o próprio nome indica, mudam a maneira com a qual a variável é acessada e modificada.

const

O modificador const faz com que a variável não possa ser modificada no programa. Como o nome já sugere é útil para se declarar constantes. Poderíamos ter, por exemplo:
const float PI=3.141;
Podemos ver pelo exemplo que as variáveis com o modificador const podem ser inicializadas. Mas PI não poderia ser alterado em qualquer outra parte do programa. Se o programador tentar modificar PI o compilador gerará um erro de compilação.
O uso mais importante de const não é declarar variáveis constantes no programa. Seu uso mais comum é evitar que um parâmetro de uma função seja alterado pela função. Isto é muito útil no caso de um ponteiro, pois o conteúdo de um ponteiro pode ser alterado por uma função. Para tanto, basta declarar o parâmetro como const. Veja o exemplo:
#include 
int sqr (const int *num);
main (void)
{
int a=10;
int b;
b=sqr (&a);
}
int sqr (const int *num)
{
return ((*num)*(*num));
}

No exemplo, num está protegido contra alterações. Isto quer dizer que, se tentássemos fazer
*num=10;
dentro da função sqr() o compilador daria uma mensagem de erro.

volatile

O modificador volatile diz ao compilador que a variável em questão pode ser alterada sem que este seja avisado. Isto evita "bugs" seríssimos. Digamos que, por exemplo, tenhamos uma variável que o BIOS do computador altera de minuto em minuto (um relógio por exemplo). Seria muito bom que declarássemos esta variável como sendo volatile.
extern float sum;
int RetornaCount (void)
{
return count;
}
Assim, o compilador irá saber que count e sum estão sendo usados no bloco mas que foram declarados em outro.

auto

O especificador de classe de armazenamento auto define variáveis automáticas, isto é, variáveis locais. Raramente usado pois todas as variáveis locais do C são auto por definição.

extern

O extern define variáveis que serão usadas em um arquivo apesar de terem sido declaradas em outro. Ao contrário dos programas até aqui vistos, podemos ter programas de vários milhares de linhas. Estes podem ser divididos em vários arquivos (módulos) que serão compilados separadamente. Digamos que para um programa grande tenhamos duas variáveis globais: um inteiro count e um float sum. Estas variáveis são declaradas normalmente em um dos módulos do programa. Por exemplo:
int count;
 
 
float sum;
 
 
main (void)
 
 
{
 
 
...
 
 
return 0;
 
 
}
Num outro módulo do programa temos uma rotina que deve usar as variáveis globais acima. Digamos que a rotina que queremos se chama RetornaCount() e retorna o valor atual de count. O problema é que este módulo será compilado em separado e não tomará conhecimento dos outros módulos. O que fazer? Será que funcionaria se fizermos assim:
int count;              /* errado */
 
 
float sum;
 
 
int RetornaCount (void)
 
 
{
 
 
return count;
 
 
}
Não. O módulo compilaria sem problema, mas, na hora que fizermos a linkagem (união dos módulos já compilados para gerar o executável) vamos nos deparar com uma mensagem de erro dizendo que as variáveis globais count e sum foram declaradas mais de uma vez. A maneira correta de se escrever o módulo com a função RetornaCount() é:
extern int count;       /* certo */
 
 
extern float sum;
 
 
int RetornaCount (void)
 
 
{
 
 
return count;
 
 
}
Assim, o compilador irá saber que count e sum estão sendo usados no bloco mas que foram declarados em outro.

 

static

O funcionamento das variáveis declaradas como static depende se estas são globais ou locais.
Variáveis globais static funcionam como variáveis globais dentro de um módulo, ou seja, são variáveis globais que não são (e nem podem ser) conhecidas em outros modulos. Isto é util se quisermos isolar pedaços de um programa para evitar mudanças acidentais em variáveis globais.
Variáveis locais static são variáveis cujo valor é mantido de uma chamada da função para a outra. Veja o exemplo:
int count (void)
{
static int num=0;
num++;
return num;
}
A função count() retorna o número de vezes que ela já foi chamada. Veja que a variável local int é inicializada. Esta inicialização só vale para a primeira vez que a função é chamada pois num deve manter o seu valor de uma chamada para a outra. O que a função faz é incrementar num a cada chamada e retornar o seu valor. A melhor maneira de se entender esta variável local static é implementando. Veja por si mesmo, executando seu próprio programa que use este conceito.

register

O computador tem a memória principal e os registradores da CPU. As variáveis (assim como o programa como um todo) são armazenados na memória. O modificador register diz ao compilador que a variável em questão deve ser, se possível, usada em um registrador da CPU.
Vamos agora ressaltar vários pontos importantes. Em primeiro lugar, porque usar o register? Variáveis nos registradores da CPU vão ser acessadas em um tempo muito menor pois os registradores são muito mais rápidos que a memória. Em segundo lugar, em que tipo de variável usar o register? O register não pode ser usado em variáveis globais. Isto implicaria que um registrador da CPU ficaria o tempo todo ocupado por conta de uma variável. Os tipos de dados onde é mais aconselhado o uso do register são os tipos char e int, mas pode-se usá-lo em qualquer tipo de dado. Em terceiro lugar, o register é um pedido que o programador faz ao compilador. Este não precisa ser atendido necessariamente.
Um exemplo do uso do register é dado:
main (void)
{
register int count;
for (count=0;count<10;count++)
        {
        ...
        }
return 0;
}
O loop for acima será executado mais rapidamente do que seria se não usássemos o register. Este é o uso mais recomendável para o register: uma variável que será usada muitas vezes em seguida.  

Conversão de Tipos

Em atribuições no C temos o seguinte formato:
destino=orígem;
Se o destino e a orígem são de tipos diferentes o compilador faz uma conversão entre os tipos. Nem todas as conversões são possíveis. O primeiro ponto a ser ressaltado é que o valor de origem é convertido para o valor de destino antes de ser atribuído e não o contrário.
É importante lembrar que quando convertemos um tipo numérico para outro nós nunca ganhamos precisão. Nós podemos perder precisão ou no máximo manter a precisão anterior. Isto pode ser entendido de uma outra forma. Quando convertemos um número não estamos introduzindo no sistema nenhuma informação adicional. Isto implica que nunca vamos ganhar precisão.
Abaixo vemos uma tabela de conversões numéricas com perda de precisão, para um compilador com palavra de 16 bits:
De
Para
Informação Perdida
unsigned char
char
Valores maiores que 127 são alterados
short int
char
Os 8 bits de mais alta ordem
int
char
Os 8 bits de mais alta ordem
long int
char
Os 24 bits de mais alta ordem
long int
short int
Os 16 bits de mais alta ordem
long int
int
Os 16 bits de mais alta ordem
float
int
Precisão - resultado arredondado
double
float
Precisão - resultado arredondado
long double
double
Precisão - resultado arredondado



Modificadores de Funções

A forma geral de uma função é, como já foi visto,
tipo_de_retorno nome_da_função (declaração_de_parâmetros)
{
corpo_da_função
}
Uma função pode aceitar um modificador de tipo. Este vai modificar o modo como a função opera na passagem de parâmetros. A forma geral da função ficaria então:
modificador_de_tipo tipo_de_retorno nome_da_função (declaração_de_parâmetros)
{
corpo_da_função
}
O nosso curso não aborda detalhes do funcionamento interno de funções. Para saber mais, consulte o manual do seu compilador ou algum livro especializado.

pascal

Faz com que a função use a convenção de funções da linguagem de programação Pascal. Isto faz com que as funções sejam compatíveis com programas em Pascal.

cdecl

O modificador de tipo cdecl faz com que a função use a convenção para funções do C. Raramente é usado pois é o default. Pode-se pensar no cdecl como sendo o "inverso" do pascal.

interrupt

Diz ao compilador que a função em questão será usada como um manipulador de interrupções. Isto faz com que o compilador preserve os registradores da CPU antes e depois da chamada à função. Mais uma vez este tópico está fora do escopo do curso.

Ponteiros para Funções

O C permite que acessemos variáveis e funções através de ponteiros! Podemos então fazer coisas como, por exemplo, passar uma função como argumento para outra função. Um ponteiro para uma função tem a seguinte declaração:
tipo_de_retorno (*nome_do_ponteiro)();
ou
tipo_de_retorno (*nome_do_ponteiro)(declaração_de_parâmetros);

Repare nos parênteses que devem ser colocados obrigatoriamente. Se declaramos:
tipo_de_retorno * nome(declaração_de_parâmetros);
Estaríamos, na realidade, declarando uma função que retornaria um ponteiro para o tipo especificado.
Porém, não é obrigatório se declarar os parâmetros da função. Veja um exemplo do uso de ponteiros para funções:
#include 
#include 
void PrintString (char *str, int (*func)(const char *));
main (void)
{
      char String [20]="Curso de C.";
      int (*p)(const char *); /* Declaracao do ponteiro para função 
                        Funcao apontada e' inteira e recebe como parametro
                        uma string constante */
      p=puts;           /* O ponteiro p passa a apontar para a função puts 
                     que tem o seguinte prototipo: int puts(const char *) */
      PrintString (String, p); 
/* O ponteiro é passado como parametro para PrintString */
      return 0;
}
void PrintString (char *str, int (*func)(const char *))
{
      (*func)(str);/* chamada a função através do ponteiro para função */
      func(str);/* maneira também válida de se fazer a chamada a função puts
               através do ponteiro para função func  */
}
Veja que fizemos a atribuição de puts a p simplesmente usando:
p = puts;
Disto, concluímos que o nome de uma função (sem os parênteses) é, na realidade, o endereço daquela função! Note, também, as duas formas alternativas de se chamar uma função através de um ponteiro. No programa acima, fizemos esta chamada por:
(*func)(str);
e
func(str);
Estas formas são equivalentes entre si.
Além disto, no programa, a função PrintString() usa uma função qualquer func para imprimir a string na tela. O programador pode então fornecer não só a string mas também a função que será usada para imprimí-la. No main() vemos como podemos atribuir, ao ponteiro para funções p, o endereço da função puts() do C.
Em síntese, ao declarar um ponteiro para função, podemos atribuir a este ponteiro o endereço de uma função e podemos também chamar a função apontada através dele. Não podemos fazer algumas coisas que fazíamos com ponteiros "normais", como, por exemplo, incrementar ou decrementar um ponteiro para função.

Alocação Dinâmica

A alocação dinâmica permite ao programador  alocar memória para variáveis quando o programa está sendo executado. Assim, poderemos definir, por exemplo, um vetor ou uma matriz cujo tamanho descobriremos em tempo de execução. O padrão C ANSI define apenas 4 funções para o sistema de alocação dinâmica, disponíveis na biblioteca stdlib.h:
No entanto, existem diversas outras funções que são amplamente utilizadas, mas dependentes do ambiente e compilador. Neste curso serão abordadas somente estas funções padronizadas.

malloc

A função malloc() serve para alocar memória e tem o seguinte protótipo:
void *malloc (unsigned int num);
A funçao toma o número de bytes que queremos alocar (num), aloca na memória e retorna um ponteiro void * para o primeiro byte alocado. O ponteiro void * pode ser atribuído a qualquer tipo de ponteiro. Se não houver memória suficiente para alocar a memória requisitada a função malloc() retorna um ponteiro nulo. Veja um exemplo de alocação dinâmica com malloc():
  
#include 
#include      /* Para usar malloc() */
 
main (void)
 
{
 
      int *p;
      int a;
      int i;
 
... /* Determina o valor de a em algum lugar */
 
      p=(int *)malloc(a*sizeof(int));/* Aloca a números inteiros 
      p pode agora ser tratado como um vetor com a posicoes*/
      if (!p)
        {
            printf ("** Erro: Memoria Insuficiente **");
            exit;
        }
 
      for (i=0; i
/* p pode ser tratado como um vetor com a posicoes */
            p[i] = i*i;
      
...
 
      return 0;
}
No exemplo acima, é alocada memória suficiente para se armazenar a números inteiros. O operador sizeof() retorna o número de bytes de um inteiro. Ele é util para se saber o tamanho de tipos. O ponteiro void* que malloc() retorna é convertido para um int* pelo cast e é atribuído a p. A declaração seguinte testa se a operação foi bem sucedida. Se não tiver sido, p terá um valor nulo, o que fará com que !p retorne verdadeiro. Se a operação tiver sido bem sucedida, podemos usar o vetor de inteiros alocados normalmente, por exemplo, indexando-o de p[0] a p[(a-1)] calloc
A função calloc() também serve para alocar memória, mas possui um protótipo um pouco diferente:
void *calloc (unsigned int num, unsigned int size);
A funçao aloca uma quantidade de memória igual a num * size, isto é, aloca memória suficiente para um vetor de num objetos de tamanho size. Retorna um ponteiro void * para o primeiro byte alocado. O ponteiro void * pode ser atribuído a qualquer tipo de ponteiro. Se não houver memória suficiente para alocar a memória requisitada a função calloc() retorna um ponteiro nulo. Veja um exemplo de alocação dinâmica com calloc():
#include 
 
#include      /* Para usar calloc() */
 
main (void)
 
{
 
      int *p;
      int a;
      int i;
 
... /* Determina o valor de a em algum lugar */
 
      p=(int *)calloc(a,sizeof(int));
/* Aloca a números inteiros 
     p pode agora ser tratado como um vetor com a posicoes*/
      if (!p)
        {
            printf ("** Erro: Memoria Insuficiente **");
            exit;
        }
 
      for (i=0; i
/* p pode ser tratado como um vetor com a posicoes */
            p[i] = i*i;
      
...
 
      return 0;
}
No exemplo acima, é alocada memória suficiente para se colocar a números inteiros. O operador sizeof() retorna o número de bytes de um inteiro. Ele é util para se saber o tamanho de tipos. O ponteiro void * que calloc() retorna é convertido para um int * pelo cast e é atribuído a p. A declaração seguinte testa se a operação foi bem sucedida. Se não tiver sido, p terá um valor nulo, o que fará com que !p retorne verdadeiro. Se a operação tiver sido bem sucedida, podemos usar o vetor de inteiros alocados normalmente, por exemplo, indexando-o de p[0] a p[(a-1)] realloc
A função realloc() serve para realocar memória e tem o seguinte protótipo:
void *realloc (void *ptr, unsigned int num);
A funçao modifica o tamanho da memória previamente alocada apontada por *ptr para aquele especificado por num. O valor de num pode ser maior ou menor que o original. Um ponteiro para o bloco é devolvido porque realloc() pode precisar mover o bloco para aumentar seu tamanho. Se isso ocorrer, o conteúdo do bloco antigo é copiado no novo bloco, e nenhuma informação é perdida. Se ptr for nulo, aloca size bytes e devolve um ponteiro; se size é zero, a memória apontada por ptr é liberada. Se não houver memória suficiente para a alocação, um ponteiro nulo é devolvido e o bloco original é deixado inalterado.
#include 
#include      /* Para usar malloc()  e realloc*/
 
main (void)
 
{
 
      int *p;
      int a;
      int i;
 
... /* Determina o valor de a em algum lugar */
      a = 30;
 
      p=(int *)malloc(a*sizeof(int)); 
     /* Aloca a números inteiros 
      p pode agora ser tratado como um vetor com a posicoes */
      if (!p)
        {
            printf ("** Erro: Memoria Insuficiente **");
            exit;
        }
 
      for (i=0; i
     /* p pode ser tratado como um vetor com a posicoes */
            p[i] = i*i;
      
      /* O tamanho de p deve ser modificado, por algum motivo ... */
      a = 100;
      p = realloc (p, a*sizeof(int));
      for (i=0; i
     /* p pode ser tratado como um vetor com a posicoes */
            p[i] = a*i*(i-6);
...
 
      return 0;
}
free
Quando alocamos memória dinamicamente é necessário que nós a 
liberemos quando ela não for mais necessária. Para isto existe a função free() 
cujo protótipo é:
void free (void *p);
Basta então passar para free() o ponteiro que aponta para o início da memória
alocada. Mas você pode se perguntar: como é que o programa vai saber quantos bytes devem ser liberados? Ele sabe pois quando você alocou a memória, ele guardou o número de bytes alocados numa "tabela de alocação" interna. Vamos reescrever o exemplo usado para a função malloc() usando o free() também agora:
#include 
#include      /* Para usar malloc e free */
 
main (void)
{
      int *p;
      int a;
      
      ...
      
      p=(int *)malloc(a*sizeof(int));
 
      if (!p)
        {
              printf ("** Erro: Memoria Insuficiente **");
              exit;
        }
 
      ...
 
      free(p);
 
      ...
 
      return 0;
 
}

Alocação Dinâmica de Vetores e Matrizes

Alocação Dinâmica de Vetores

Alocação dinâmica de vetores utiliza os conceitos aprendidos na aula 

sobre ponteiros e as funções de alocação dinâmica apresentados.

Um exemplo de implementação para vetor real é fornecido a seguir:
#include 
#include 
 
float *Alocar_vetor_real (int n)
{
  float *v;        /* ponteiro para o vetor */
  if (n < 1) {  /* verifica parametros recebidos */
     printf ("** Erro: Parametro invalido **\n");
     return (NULL);
     }
  /* aloca o vetor */
  v = (float *) calloc (n, sizeof(float));
  if (v == NULL) {
     printf ("** Erro: Memoria Insuficiente **");
     return (NULL);
     }
  return (v);    /* retorna o ponteiro para o vetor */
}
 
float *Liberar_vetor_real (float *v)
{
  if (v == NULL) return (NULL);
  free(v);        /* libera o vetor */
  return (NULL);  /* retorna o ponteiro */
}
 
void main (void)
{
  float *p;
  int a;
  ...    /* outros comandos, inclusive a inicializacao de a */
  p = Alocar_vetor_real (a);
  ...    /* outros comandos, utilizando p[] normalmente */
  p = Liberar_vetor_real (p);
}

Alocação Dinâmica de Matrizes

A alocação dinâmica de memória para matrizes é realizada da mesma forma que para vetores, com a diferença que teremos um ponteiro apontando para outro ponteiro que aponta para o valor final, ou seja é um ponteiro para ponteiro, o que é denominado indireção múltipla. A indireção múltipla pode ser levada a qualquer dimensão desejada, mas raramente é necessário mais de um ponteiro para um ponteiro. Um exemplo de implementação para matriz real bidimensional é fornecido a seguir. A estrutura de dados utilizada neste exemplo é composta por um vetor de ponteiros (correspondendo ao primeiro índice da matriz), sendo que cada ponteiro aponta para o início de uma linha da matriz. Em cada linha existe um vetor alocado dinamicamente, como descrito anteriormente (compondo o segundo índice da matriz).
#include 
#include 
 
float **Alocar_matriz_real (int m, int n)
{
  float **v;  /* ponteiro para a matriz */
  int   i;    /* variavel auxiliar      */
  if (m < 1 || n < 1) { /* verifica parametros recebidos */
     printf ("** Erro: Parametro invalido **\n");
     return (NULL);
     }
  /* aloca as linhas da matriz */
  v = (float **) calloc (m, sizeof(float *)); 
/*Um vetor de m ponteiros para float */
  if (v == NULL) {
     printf ("** Erro: Memoria Insuficiente **");
     return (NULL);
     }
  /* aloca as colunas da matriz */
  for ( i = 0; i < m; i++ ) {
      v[i] = (float*) calloc (n, sizeof(float)); /* m vetores de n floats */
      if (v[i] == NULL) {
         printf ("** Erro: Memoria Insuficiente **");
         return (NULL);
         }
      }
  return (v); /* retorna o ponteiro para a matriz */
}
 
float **Liberar_matriz_real (int m, int n, float **v)
{
  int  i;  /* variavel auxiliar */
  if (v == NULL) return (NULL);
  if (m < 1 || n < 1) {  /* verifica parametros recebidos */
     printf ("** Erro: Parametro invalido **\n");
     return (v);
     }
  for (i=0; i
  free (v);      /* libera a matriz (vetor de ponteiros) */
  return (NULL); /* retorna um ponteiro nulo */
}
 
 
void main (void)
{
  float **mat;  /* matriz a ser alocada */
  int   l, c;   /* numero de linhas e colunas da matriz */
  int i, j;
  ...   /* outros comandos, inclusive inicializacao para l e c */
  mat = Alocar_matriz_real (l, c);
 
  for (i = 0; i < l; i++)
     for ( j = 0; j < c; j++)
      mat[i][j] = i+j;
  
  ...           /* outros comandos utilizando mat[][] normalmente */
  mat = Liberar_matriz_real (l, c, mat);
  ...
}

Aula 9 - Entradas e Saídas Padronizadas

Introdução
O sistema de entrada e saída da linguagem C está estruturado na forma de uma biblioteca de funções . Já vimos algumas destas funções, e agora elas serão reestudadas. Novas funções também serão apresentadas.
Não é objetivo deste curso explicar, em detalhes, todas as possíveis funções da biblioteca de entrada e saída do C. A sintaxe completa destas funções pode ser encontrada no manual do seu compilador. Alguns sistemas trazem um descrição das funções na ajuda do compilador, que pode ser acessada "on line". Isto pode ser feito, por exemplo, no Rhide.
Um ponto importante é que agora, quando apresentarmos uma função, vamos, em primeiro lugar, apresentar o seu protótipo. Você já deve ser capaz de interpretar as informações que um protótipo nos passa. Se não, deve voltar a estudar a aula sobre funções.
Outro aspecto importante, quando se discute a entrada e saída na linguagem C é o conceito de fluxo. Seja qual for o dispositivo de entrada e saída (discos, terminais, teclados, acionadores de fitas) que se estiver trabalhando, o C vai enxergá-lo como um fluxo, que nada mais é que um dispositivo lógico de entrada ou saída. Todos os fluxos são similares em seu funcionamento e independentes do dispositivo ao qual estão associados. Assim, as mesmas funções que descrevem o acesso aos discos podem ser utilizadas para se acessar um terminal de vídeo. Todas as operações de entrada e saída são realizadas por meio de fluxos.
Na linguagem C, um arquivo é entendido como um conceito que pode ser aplicado a arquivos em disco, terminais, modens, etc ... Um fluxo é associado a um arquivo através da realização de uma operação de abertura. Uma vez aberto, informações podem ser trocadas entre o arquivo e o programa. Um arquivo é dissociado de um fluxo através de uma operação de fechamento de arquivo.

Lendo e Escrevendo Caracteres

Uma das funções mais básicas de um sistema é a entrada e saída de informações em dispositivos. Estes podem ser um monitor, uma impressora ou um arquivo em disco. Vamos ver os principais comandos que o C nos fornece para isto.

- getche e getch

As funções getch() e getche() não são definidas pelo padrão ANSI. Porém, elas geralmente são incluídas em compiladores baseados no DOS, e se encontram no header file conio.h. Vale a pena repetir: são funções comuns apenas para compiladores baseados em DOS e, se você estiver no UNIX normalmente não terá estas funções disponíveis.

Protótipos:
                int getch (void);
                int getche (void);
getch() espera que o usuário digite uma tecla e retorna este caractere. Você pode estar estranhando o fato de getch() retornar um inteiro, mas não há problema pois este inteiro é tal que quando igualado a um char a conversão é feita corretamente. A função getche() funciona exatamente como getch(). A diferença é que getche() gera um "echo" na tela antes de retornar a tecla.
Se a tecla pressionada for um caractere especial estas funções retornam zero. Neste caso você deve usar as funções novamente para pegar o código da tecla extendida pressionada.
A função equivalente a getche() no mundo ANSI é o getchar(). O problema com getchar é que o caracter lido é colocado em uma área intermediária até que o usuário digite um , o que pode ser extremamente inconveniente em ambientes interativos.

- putchar

Protótipo:
                int putchar (int c);
putchar() coloca o caractere c na tela. Este caractere é colocado na posição atual do cursor. Mais uma vez os tipos são inteiros, mas você não precisa se preocupar com este fato. O header file é stdio.h.

Lendo e Escrevendo Strings

- gets

Protótipo:
                char *gets (char *s);
Pede ao usuário que entre uma string, que será armazenada na string s. O ponteiro que a função retorna é o próprio s. gets não é uma função segura. Por quê? Simplesmente porque com gets pode ocorrer um estouro da quantidade de posições que foi especificada na string . Veja o exemplo abaixo:
#include
int main()
{
    char buffer[10];
    printf("Entre com o seu nome");
    gets(buffer);
    printf("O nome é: %s", buffer);
    return 0;
}


Se o usuário digitar como entrada:
Renato Cardoso Mesquita
ou seja, digitar um total de 23 caracteres: 24 posições (incluindo o '\0' ) serão utilizadas para armazenar a string. Como a string buffer[] só tem 10 caracteres, os 14 caracteres adicionais serão colocados na área de memória subsequente à ocupada por ela, escrevendo uma região de memória que não está reservada à string. Este efeito é conhecido como "estouro de buffer" e pode causar problemas imprevisíveis. Uma forma de se evitar este problema é usar a função fgets, conforme veremos posteriormente 

- puts

Protótipo:
                int puts (char *s);
puts() coloca a string s na tela.
AUTO AVALIAÇÃO
Veja como você está. Escreva um programa que leia nomes pelo teclado e os imprima na tela. Use as funções puts e gets para a leitura e impressão na tela.

Entrada e Saída Formatada

As funções que resumem todas as funções de entrada e saída formatada no C são as funções printf() e scanf(). Um domínio destas funções é fundamental ao programador.

- printf

Protótipo:
                int printf (char *str,...);
As reticências no protótipo da função indicam que esta função tem um número de argumentos variável. Este número está diretamente relacionado com a string de controle str, que deve ser fornecida como primeiro argumento. A string de controle tem dois componentes. O primeiro são caracteres a serem impressos na tela. O segundo são os comandos de formato. Como já vimos, os últimos determinam uma exibição de variáveis na saída. Os comandos de formato são precedidos de %. A cada comando de formato deve corresponder um argumento na função printf(). Se isto não ocorrer podem acontecer erros imprevisíveis no programa.



Abaixo apresentamos a tabela de códigos de formato:
 
Código
Formato
%c
Um caracter (char)
%d
Um número inteiro decimal (int)
%i
O mesmo que %d
%e
Número em notação científica com o "e"minúsculo
%E
Número em notação científica com o "e"maiúsculo
%f
Ponto flutuante decimal
%g
Escolhe automaticamente o melhor entre %f e %e
%G
Escolhe automaticamente o melhor entre %f e %E
%o
Número octal
%s
String
%u
Decimal "unsigned" (sem sinal)
%x
Hexadecimal com letras minúsculas
%X
Hexadecimal com letras maiúsculas
%%
Imprime um %
%p
Ponteiro
Vamos ver alguns exemplos:
Código
Imprime
printf ("Um %%%c %s",'c',"char");
Um %c char
printf ("%X %f %e",107,49.67,49.67);
6B 49.67 4.967e1
printf ("%d %o",10,10);
10 12
É possível também indicar o tamanho do campo, justificação e o número de casas decimais. Para isto usa-se códigos colocados entre o % e a letra que indica o tipo de formato.
Um inteiro indica o tamanho mínimo, em caracteres, que deve ser reservado para a saída. Se colocarmos então %5d estamos indicando que o campo terá cinco caracteres de comprimento no mínimo. Se o inteiro precisar de mais de cinco caracteres para ser exibido então o campo terá o comprimento necessário para exibi-lo. Se o comprimento do inteiro for menor que cinco então o campo terá cinco de comprimento e será preenchido com espaços em branco. Se se quiser um preenchimento com zeros pode-se colocar um zero antes do número. Temos então que %05d reservará cinco casas para o número e se este for menor então se fará o preenchimento com zeros.
O alinhamento padrão é à direita. Para se alinhar um número à esquerda usa-se um sinal - antes do número de casas. Então %-5d será o nosso inteiro com o número mínimo de cinco casas, só que justificado a esquerda.

Pode-se indicar o número de casas decimais de um número de ponto flutuante. Por exemplo, a notação %10.4f indica um ponto flutuante de comprimento total dez e com 4 casas decimais. Entretanto, esta mesma notação, quando aplicada a tipos como inteiros e strings indica o número mínimo e máximo de casas. Então %5.8d é um inteiro com comprimento mínimo de cinco e máximo de oito.
Vamos ver alguns exemplos: 
Código
Imprime
printf ("%-5.2f",456.671);
| 456.67|
printf ("%5.2f",2.671);
| 2.67|
printf ("%-10s","Ola");
|Ola       |
Nos exemplos o "pipe" ( | ) indica o início e o fim do campo mas não são escritos na tela.

- scanf

Protótipo:
                int scanf (char *str,...);
A string de controle str determina, assim como com a função printf(), quantos parâmetros a função vai necessitar. Devemos sempre nos lembrar que a função scanf() deve receber ponteiros como parâmetros. Isto significa que as variáveis que não sejam por natureza ponteiros devem ser passadas precedidas do operador &. Os especificadores de formato de entrada são muito parecidos com os de printf(). Os caracteres de conversão d, i, u e x podem ser precedidos por h para indicarem que um apontador para short ao invés de int aparece na lista de argumento, ou pela letra l (letra ele) para indicar que que um apontador para long aparece na lista de argumento. Semelhantemente, os caracteres de conversão e, f e g podem ser precedidos por l para indicarem que um apontador para double ao invés de float está na lista de argumento.  Exemplos:
Código
Formato
%c
Um único caracter (char)
%d
Um número decimal (int)
%i
Um número inteiro
%hi
Um short int
%li
Um long int
%e
Um ponto flutuante
%f
Um ponto flutuante
%lf
Um double
%h
Inteiro curto
%o
Número octal
%s
String
%x
Número hexadecimal
%p
Ponteiro
- sprintf e sscanf
sprintf e sscanf são semelhantes a printf e scanf. Porém, ao invés de escreverem na saída padrão ou lerem da entrada padrão, escrevem ou leem em uma string. Os protótipos são:
int sprintf (char *destino, char *controle, ...);
int sscanf (char *destino, char *controle, ...);
Estas funções são muito utilizadas para fazer a conversão entre dados na forma numérica e sua representação na forma de strings. No programa abaixo, por exemplo, a variável i é "impressa" em string1. Além da representação de i como uma string, string1 também conterá "Valor de i=" .
#include 
int main()
{
    int i;
    char string1[20];
    printf( " Entre um valor inteiro: ");
    scanf("%d", &i);
    sprintf(string1,"Valor de i = %d", i);
    puts(string1);
    return 0;
}
Já no programa abaixo, foi utilizada a função sscanf para converter a informação armazenada em string1 em seu valor numérico:
#include 
int main()
{
    int i, j, k;
    char string1[]= "10 20 30";
    sscanf(string1, "%d %d %d", &i, &j, &k);
    printf("Valores lidos: %d, %d, %d", i, j, k);
    return 0;
}
AUTO AVALIAÇÃO
Veja como você está. Escreva um programa que leia (via teclado) e apresente uma matriz 3X3 na tela. Utilize os novos códigos de formato aprendidos para que a matriz se apresente corretamente identada. Altere os tipos de dados da matriz (int, float, double) e verifique a formatação correta para a identação. Verifique também a leitura e impressão de números hexadecimais.


 Abrindo e Fechando um Arquivo
O sistema de entrada e saída do ANSI C é composto por uma série de funções, cujos protótipos estão reunidos em stdio.h . Todas estas funções trabalham com o conceito de "ponteiro de arquivo". Este não é um tipo propriamente dito, mas uma definição usando o comando typedef. Esta definição também está no arquivo stdio.h. Podemos declarar um ponteiro de arquivo da seguinte maneira:
FILE *p;
p será então um ponteiro para um arquivo. É usando este tipo de ponteiro que vamos poder manipular arquivos no C.

- fopen

Esta é a função de abertura de arquivos. Seu protótipo é:
FILE *fopen (char *nome_do_arquivo,char *modo);
O nome_do_arquivo determina qual arquivo deverá ser aberto. Este nome deve ser válido no sistema operacional que estiver sendo utilizado. O modo de abertura diz à função fopen() que tipo de uso você vai fazer do arquivo. A tabela abaixo mostra os valores de modo válidos: 
Modo
Significado
"r"
Abre um arquivo texto para leitura. O arquivo deve existir antes de ser aberto.
"w"
Abrir um arquivo texto para gravação. Se o arquivo não existir, ele será criado. Se já existir, o conteúdo anterior será destruído.
"a"
Abrir um arquivo texto para gravação. Os dados serão adicionados no fim do arquivo ("append"), se ele já existir, ou um novo arquivo será criado, no caso de arquivo não existente anteriormente.
"rb"
Abre um arquivo binário para leitura. Igual ao modo "r" anterior, só que o arquivo é binário.
"wb"
Cria um arquivo binário para escrita, como no modo "w" anterior, só que o arquivo é binário.
"ab"
Acrescenta dados binários no fim do arquivo, como no modo "a" anterior, só que o arquivo é binário.
"r+"
Abre um arquivo texto para leitura e gravação. O arquivo deve existir e pode ser modificado.
"w+"
Cria um arquivo texto para leitura e gravação. Se o arquivo existir, o conteúdo anterior será destruído. Se não existir, será criado.
"a+"
Abre um arquivo texto para gravação e leitura. Os dados serão adicionados no fim do arquivo se ele já existir, ou um novo arquivo será criado, no caso de arquivo não existente anteriormente.
"r+b"
Abre um arquivo binário para leitura e escrita. O mesmo que "r+" acima, só que o arquivo é binário.
"w+b"
Cria um arquivo binário para leitura e escrita. O mesmo que "w+" acima, só que o arquivo é binário.
"a+b"
Acrescenta dados ou cria uma arquivo binário para leitura e escrita. O mesmo que "a+" acima, só que o arquivo é binário

Poderíamos então, para abrir um arquivo binário para escrita, escrever:
FILE *fp;         /* Declaração da estrutura
fp=fopen ("exemplo.bin","wb");  /* o arquivo se chama 
exemplo.bin e está localizado no diretório corrente */
if (!fp)
        printf ("Erro na abertura do arquivo.");
A condição !fp testa se o arquivo foi aberto com sucesso porque no caso de um erro a função fopen() retorna um ponteiro nullo (NULL). 
Uma vez aberto um arquivo, vamos poder ler ou escrever nele utilizando as funções que serão apresentadas nas próximas páginas.
Toda vez que estamos trabalhando com arquivos, há uma espécie de posição atual no arquivo. Esta  é a posição de onde será lido ou escrito o próximo caractere. Normalmente, num acesso sequencial a um arquivo, não temos que mexer nesta posição pois quando lemos um caractere a posição no arquivo é automaticamente atualizada. Num acesso randômico teremos que mexer nesta posição (ver fseek()).

- exit

Aqui abrimos um parênteses para explicar a função exit() cujo protótipo é:
void exit (int codigo_de_retorno);
Para utilizá-la deve-se colocar um include para o arquivo de cabeçalho stdlib.h. Esta função aborta a execução do programa. Pode ser chamada de qualquer ponto no programa e faz com que o programa termine e retorne, para o sistema operacional, o código_de_retorno. A convenção mais usada é que um programa retorne zero no caso de um término normal e retorne um número não nulo no caso de ter ocorrido um problema. A função exit() se torna importante em casos como alocação dinâmica e abertura de arquivos pois nestes casos, se o programa não conseguir a memória necessária ou abrir o arquivo, a melhor saída pode ser terminar a execução do programa. Poderíamos reescrever o exemplo da seção anterior usando agora o exit() para garantir que o programa não deixará de abrir o arquivo:
#include 
#include  /* Para a função exit() */
main (void)
{
FILE *fp;
...
fp=fopen ("exemplo.bin","wb");
if (!fp)
   {
    printf ("Erro na abertura do arquivo. Fim de programa.");
    exit (1);
   }
...
return 0;
}

- fclose

Quando acabamos de usar um arquivo que abrimos, devemos fechá-lo. Para tanto usa-se a função fclose():
int fclose (FILE *fp);
O ponteiro fp passado à função fclose() determina o arquivo a ser fechado. A função retorna zero no caso de sucesso.
Fechar um arquivo faz com que qualquer caracter que tenha permanecido no "buffer" associado ao fluxo de saída seja gravado. Mas, o que é este "buffer"? Quando você envia caracteres para serem gravados em um arquivo, estes caracteres são armazenados temporariamente em uma área de memória (o "buffer") em vez de serem escritos em disco imediatamente. Quando o "buffer" estiver cheio, seu conteúdo é escrito no disco de uma vez. A razão para se fazer isto tem a ver com a eficiência nas leituras e gravações de arquivos. Se, para cada caracter que fossemos gravar, tivéssemos que posicionar a cabeça de gravação em um ponto específico do disco, apenas para gravar aquele caracter, as gravações seriam muito lentas. Assim estas gravações só serão efetuadas quando houver um volume razoável de informações a serem gravadas ou quando o arquivo for fechado.
A função exit() fecha todos os arquivos que um programa tiver aberto.

Lendo e Escrevendo Caracteres em Arquivos

- putc

A função putc é a primeira função de escrita de arquivo que veremos. Seu protótipo é:
int putc (int ch,FILE *fp);
Escreve um caractere no arquivo.
O programa a seguir lê uma string do teclado e escreve-a, caractere por caractere em um arquivo em disco (o arquivo arquivo.txt, que será aberto no diretório corrente).
#include 
#include 
int main()
{
 FILE *fp;
 char string[100];
 int i;
fp = fopen("arquivo.txt","w");/* Arquivo ASCII, para escrita */
if(!fp)
{
    printf( "Erro na abertura do arquivo");
    exit(0);
}
printf("Entre com a string a ser gravada no arquivo:");
gets(string);
 for(i=0; string[i]; i++) putc(string[i], fp);
 /* Grava a string, caractere a caractere */
 fclose(fp);
return 0;
}
Depois de executar este programa, verifique o conteúdo do arquivo arquivo.txt (você pode usar qualquer editor de textos). Você verá que a string que você digitou está armazenada nele.

- getc

Retorna um caractere lido do arquivo. Protótipo:
                int getc (FILE *fp);

- feof

EOF ("End of file") indica o fim de um arquivo. Às vezes, é necessário verificar se um arquivo chegou ao fim. Para isto podemos usar a função feof(). Ela retorna não-zero se o arquivo chegou ao EOF, caso contrário retorna zero. Seu protótipo é:
int feof (FILE *fp);
Outra forma de se verificar se o final do arquivo foi atingido é comparar o caractere lido por getc com EOF. O programa a seguir abre um arquivo já existente e o lê, caracter por caracter, até que o final do arquivo seja atingido. Os caracteres lidos são apresentados na tela:
#include
#include
int main()
{
FILE *fp;
char c;
fp = fopen("arquivo.txt","r");/* Arquivo ASCII, para leitura */
if(!fp)
{
    printf( "Erro na abertura do arquivo");
    exit(0);
}
while((c = getc(fp) ) != EOF)/* Enquanto não chegar ao final do arquivo */

    printf("%c", c);         /* imprime o caracter lido */
fclose(fp);
return 0;
}


A seguir é apresentado um programa onde várias operações com arquivos são realizadas, usando as funções vistas nesta página. Primeiro o arquivo é aberto para a escrita, e imprime-se algo nele. Em seguida, o arquivo é fechado e novamente aberto para a leitura. Verifique o exemplo.
#include 
#include 
#include 
 
void main()
{
        FILE *p;
        char c, str[30], frase[80] = "Este e um arquivo chamado: ";
        int i;
        /* Le um nome para o arquivo a ser aberto: */
        printf("\n\n Entre com um nome para o arquivo:\n");
        gets(str);
 
        if (!(p = fopen(str,"w")))
  /* Caso ocorra algum erro na abertura do arquivo..*/
        { /* o programa aborta automaticamente */
          printf("Erro! Impossivel abrir o arquivo!\n");
          exit(1);
        }
        /* Se nao houve erro, imprime no arquivo e o fecha ...*/
        strcat(frase, str);
        for (i=0; frase[i]; i++)
         putc(frase[i],p);
        fclose(p);
 
        /* Abre novamente para  leitura  */
        p = fopen(str,"r");
        c = getc(p);    /* Le o primeiro caracter */
        while (!feof(p))/* Enquanto não se chegar no final do arquivo */
        {    
                printf("%c",c); /*   Imprime o caracter na tela */
                c = getc(p);    /* Le um novo caracter no arquivo */
        }
        fclose(p);      /* Fecha o arquivo */
}
Auto-Avaliação
Veja como você está: escreva um programa que abra um arquivo texto e conte o número de caracteres presentes nele. Imprima o número de caracteres na tela.

Outros Comandos de Acesso a Arquivos

- Arquivos pré-definidos

Quando se começa a execução de um programa, o sistema automaticamente abre alguns arquivos pré-definidos:
  • stdin: dispositivo de entrada padrão (geralmente o teclado)
  • stdout: dispositivo de saída padrão (geralmente o vídeo)
  • stderr: dispositivo de saída de erro padrão (geralmente o vídeo)
  • stdaux: dispositivo de saída auxiliar (em muitos sistemas, associado à porta serial)
  • stdprn : dispositivo de impressão padrão (em muitos sistemas, associado à porta paralela)
Cada uma destas constantes pode ser utilizada como um ponteiro para FILE, para acessar os periféricos associados a eles. Desta maneira, pode-se, por exemplo, usar:
ch =getc(stdin);
para efetuar a leitura de um caracter a partir do teclado, ou :          
                    putc(ch, stdout);
para imprimí-lo na tela.

- fgets

Para se ler uma string num arquivo podemos usar fgets() cujo protótipo é:
char *fgets (char *str, int tamanho,FILE *fp);
A função recebe 3 argumentos: a string a ser lida, o limite máximo de caracteres a serem lidos e o ponteiro para FILE, que está associado ao arquivo de onde a string será lida. A função lê a string até que um caracter de nova linha seja lido ou tamanho-1 caracteres tenham sido lidos. Se o caracter de nova linha ('\n') for lido, ele fará parte da string, o que não acontecia com gets. A string resultante sempre terminará com '\0' (por isto somente tamanho-1 caracteres, no máximo, serão lidos).
A função fgets é semelhante à função gets(), porém, além dela poder fazer a leitura a partir de um arquivo de dados  e incluir o caracter de nova linha na string, ela ainda especifica o tamanho máximo da string de entrada. Como vimos, a função gets não tinha este controle, o que poderia acarretar erros de "estouro de buffer".  Portanto, levando em conta que o ponteiro fp pode ser substituído por stdin, como vimos acima, uma alternativa ao uso de gets é usar a seguinte construção:
                fgets (str, tamanho, stdin);
onde str e' a string que se está lendo e tamanho deve ser igual ao tamanho alocado para a string subtraído de 1, por causa do '\0'.

- fputs

Protótipo:
                char *fputs (char *str,FILE *fp);
Escreve uma string num arquivo.  

- ferror e perror

Protótipo de ferror:
int ferror (FILE *fp);
A função retorna zero, se nenhum erro ocorreu e um número diferente de zero se algum erro ocorreu durante o acesso ao arquivo.
ferror() se torna muito útil quando queremos verificar se cada acesso a um arquivo teve sucesso, de modo que consigamos garantir a integridade dos nossos dados. Na maioria dos casos, se um arquivo pode ser aberto, ele pode ser lido ou gravado. Porém, existem situações em que isto não ocorre. Por exemplo, pode acabar o espaço em disco enquanto gravamos, ou o disco pode estar com problemas e não conseguimos ler, etc.
            Uma função que pode ser usada em conjunto com ferror() é a função perror() (print error), cujo argumento é uma string que normalmente indica em que parte do programa o problema ocorreu.
            No exemplo a seguir, fazemos uso de ferror, perror e fputs
#include
#include
int main()
{
    FILE *pf;
    char string[100];
    if((pf = fopen("arquivo.txt","w")) ==NULL)
    {
        printf("\nNao consigo abrir o arquivo ! ");
        exit(1);
    }
    do
    {
        printf("\nDigite uma nova string. Para terminar, digite : ");
        gets(string);
        fputs(string, pf);
        putc('\n', pf);
        if(ferror(pf))
        {
            perror("Erro na gravacao");
            fclose(pf);
            exit(1);
        }
    } while (strlen(string) > 0);
    fclose(pf);
}

- fread

Podemos escrever e ler blocos de dados. Para tanto, temos as funções fread() e fwrite(). O protótipo de fread() é:
unsigned fread (void *buffer, int numero_de_bytes, int count, FILE *fp);
O buffer é a região de memória na qual serão armazenados os dados lidos. O número de bytes é o tamanho da unidade a ser lida. count indica quantas unidades devem ser lidas. Isto significa que o número total de bytes lidos é:
numero_de_bytes*count
A função retorna o número de unidades efetivamente lidas. Este número pode ser menor que count quando o fim do arquivo for encontrado ou ocorrer algum erro.
Quando o arquivo for aberto para dados binários, fread pode ler qualquer tipo de dados.

- fwrite

A função fwrite() funciona como a sua companheira fread(), porém escrevendo no arquivo. Seu protótipo é:
unsigned fwrite(void *buffer,int numero_de_bytes,int count,FILE *fp);
A função retorna o número de itens escritos. Este valor será igual a count a menos que ocorra algum erro.
O exemplo abaixo ilustra o uso de fwrite e fread para gravar e posteriormente ler uma variável float em um arquivo binário.
#include
#include
int main()
{
FILE *pf;
float pi = 3.1415;
float pilido;
if((pf = fopen("arquivo.bin", "wb")) == NULL) /* Abre arquivo binário para escrita */
{
    printf("Erro na abertura do arquivo");
    exit(1);
}
if(fwrite(&pi, sizeof(float), 1,pf) != 1)     /* Escreve a variável pi */
    printf("Erro na escrita do arquivo");
fclose(pf);                                  /* Fecha o arquivo */
if((pf = fopen("arquivo.bin", "rb")) == NULL)/* Abre o arquivo novamente para leitura */
{
    printf("Erro na abertura do arquivo");
    exit(1);
}


if(fread(&pilido, sizeof(float), 1,pf) != 1)/* Le em pilido o valor da variável armazenada anteriormente */
    printf("Erro na leitura do arquivo");
printf("\nO valor de PI, lido do arquivo e': %f", pilido);
fclose(pf);
return(0);
}

Note-se o uso do operador sizeof, que retorna o tamanho em bytes da variável ou do tipo de dados.

- fseek

Para se fazer procuras e acessos randômicos em arquivos usa-se a função fseek(). Esta move a posição corrente de leitura ou escrita no arquivo de um valor especificado, a partir de um ponto especificado. Seu protótipo é:
int fseek (FILE *fp,long numbytes,int origem);
O parâmetro origem determina a partir de onde os numbytes de movimentação serão contados. Os valores possíveis são definidos por macros em stdio.h e são:
Nome
Valor
Significado
SEEK_SET
0
Início do arquivo
SEEK_CUR
1
Ponto corrente no arquivo
SEEK_END
2
Fim do arquivo
Tendo-se definido a partir de onde irá se contar, numbytes determina quantos bytes de deslocamento serão dados na posição atual.  

- rewind

A função rewind() de protótipo
void rewind (FILE *fp);
retorna a posição corrente do arquivo para o início.

- remove

Protótipo:
                int remove (char *nome_do_arquivo);
Apaga um arquivo especificado.
O exercício da página anterior poderia ser reescrito usando-se, por exemplo, fgets() e fputs(), ou fwrite() e fread(). A seguir apresentamos uma segunda versão que se usa das funções fgets() e fputs(), e que acrescenta algumas inovações.
 
#include 
#include 
#include 
 
 
int main()
{
      FILE *p;
      char str[30], frase[] = "Este e um arquivo chamado: ", resposta[80];
      int i;
 
      /* Le um nome para o arquivo a ser aberto: */
      printf("\n\n Entre com um nome para o arquivo:\n");
      fgets(str,29,stdin);                             /* Usa fgets como se fosse gets */
      for(i=0; str[i]; i++) if(str[i]=='\n') str[i]=0; /* Elimina o \n da string lida */
      if (!(p = fopen(str,"w")))    /* Caso ocorra algum erro na abertura do arquivo..*/ 
      {                            /* o programa aborta automaticamente */
            printf("Erro! Impossivel abrir o arquivo!\n");
            exit(1);
      }
      /* Se nao houve erro, imprime no arquivo, e o fecha ...*/
      fputs(frase, p);
      fputs(str,p);
      fclose(p);
 
      /* abre novamente e le */
      p = fopen(str,"r");
      fgets(resposta, 79, p);
      printf("\n\n%s\n", resposta);
      fclose(p);              /* Fecha o arquivo */
      remove(str);            /* Apaga o arquivo */
      return(0);
}

Fluxos Padrão

Os fluxos padrão em arquivos permitem ao programador ler e escrever em arquivos da maneira padrão com a qual o  já líamos e escrevíamos na tela.

- fprintf

A função fprintf() funciona como a função printf(). A diferença é que a saída de fprintf() é um arquivo e não a tela do computador. Protótipo:
               
int fprintf (FILE *fp,char *str,...);
Como já poderíamos esperar, a única diferença do protótipo de fprintf() para o de printf() é a especificação do arquivo destino através do ponteiro de arquivo.

- fscanf

A função fscanf() funciona como a função scanf(). A diferença é que fscanf() lê de um arquivo e não do teclado do computador. Protótipo:
int fscanf (FILE *fp,char *str,...);
Como já poderíamos esperar, a única diferença do protótipo de fscanf() para o de scanf() é a especificação do arquivo destino através do ponteiro de arquivo.
Talvez a forma mais simples de escrever o programa da página 97 seja usando fprintf () e fscanf(). Fica assim:
#include 
#include 
int main()
{
        FILE *p;
        char str[80],c;
 
        /* Le um nome para o arquivo a ser aberto: */
        printf("\n\n Entre com um nome para o arquivo:\n");
        gets(str);
 
        if (!(p = fopen(str,"w")))  /* Caso ocorra algum erro na abertura do arquivo..*/ 
        {                           /* o programa aborta automaticamente */
                printf("Erro! Impossivel abrir o arquivo!\n");
                exit(1);
        }
        /* Se nao houve erro, imprime no arquivo, fecha ...*/
        fprintf(p,"Este e um arquivo chamado:\n%s\n", str);
        fclose(p);
 
        /* abre novamente para a leitura  */
        p = fopen(str,"r");
        while (!feof(p))
        {
                fscanf(p,"%c",&c);
                printf("%c",c);
        } 
        fclose(p);
        return(0);
}
AUTO AVALIAÇÃO
Veja como você está. Escreva um programa que leia uma lista de nomes e idades de um arquivo texto. Prepare um arquivo para ser lido com nomes e idades. Apresente os dados lidos em forma de tabela na tela. Use as funções de sua preferência, mas faça pelo menos duas versões do programa usando funções de leitura diferentes.