Ponteiros em C/C++ - Parte 1

Started by Dark_Side, 19 de May , 2007, 07:04:06 PM

Previous topic - Next topic

0 Members and 1 Guest are viewing this topic.

Dark_Side

Hi,

O uso de ponteiros em C e C++ está entre as maiores dificuldades em programar nestas linguagens. Meu objetivo, com este texto, é tentar explicar o que é, para que serve, como funciona e qual a utilidade de um tipo especial de variável denominado ponteiro.

Começaremos definindo um ponteiro. Variáveis do tipo int (inteiro) armazenam valores inteiros, o tipo char armazena caracteres, etc. Por ser uma variável, um ponteiro também armazena um valor. Mas há uma grande diferença no tipo de valor armazenado: enquanto em uma variável comum nós armazenamos valores do seu respectivo tipo, com ponteiros, podemos armazenar apenas endereços de memória.

Mas o que vem a ser um endereço de memória? Trata-se de um identificador único que indica um local na memória do computador no qual pode-se armazenar segmentos de dados. Exemplos:

0x00000100, 0x02000100,0x00FF2DA9, 0xFFFFFFFF, etc.

Endereços de memória são identificados por números expressos na base hexadecimal, como nos exemplos acima. Nos computadores atuais, cada endereço de memória armazena 1 byte.

Sempre quando declaramos uma variável, seja de qualquer tipo, é reservada, na memória, uma faixa de endereços correspondentes a esta variável. Exemplo:

int exemplo;

Na declaração acima, teríamos um endereçamento similar a:

Quote0x00000001 -> Início da memória relativa à variável "exemplo"
0x00000002
0x00000003
0x00000004 -> Fim da memória relativa à variável "exemplo"

obs:: os endereços acima são fictícios e foram usados para simplificar o entendimento.

Como se nota, a variável exemplo pode armazenar dados iniciando pelo endereço 0x00000001 até 0x00000004. Mas por que esta faixa de endereços?

Para declarar uma variável em C, devemos informar seu tipo e nome: "int" e "exemplo" no "exemplo". Sabemos que uma variável do tipo "int" possui 4 bytes. Por esta razão - o tipo ter 4 bytes -, é que os endereços usados são de 0x00000001 até 0x00000004, totalizando, igualmente, 4 bytes.

Se, ao invés de um inteiro, tivéssemos um char:

char exemplo;

O mapa de memória seria:

Quote0x00000001 -> Início e fim da memória relativa à variável "exemplo"

Um double:

double exemplo;

Quote0x00000001 -> Início da memória relativa à variável "exemplo"
0x00000002
0x00000003
0x00000004
0x00000005
0x00000006
0x00000007
0x00000008 -> Fim da memória relativa à variável "exemplo"

Resumindo: a faixa de endereços reservada para uma variável depende diretamente do tipo desta variável.

Quando estamos trabalhando com ponteiros, a declaração é feita da seguinte forma:

int  * pt;

Dizemos que definimos um ponteiro para inteiro chamado "pt". A sintaxe então é:

tipo * nome_ponteiro;

Como se nota, a diferença entre uma variável comum e um ponteiro se evidencia no uso do caractere *.

Voltemos ao conceito de variáveis e endereços de memória. Já que um ponteiro é também uma variável, há também um endereço de memória no qual este ponteiro se situa.

E, como não poderia ser diferente, uma variável deste tipo possui um tamanho. Quando se trata de um sistema de 32 bits o tamanho de um ponteiro é sempre 4 bytes, não importando se é um ponteiro para inteiro, double, char, etc.

Como já foi dito, ponteiros armazenam endereços de memória e em uma plataforma na qual os endereços de memória são tratados como inteiros de 32 bits, estes endereços podem variar de 0 até 4.294.967.295 (0xFFFFFFFF), que são os valores possíveis em 4 bytes.

Em C, existe uma função muito útil que nos permite saber o tamanho, em bytes, de uma variável ou estrutura: sizeof(). Veja um exemplo de uso:


#include <stdio.h>
int main()
{
printf("%d\n",sizeof(char));
printf("%d\n",sizeof(int));
printf("%d\n",sizeof(double));
return 0;
}

Saída do programa:

Quote1
4
8

