Ponteiros

Os ponteiros ou pointers no inglês, são variáveis que guardam endereços de memória.

Lembra do & antes da variável no scanf?

int n;
scanf("%i", &n);

Este & indica um endereço de memória da variável n, e para guardar este endereço em uma variável, é necessário que agente crie um ponteiro:

int n = 9;
int * ponteiro_n = &n;

Este * antes do nome da variável diz para o C que esta variável vai guardar endereços de memória... “Mas porque colocar um tipo se a variável vai guardar só endereços? Por acaso endereço tem tipo? ... Não é bem assim, temos que dar um tipo ao ponteiro porque ele também tem tamanho, e para que consiga armazenar o endereço de uma variável ele tem que ter o mesmo tamanho.

“Tá, mas ainda não entendi a utilidade desse negócio!” , acho que essa mentalidade vai mudar assim que você descobrir que um array é um ponteiro que aloca vários espaços na memória.

Existem algumas regras sobre o uso de ponteiros, por exemplo:

int i = 90;

int * p = &i; // o ponteiro "p" agora aponta para a variável "i"

printf("%i\n", *p);

Quando damos o endereço de memória de uma variável para um ponteiro, nós dizemos que esse ponteiro aponta para essa variável

No exemplo acima, declaramos um ponteiro p apontando para i, agora nós podemos ter acesso ao valor de i apenas adicionando um * antes do p, e com isso conseguimos exibir o valor de i na tela com o printf.

E usando esse * também podemos alterar o valor de i:

int i = 90;

int * p = &i;
*p = 89;

Só que nós estamos alterando o valor diretamente na memória, e uma prova disso é que se você incrementar o ponteiro p, terá acesso a outro endereço de memória.

int i = 90;
int * p = &i;

p ++;

*p = 89;

Quando você executar o código acima irá ocorrer um erro de segmentação (quando o programa tenta acessar uma memória que não pertence a ele) ou se não ocorrer erro, quer dizer que ele acessou um espaço de memória desconhecido, e quando este ultimo ocorre o valor que está nesse espaço é um lixo do sistema ou o local onde está alocada outra variável.

int a[] = { 2, 4, 5, 6};

printf("%i\n", a[1]); // 4

a ++;
printf("%i\n", * a);  // 4

Como um array é um ponteiro, nós podemos usar o array como um ponteiro, “Mas por que você incrementou o a antes de exibi-lo?” , porque se eu usá-se o endereço original, o valor exibido seria o 2, pois o endereço de memória sempre se refere ao primeiro valor.

Mas o método que usei anteriormente não é muito adequado já que uma vez que você incremente o array ele estará apontando para outra posição e assim você tem que decrementar toda vez, o que não é prático, então, a forma mais indicada para isso seria:

int a[] = { 2, 4, 5, 6};

printf("%i\n", a[1]);     // 4

printf("%i\n", *(a+1) );  // 4

Desta forma o valor de a não será alterado.

Isso também serve para atribuir valor aos itens de um array.

int a[] = { 2, 4, 5, 6};

a[1] = 90;

*(a+2) = 56

Notem que o índice ( o valor entre [ e ] ), é somado a a, isso acontece porque um array cria uma fila de espaços do mesmo tipo, uma do lado da outra, por isso *(a+3) é o mesmo que a[3].

Alocação dinâmica (arrays dinâmicos)

Em alguns casos, precisamos de mais espaço do que a variável comum para guardar dados, e para esses casos geralmente usamos arrays, mas e se durante a execução eu necessite de um array maior... “É só criar um array maior e usar ele para a manipulação do novos dados!” ... Isso pode até funcionar, mas não é recomendável, pois seria um desperdício de memória.

Para resolver isso nós podemos alocar a quantidade de memória que queremos (em bytes) e usar um ponteiro com este endereço de memória, e se quisermos um espaço maior, é só realocar a memória deste ponteiro, assim nós poderemos aumentar e diminuir o tamanho do array.

E como prometi no capítulo sobre strings... Esta é a terceira forma de atribuir uma string:

char * str;
str = "string";

Isto só funciona com strings, arrays de outros tipos tem que ser atribuídos item a item.

