segunda-feira, 28 de janeiro de 2013

Ponteiros


               Apesar de ponteiros serem mais uma característica da linguagem C, é importante separar um texto para o mesmo, o ponteiro é uma das mais uteis ferramentas que a linguagem apresenta, apesar disso, é o ponto mais criticado, devido aos casos em que sua aplicação incorreta gera erros graves, seu uso inclusive, passou a ser abolido das linguagens modernas que usam orientação de objetos, apesar do suporte de C++ para ponteiros, um programa não é considerado OO (Orientado a Objetos) se usa algum ponteiro.

Uma das limitações que existiam para quem desenvolvia estrutura de dados em C era o argumento de entrada, quando se define um tipo de dado abstrato, a primeira entrada configurável é o argumento que representa a informação que será utilizada, observando que C possui quatro tipos aritméticos básicos como char, int, float e double, ficariam restritos a uma escolha destes com variações de especificadores, com tal limite sempre que fosse criado um vetor, pilha, fila ou lista, seria declarada a variável de forma estática e imutável, este tipo de solução não deve se considerada como prejudicial, está se relevando uma otimização de tempo e espaço que somente deverá ser considerado quando algum destes dois problemas surgir.

Com o uso dos ponteiros é possível apresentar uma alternativa para criar algoritmos de uso geral em C, fugindo do modelo monolítico ao usar uma biblioteca de funções genéricas a vantagem fica evidente quando ao final as rotinas servirem para qualquer dado que o programador criar.

Uma breve lista com os tipos de C.

Básicos: O grupo consiste nas variações dos quatro tipos aritméticos (char, int, float e double) com os especificadores opcionais (signed, unsigned, short e long).

char alligator;
signed char bufallo;
unsigned char camel;
short deer;
short int eagle;
signed short flamingo;
signed short int gorilla;
unsigned short hyena;
unsigned short int iguana;
int jaguar;
signed int cheetah;
unsigned koala;
unsigned int ladybug;
long manatee;
long int narwhal;
signed long octopus;
signed long int penguim;
unsigned long quoll;
unsigned long int reindeer;
long long shark;
long long int turkey;
signed long long urchin;
signed long long int vulture;
unsigned long long warthog;
unsigned long long int xenops;
float yak;
double zebra;
long double unicorn;

Estruturas: formato que permite armazenar várias partes de uma informação dentro de uma variável.
 struct schedule {
       char event[40];
       int day;
       int month;
       int year;
};

Vetores: consiste numa coleção de valores de mesmo tipo armazenados continuamente numa posição da memoria.
int dogs[20];

Ponteiros: para cada tipo T, existe um ponteiro equivalente, variáveis podem ser declaradas como ponteiros ao preceder o nome com um asterisco.
char *rat;
int *mouse;

Uniões:  funciona semelhante às estruturas, com a característica especial de compartilhar a mesma memoria para diferentes descrições de tipos, assim é possível ler.
union {
       char byte[2];
       int word;
} reg;


FUNDAMENTOS

Iremos regredir um pouco, quando um programador desenvolve com assembler,  o valor que deseja manipular é um endereço na memória, portanto se desejo fazer uma soma, uso o valor armazenado de um byte na posição 156 e o faço a adição com o valor armazenado na posição 270, ou seja, usamos valores empíricos,  esse foi uns dos muitos problemas encontrados por Dennis Ritchie ao fazer a portabilidade de seu SO(Sistema Operacional), a necessidade de se preocupar com o endereço seriam suprimidos com o uso de uma linguagem de alto nível, ao criar uma variável em C, joga-se toda essa resolução de endereços e valores para o compilador, claro que uma péssima codificação pode declarar variáveis globais e permitir assim que apenas se atualize o problema de Assembler para C, ou seja, usar globais é um ato que conforme o programa aumente, se torna impossível atualizar, expandir ou mesmo manutenção, já virou referencia anedótica os comentários de Dijkstra sobre o uso do COBOL numa época em que este ainda não suportava variáveis locais e os danos mentais que causa tal pratica.

Programação estruturada é uma boa pratica e deve ser cultivada, um dos pontos fundamentais para isso, consiste no uso de sub-rotinas, se necessário mudar uma variável com o uso de uma sub-rotina, basta indicar a posição desta na memoria, a partir deste ponto fica visível à utilidade dos ponteiros e o porquê de seu uso em rotinas genéricas.

Deve-se ter sempre em mente que o ponteiro é simplesmente uma variável que armazena o endereço onde reside uma porção de dado na memoria, ao invés de armazenar a próprio dado, isto é, ponteiros  têm endereços de memoria. Abaixo uma tabela exemplifica o funcionamento.

       int a;
       int *iptr;
       int *jptr;
       int *kptr;

Endereço
Variável
Conteúdo
0x100
a
0
0x102
iptr
?
0x104
jptr
?
0x106
kptr
?


       iptr = &a;

Endereço
Variável
Conteúdo
0x100
a
0
0x102
iptr
0x100
0x104
jptr
?
0x106
kptr
?


       jptr = iptr;

Endereço
Variável
Conteúdo
0x100
a
0
0x102
iptr
0x100
0x104
jptr
0x100
0x106
kptr
?


       *jptr = 234;