O programa mostra na tela os tamanhos, em bytes, de uma variável do tipo char, int e double, respectivamente.

Observe:

#include <stdio.h>
int main()
{
printf("%d\n",sizeof(char *));
printf("%d\n",sizeof(int *));
printf("%d\n",sizeof(double *));

return 0;
}

Saída:

Quote4
4
4

Assim como disse, o tamanho de um ponteiro é sempre 4 bytes (plataforma 32 bits).

Você pode utilizar a função sizeof() passando variáveis como argumento:

#include <stdio.h>
int main()
{
int exemplo;

printf("%d\n",sizeof(exemplo);

return 0;
}

Saída:

Quote4

Atribuir valores a variáveis é simples:

int exemplo = 10; // Atribuir-se à variável "exemplo" o valor inteiro 10

Quando fazemos uma atribuição da forma acima, a faixa de endereço da variável "exemplo" contém o valor 10.

Com ponteiros, embora a idéia seja a mesma, utilizamos um operador próprio para fornecer o seu valor: &. Este operador retorna o endereço de memória de uma variável ou estrutura que o sucede. Exemplos:

#include <stdio.h>
int main()
{
int exemplo;
printf("%d",&exemplo);

return 0;
}

O programa acima simplesmente mostra o endereço de memória reservado para a variável "exemplo".

É muito importante conhecer este atributo para usarmos ponteiros.

Vejamos como trabalhar com o operador & usando ponteiros:

#include <stdio.h>
int main()
{
int exemplo;
int * ponteiro;

exemplo = 10;
ponteiro = &exemplo;
return 0;
}

Um exemplo bastante simples. Vamos analisar as declarações:

int exemplo = 10; // Declaramos uma variável do tipo inteiro.
int * ponteiro; // Declaramos uma variável do tipo ponteiro para inteiro.

Vamos tomar como endereço inicial o valor 0x00000001. Então, uma vez em que a variável "exemplo" é do tipo inteiro e este, por sua vez, possui 4 bytes, os endereços de memória relativos à variável seriam:

0x00000001
0x00000002
0x00000003
0x00000004

Posteriormente, temos a declaração do ponteiro para inteiro "ponteiro" que, por ser um ponteiro, também ocupa 4 bytes. Os endereços seriam:

0x00000005
0x00000006
0x00000007
0x00000008

Seguindo o código, temos a atribuição do valor 10 à variável exemplo:

exemplo = 10;

Ao fazermos isso, ocorre o seguinte:



O mesmo ocorre quando atribuimos o valor ao ponteiro "ponteiro":



A figura abaixo ilustra a idéia de como funciona um ponteiro:




Como vemos, o ponteiro aponta para a varíavel "exemplo".

O programa abaixo confirma a idéia:

#include <stdio.h>
int main()
{
int exemplo;
int * ponteiro = &exemplo; // Inicializa o ponteiro com o endereço de "exemplo"

printf("%p => %p",ponteiro,&exemplo); // Mostra o valor do ponteiro e o endereço de "exemplo"
return 0;
}

Ao compilar e executar o programa acima, você verá que a saída será algo similar a:

00000001 => 00000001

Obs:: utilizamos o caractere formatador "%p" para imprimir um endereço de memória na tela.

Importante:
1) Os endereços reais serão diferentes;
2) Neste texto, os endereços de memória estão sendo mostrados de forma crescente:

Quoteint a,b:

a:

0x00000001
0x00000002
0x00000003
0x00000004

b:

0x00000005
0x00000006
0x00000007
0x00000008

No entanto, estes endereços são DECRESCENTES na realidade, o correto então seria:

Quotea:

0x00000005
0x00000006
0x00000007
0x00000008

b:

0x00000001
0x00000002
0x00000003
0x00000004

O uso da forma crescente apenas foi feito para simplificar o entendimento.


Uma vez atribuído um endereço de memória como valor de um ponteiro, podemos utilizar este ponteiro para acessarmos/alterarmos o valor armazenado na variável (endereço de memória) para a qual ele aponta.

Retomemos o exemplo inicial:

#include <stdio.h>
int main()
{
int exemplo;
int * ponteiro;

exemplo = 10;
ponteiro = &exemplo;

printf("Endereco da variavel: %p\n",ponteiro);
printf("Valor da variavel: %d\n",*ponteiro);
return 0;
}