O próprio exemplo da atribuição de uma string é um exemplo de alocação dinâmica, mas ela é feita automaticamente:

char * str;          // aqui temos um ponteiro vazio.
str = "coisa";       /* 
                        aqui nós alocamos 6 bytes na memória para
                        guardar { 'c', 'o', 'i', 's', 'a', '\0' }
                     */

printf("%s\n", str);

str = "outra coisa"; /*
                        aqui nós realocamos o espaço de 6 bytes
                        para 12 bytes e assim podemos guardar 
                        { 
                           'o', 'u', 't', 'r', 'a', ' ',
                           'c', 'o', 'i', 's', 'a'
                        }
                     */

printf("%s\n", str);

Se fossemos fazer o código acima usando puramente ponteiros, nós faríamos assim:

Lembrando que ao alocar espaços e referenciando com ponteiros, nós estamos criando arrays.

Antes de mais nada você tem que incluir o stdlib.h no seu arquivo (para evitar erros, sempre faça qualquer include no inicio do arquivo)

#include <stdlib.h>           // biblioteca necessária para usar as funções de alocação.

Para garantir inclua esta biblioteca em todos os exemplos a partir daqui.

Agora sim, podemos continuar...


char * str;                   // aqui temos um ponteiro vazio.

str = malloc (6);             // aqui nós alocamos 6 bytes na memória.

// guardando dados...
*( str + 0 ) = 'c';   // str[0] = 'c';
*( str + 1 ) = 'o';   // str[1] = 'o';
*( str + 2 ) = 'i';   // str[2] = 'i';
*( str + 3 ) = 's';   // str[3] = 's';
*( str + 4 ) = 'a';   // str[4] = 'a';
*( str + 5 ) = '\0';  // str[5] = '\0';

printf( str );
putchar('\n');

str = realloc (str, 12);        // aqui nós realocamos o espaço de 6 bytes para 12 bytes

// guardando dados...
str = "outra coisa";
str[12] = '\0';

printf("%s\n", str);

free( str );           /*
                           essa linha vai no fim do programa e serve
                           para liberar a memória que nó alocamos,
                           para não ocorrerem erros sempre temos
                           que liberar a memória.
                       */

Note que o ultimo printf está antes do free, pois se ele estiver depois, vai dar erro já que o espaço alocado anteriormente seria apagado.

A saída de ambos os códigos é a mesma:

coisa
outra coisa

“Ah então eu vou sempre usar a primeira forma, porque é mais fácil!” , use, mas não se esqueça que a primeira forma só funciona com strings, para outros tipos de arrays você terá que usar a segunda forma.

Só para fixar melhor veja como funcionariam o array dinâmico com o tipo int.


// alocando a memória que o array terá
int * array_dinamico = malloc ( sizeof (int) * 4); /*
                                                aqui nós alocamos um espaço que caiba 4 inteiros,
                                                pois o nosso array inicial terá 4 posições.
                                              */

array_dinamico [0] = 2;
*( array_dinamico + 1) = 3;
array_dinamico [2] = 23;
array_dinamico [3] = 894;

// realocando memória para que caibam 5 posições
array_dinamico = realloc ( array_dinamico , sizeof (int) * 5);

array_dinamico [0] = 2;
*( array_dinamico + 1) = 3;
array_dinamico [2] = 23;
array_dinamico [3] = 894;
*( array_dinamico + 4) = 34;

Lembre-se de alocar a quantidade certa de memória para o ponteiro, ao contrário dos arrays aqui você tem que saber a quantidade exata de bytes reservar, um macete muito útil é:

<tipo> * <variável> = malloc ( sizeof (<tipo>) * <quantidade de posições>);

Desta forma a quantidade de bytes necessária será sempre respeitada.

E de quebra vai aí uma dica extra sobre arrays dinâmicos, eles podem ser atribuídos diretamente, é só colocar um (<tipo> []) antes do array que você quer atribuir:

Mas cuidado, arrays normais não aceitam isso, só use em arrays dinâmicos

// Array normal
int a[3] = {0, 1, 2};
a = (int []){1, 2, 3};          // Não funciona!

// Array dinâmico
int * ad = {0, 1, 2};
ad = (int []){1, 2, 3};         // Funciona!