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);
  ...
}