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.