Até a linha que mostra o endereço da váriavel, já compreendemos. O que aparece de novo no código é a seguinte linha:

printf("Valor da variavel: %d\n",*ponteiro);

Quando utilizamos o operador * antes do nome de um ponteiro, estamos acessando o valor do endereço de memória apontado por ele. No caso, este endereço é o mesmo reservado para a variável "exemplo". Inicialmente, atribuimos o valor 10 à variável, então, por lógica, ao fazer uso da expressão "*ponteiro" a saída deverá ser 10.

Algumas observações:

1) Embora o símbolo seja o mesmo, o operador matemático * (multiplicação) não deve ser confundido com o operador de referência * (ponteiros).

2) Quando acessamos um valor através de um ponteiro, deferenciamos este ponteiro.

3) Quando utilizamos o operador * da forma *nome_ponteiro, estamos acessando o valor do endereço de memória apontado pelo ponteiro; quando o utilizamos após o nome de um tipo de variável, estamos sinalizando a declaração de um ponteiro, como por exemplo: "int * pt".

Observe o exemplo abaixo:

#include <stdio.h>
int main()
{
int a,b;
int * ponteiro;

ponteiro = &a;
*ponteiro = 10;

ponteiro = &b;
*ponteiro = 20;

printf("A = %d\nB = %d",a,b);

return 0;
}


O programa mostra como podemos alterar valores de variáveis através de um ponteiro. Vamos analisar trecho por trecho:

Declaração de variáveis:

int a,b;
int * ponteiro;

ponteiro = &a;
*ponteiro = 10;

Acima, nós fazemos com que o ponteiro "ponteiro" aponte para o endereço da variável "a" e, em seguida, alteramos o valor desta variável para 10 através deste ponteiro.

O mesmo ocorre abaixo:

ponteiro = &b; // Ponteiro aponta para o endereço de "b"
*ponteiro = 20; // Altera o valor para 20

Mostramos os valores das variáveis:

printf("A = %d\nB = %d",a,b); // Imprime os valores 10 e 20, respectivamente.

Um erro muito comum é usar um ponteiro que não foi inicializado, chamado ponteiro "selvagem". Por inicializar um ponteiro, me refiro à atribuição de um valor para ele.

Exemplos:

int  * ponteiro; // Ponteiro selvagem
printf("%d",*ponteiro); // Tenta acessar o valor apontando pelo ponteiro

O programa acima tenta acessar o valor de um endereço de memória apontado por "ponteiro". Uma vez em que este ponteiro não foi inicializado, seu valor é indeterminado. O endereço de memória para o qual este ponteiro aponta pode ser NULO (ZERO) ou até mesmo reservado para o sistema. Na tentativa de obter o seu valor, erros de violação de memória podem ocorrer.

Por isso, deve-se sempre atribuir um valor a um ponteiro antes de usá-lo:

int  * ponteiro; // Ponteiro selvagem
int numero = 10; // Variável do tipo inteiro; seu valor é 10

ponteiro = № // O ponteiro deixa de ser "selvagem" pois aponta agora para "numero"
printf("%d",*ponteiro); // Tenta acessar o valor apontando pelo ponteiro

Acima, temos o uso correto de ponteiros. Um outro exemplo de uso correto:

int numero = 20;
int  * ponteiro = № // Declara e inicializa o ponteiro com o endereço de "numero"
printf("%d",*ponteiro); // Tenta acessar o valor apontando pelo ponteiro

Algo muito importante a ser notado é que, ao atribuir o endereço de uma variável a um ponteiro, a variável permanece inalterada, exceto se utilizarmos o ponteiro para modificarmos o seu valor como vimos anteriormente.

É possível realizar algumas operações com ponteiros, como comparar, incrementar e decrementar ponteiros.

1) Igualando ponteiros

Observe:

#include <stdio.h>
int main()
{
int numero;
int *p1, *p2;

p1 = №
*p1 = 10;

p2=p1;

printf("Valor da variavel 'numero': %d",*p2);

return 0;
}

Analisando trecho por trecho:

int numero; // Declara uma variável do tipo inteiro
int *p1, *p2; // Dois ponteiros para inteiro

p1 = № // Faz com que "p1" aponte para "numero"
*p1 = 10; // Altera o valor de "numero" para 10