Endereço
Variável
Conteúdo
0x100
a
234
0x102
iptr
0x100
0x104
jptr
0x100
0x106
kptr
?


       kptr = NULL;

Endereço
Variável
Conteúdo
0x100
a
0
0x102
iptr
0x100
0x104
jptr
0x100
0x106
kptr
NULL


ALOCAÇÃO

Alocar uma área da memoria para dados é feito de duas maneiras, declaração de uma variável para o ponteiro, ou através de alocação dinâmica, em C quando reservamos de maneira dinâmica, usamos um ponteiro para algum armazenamento no heap (um largo bloco de memória reservado para alocação dinâmica) através de funções como malloc ou realloc, momento em que passa ser responsabilidade do programa gerenciar esse bloco, até que seja decidido liberar a mesma, como mostrado no exemplo a seguir.

#include <stdlib.h>

int create(int **iptr)
{
      *iptr = malloc(sizeof(int));

      if (*iptr == NULL)
            return -1;

      return 0;
}

void destroy(int **iptr)
{
      if (*iptr != NULL)
            free(*iptr);
}

void main(void)
{
      int *jptr;

      if (create(&jptr) != -1) {
            *jptr = 100;
            destroy(&jptr);
      }
}

Seguindo a sequencia de operações teremos a seguinte ação:
create(&jptr)

Endereço
Variável
Conteúdo
0x104
jptr
?
0x200
iptr
0x104

*iptr = malloc(sizeof(int))

Endereço
Variável
Conteúdo
0x104
jptr
0x500
0x200
iptr
0x104
0x500

?

*jptr = 100

Endereço
Variável
Conteúdo
0x104
jptr
0x500
0x200
iptr
0x104
0x500

100

Ponteiros e alocação de memoria são as áreas que fornecem as principais criticas em C, o mau uso de reservar espaço sem a devida liberação do recurso causa vazamentos na memória(memory leak),  com o correto controle, tem-se um poderoso recurso ao controlar blocos de memória com dado sem especificar o tipo, caminha-se assim para o objetivo de uma operação genérica.


ARITMETICA

Um ponto relevante no acesso aos dados através de ponteiros é compreender a sua aritmética,  o incremento de uma posição na memória ou decremento é em cima do tamanho do tipo, uma posição equivale a tamanho em bytes do tipos.

      int i;
      char *pc;
      short int *pi;
      float *pf;
      double *pd;

      pd = pf = pi = pc = &i;

      pc++; pi++; pf++; pd++;

O programa acima gera(caso o compilador ignore os avisos de tipos incompatíveis) o seguinte resultado:

Endereço





0x100
&i




0x101

pc



0x102


pi


0x103





0x104



pf

0x105





0x106





0x107





0x108




pd

Para efeitos de comparação, um vetor trabalha igual ao ponteiro, assim quando definimos um acesso a[i], temos o equivalente *(a + i), uma vez que internamente a linguagem C converte o vetor em um ponteiro o que permite declarações como mostrado a seguir.

void fv (void)
{
      int a[10], *iptr;

      iptr = &a;
      iptr[0] = 12;
}
void fp (void)
{
      int a[10], *iptr;

      iptr = &a;
      *iptr = 12;
}


PONTEIROS GENERICOS

Ponteiros void (void *) apontam para objetos de tipo não especificados   , e podem assim serem usados como apontadores de dados genéricos, uma vez que o tamanho ou tipo do objeto apontado não é conhecido,  por tal característica, não é possível aplicar aritmética de ponteiros  ou passar valores por referencia, mas podem ser facilmente convertidos para qualquer formato ao qual seja necessária a operação, com a ajuda da ferramenta de molde (cast). A seguir reapresentamos um programa usando este tipo de ponteiro.


int create(void **gptr, int size)
{
      *gptr = malloc(size);

      if (*gptr == NULL)
            return -1;

      return 0;
}

void destroy(void **gptr)
{
      if (*gptr != NULL)
            free(*gptr);
}
  
void main(void)
{
      void *gptr;
      int *iptr;

      if (create(&gptr, sizeof(int)) != -1) {
            iptr = (int *) gptr;
            *iptr = 100;
            destroy(&gptr);
      }
}

A ideia apresentada nesta alteração é de que agora rotina create irá funcionar para qualquer tipo, possibilitando o seu uso em diversas situações.


PONTEIROS DE FUNÇÃO

Ponteiros de função são ponteiros que ao invés de apontar para os dados, apontam para códigos executáveis ou blocos de informações necessários para que se invoquem códigos executáveis, usados para armazenar e gerenciar funções como peças de dados, a sintaxe para declarar um ponteiro de função é semelhante a declarar uma função, o detalhe fica no momento em que se escreve o nome da função deverá usar (*)  da seguinte maneira, void (*pf) (int), o código abaixo serve como exemplo.

#include <stdio.h>

void func(int i)
{
    printf( "%i\n", i);
}

void main()
{
    void (*pf)(int);
    pf = &func;

    pf( 2 );
      (*pf)( 4 );
}

Maior uso de ponteiros para função são para encapsular as mesmas em estruturas de dados aumentando o escopo das implementações genéricas.