p2=p1; // Igualamos o valor de "p2" com "p1"

printf("Valor da variavel 'numero': %d",*p2); // Mostra o valor através do ponteiro "p2"

Vamos destacar a seguinte expressão:

p2=p1; // Igualamos os valores de "p2" e "p1"

Quando igualamos os valores de ponteiros (endereços de memórias), fazemos com que estes ponteiros apontem para o mesmo local. No exemplo, "p1" aponta para "numero" e, após igualarmos o valor deste ponteiro com "p2", este último também passará apontar para a variável "numero". Desta forma, ao alteramos o valor apontado por "p1" ou "p2", a alteração será feita sob uma mesma variável: "numero".

Ainda falando sobre como igualar ponteiros, podemos, em vez de igualar os endereços de memória entre ponteiros, igualar os valores dos endereços de memória de determinados ponteiros, como se vê no exemplo:


#include <stdio.h>
int main()
{
int numero1 = 0;
int numero2 = 50;
int *p1, *p2;

p1 = &numero1;
p2 = &numero2;

printf("Antes de igualar os valores...\n\n");
printf("Valor de numero1: %d\n",*p1);
printf("Valor de numero2: %d\n",*p2);

*p1 = *p2;

printf("\nApos igualar os valores...\n\n");
printf("Valor de numero1: %d\n",*p1);
printf("Valor de numero2: %d\n",*p2);

return 0;
}

Vejamos:

int numero1 = 0;
int numero2 = 50;
int *p1, *p2;
Declaramos e inicializamos as variável "numero1", cujo valor é 0, e "numero2" com o valor 50. Em seguida, dois ponteiros para inteiros são declarados: p1 e p2.

Inicializamos cada ponteiro com o endereço de memória de cada variável:

p1 = &numero1;
p2 = &numero2;

O trecho abaixo mostra os valores das variáveis referenciados pelos ponteiros antes de igualarmos os valores apontados por estes ponteiros:

printf("Antes de igualar os valores...\n\n");
printf("Valor de numero1: %d\n",*p1);
printf("Valor de numero2: %d\n",*p2);

*p1 = *p2;

Igualamos os valores apontados por "p1" e "p2". Com isso, o valor apontado por "p1" (valor de numero1) passar ser o mesmo apontado por "p2" (valor de numero2).

Mostramos novamente os valores:

printf("\nApos igualar os valores...\n\n");
printf("Valor de numero1: %d\n",*p1);
printf("Valor de numero2: %d\n",*p2);

Saída do programa:

QuoteAntes de igualar os valores...

Valor de numero1: 0
Valor de numero2: 50

Apos igualar os valores...

Valor de numero1: 50
Valor de numero2: 50

2) Comparando ponteiros

Como se sabe, podemos utilizar os seguintes operadores para realizarmos comparações:

Quote== igual
!= diferente
>  maior
<  menor
>= maior ou igual
<= menor igual

Podemos utilizar estes mesmos operadores em comparações com ponteiros.

Para começar, vejamos o seguinte exemplo:

#include <stdio.h>
int main()
{
int numero1 = 0;
int numero2 = 50;
int *p1, *p2;

p1 = &numero1;
p2 = &numero2;

if(p1 == p2)
 printf("Os ponteiros apontam para o mesmo local.");
else
 printf("Os ponteiros apontam para locais diferentes.");
 
return 0;
}

Neste primeiro exemplo, verificamos se o valor do ponteiro "p1" é igual ao valor de "p2". Se a condição for verdadeira, então dizemos que estes ponteiros apontam para um mesmo local.

Outro exemplo:

#include <stdio.h>
int main()
{
int numero1;
int numero2;
int *p1, *p2;

p1 = &numero1;
p2 = &numero2;

if(p1 > p2)
 printf("O ponteiro 'p1' aponta para um endereco de memoria maior.");
else
 printf("O ponteiro 'p2' aponta para um endereco de memoria maior.");
 
 
return 0;
}

Desta vez, são comparados os endereços apontados por "p1" e "p2". Com essa verificação, podemos afirmar qual ponteiro aponta para um local na memória mais alto.

Comparando valores de variáveis através de ponteiros:

#include <stdio.h>
int main()
{
int numero1;
int numero2;
int *p1, *p2;

p1 = &numero1;
p2 = &numero2;

*p1 = 10;
*p2 = 20;

if(*p1 > *p2)
 printf("O valor apontado por 'p1' e' maior.");
else
 printf("O valor apontado por 'p2' e' maior.");

return 0;
}

3) Incrementando e decrementando

Quando incrementamos um ponteiro, este passa a apontar para o próximo endereço de memória que armazena o valor do mesmo tipo do ponteiro. Por exemplo, se incrementarmos um ponteiro para inteiro, ele passa apontar para o endereço de memória localizado 4 bytes adiante - pois um inteiro possui 4 bytes-, este é um motivo pelo o qual devemos informar o tipo de ponteiro: se for do tipo inteiro, ao incrementá-lo, 4 bytes são avançados na memória; se or do tipo char, apenas 1 byte é avançando; caso seja double, 8 bytes e etc.

O mesmo ocorre quando decrementamos ponteiros. Só que, desta vez, ao invés de avançarmos nos endereços de memórias, retrocedemos.

Exemplos:

int *p;
//...
p++; // Incrementa o ponteiro -> aponta para o endereço de memória localizado 4 bytes adiante


int *p;
//...
p--; // Decrementa o ponteiro -> aponta para o endereço de memória localizado 4 bytes atrás


É possível ainda incrementar/decrementa um ponteiro em um determinado número de posições:

int *p;
//...
p += 5; // Faz com que o ponteiro avance 5 posições referentes a inteiros na memória, isto é, avança 5 * 4 = 20 bytes.


char *p;
//...
p +=10 ; // Faz com que o ponteiro retroceda 10 posições referentes a chars na memória, isto é, retrocede 10 * 1 = 10 bytes.


Um outro uso muito comum dessas operações é obter o valor de um determinado endereço de memória:

char * p;
//...
printf("%c",*(p++)); // Obtém o valor do próximo endereço de memória - já que 1 byte é avançado

A sitaxe é:

Quote*(ponteiro++);
*(ponteiro--);
*(ponteiro += posicoes);
*(ponteiro -= posicoes);


Veremos como decrementar e incrementar ponteiros com mais detalhes em próximas partes deste texto, nas quais abordarei o uso de ponteiros com vetores.

Assim como incrementamos/decrementamos valores de variáveis, podemos fazê-lo através de ponteiros:

#include <stdio.h>
int main()
{

int numero = 10;

int * p = №
printf("%d\n",*p); // Mostra o valor inicial

(*p)++; // Incrementa o valor de "numero"
printf("%d\n",*p); // Mostra o valor atual

(*p) += 10; // Incrementa o VALOR em 10
printf("%d\n",*p);

(*p)--; // Decrementa o valor
printf("%d\n",*p);

(*p) -= 5; // Decrementa em 5
printf("%d\n",*p);

return 0;
}

Saída:

Quote10
11
21
20
15


Com base nos exemplos, podemos estabelecer a seguinte relação:

Para incrementar/decrementar endereços apontados por ponteiros fazemos:

Quotenome_ponteiro++;
nome_ponteiro += numero_posicoes;

nome_ponteiro--;
nome_ponteiro -= numero_posicoes;

Quando queremos realizar estas operações com o VALOR apontando por ponteiro, fazemos:


Quote(*nome_ponteiro)++;
(*nome_ponteiro) += numero_posicoes;

(*nome_ponteiro)--;
(*nome_ponteiro) -= numero_posicoes;


Vamos complicar um pouco.

Já estamos cientes que a declaração abaixo define um ponteiro para inteiro:

int *p;

Mas o uso de ponteiros em C/C++ não se limita a isso.

Veja:

int **p;

Como você define uma variável declarada da forma acima ;)

Termino aqui esta primeira parte do artigo.
Até as próximas xD
Bye.

NetKiler

Muito bom, to comessando a estudar C, mas tava meio perdido na parte dos ponteiros.
belo post e muito bem explicado, aguardando as proximas partes do artigo ;D

Dark_Side

Hi,

O texto foi revisado/reformulado/atualizado. Link:

http://www.wesk.org/textos/darkside/4.html

Gostaria de agradecer ao whit3_sh4rk por hospedar este e outros arquivos :)

Bye.