Projeto de Algoritmos

Endereços e ponteiros

Os conceitos de endereço e ponteiro são fundamentais em qualquer linguagem de programação (embora sejam mais visíveis em C que em outras linguagens).  O conceito de ponteiro é muito útil mas difícil; seu domínio exige um certo esforço.

Veja o verbete Pointer na Wikipedia.

Endereços

A memória de qualquer computador é uma sequência de bytes.  Cada byte armazena um de 256 possíveis valores.  Os bytes são numerados sequencialmente.  O número de um byte é o seu endereço (= address).

Cada objeto na memória do computador ocupa um certo número de bytes consecutivos. No meu computador, um char ocupa 1 byte, um int ocupa 4 bytes e um double ocupa 8 bytes

Cada objeto na memória do computador tem um endereço. Na maioria dos computadores, o endereço de um objeto é o endereço do seu primeiro byte. Por exemplo, depois das declarações

   char c;
   int i;
   struct {
      int x, y;
   } ponto;
   int v[4];

os endereços das variáveis poderiam ser
c 89421
i 89422
ponto 89426
v[0] 89434
v[1] 89438
v[2] 89442

(o exemplo é fictício).  O endereço de uma variável é dado pelo operador  &.  (Não confunda esse uso de "&" com o operador lógico and, que em C se escreve "&&".)  Se  i  é uma variável então  &i  é o seu endereço.  No exemplo acima,   &i   vale  89422   e   &v[3]  vale  89446.

Exemplo:  O segundo argumento da função de biblioteca scanf é o endereço da posição na memória onde devem ser depositados os objetos lidos no dispositivo padrão de entrada:

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

Ponteiros

Um ponteiro (= apontador = pointer) é um tipo especial de variável que armazena endereços.  Um ponteiro pode ter o valor especial

   NULL

que não é o endereço de lugar algum.  A constante NULL está definida no arquivo-interface stdlib e seu valor é 0 na maioria dos computadores. 

Se um ponteiro p armazena o endereço de uma variável i, podemos dizer "p aponta para i" ou "p é o endereço de i"  (querendo dizer "o ponteiro p aponta para i" no primeiro caso e "o valor de p é o endereço de i" no segundo).  Se um ponteiro p tem valor diferente de NULL então

   *p

é o valor do objeto apontado por p.  (Não confunda esse uso de "*" com o operador de multiplicação!)  Por exemplo, se i é uma variável e p é igual a &i então dizer "*p" é o mesmo que dizer "i".

diagrama de um ponteiro              diagrama de um ponteiro
Figura esquerda:  um ponteiro p, armazenado no endereço 90001, contém o endereço de um inteiro.  Figura direita:  representação esquemática da situação.

Há vários tipos de ponteiros: ponteiros para caracteres, ponteiros para inteiros, ponteiros para ponteiros para inteiros, ponteiros para registros etc.  O computador faz questão de saber de que tipo de ponteiro você está falando. Para declarar um ponteiro p para um inteiro, diga

   int *p;

Para declarar um ponteiro p para um registro cel, diga

   struct cel *p;

Um ponteiro r para um ponteiro que apontará um inteiro é declarado assim:

   int **r;

Exemplos

Suponha que a, b e c são variáveis inteiras. Eis um jeito bobo de fazer  "c = a+b":

int *p;        /* p é um ponteiro para um inteiro */
int *q; 
p = &a;        /* o valor de p é o endereço de a */
q = &b;        /* q aponta para b */
c = *p + *q;

Outro exemplo bobo:

int *p;  
int **r;       /* r é um ponteiro para um ponteiro para um inteiro */
p = &a;        /* p aponta para a */
r = &p;        /* r aponta para p e *r aponta para a */
c = **r + b;

Aplicação

Suponha que precisamos de uma função que troque os valores de duas variáveis inteiras, digamos ij.  É claro que a função

void troca( int i, int j) /* errado! */
{
   int temp;
   temp = i; i = j; j = temp;
}

não produz o efeito desejado, pois recebe apenas os valores das variáveis e não as variáveis propriamente ditas.  A função recebe "cópias" das variáveis e troca os valores dessas cópias, enquanto as variáveis "originais" permanecem inalteradas.  Para obter o efeito desejado, é preciso passar à função os endereços das variáveis:

void troca( int *p, int *q)
{
   int temp;
   temp = *p; *p = *q; *q = temp;
}

Para aplicar a função às variáveis i e j basta dizer

troca( &i, &j);

ou ainda

int *p, *q;
p = &i;
q = &j;
troca( p, q);

Exercícios

  1. Por que o código abaixo está errado?
    void troca( int *i, int *j) {
       int *temp;
       *temp = *i; *i = *j; *j = *temp;
    }
    
  2. Um ponteiro pode ser usado para dizer a uma função onde ela deve depositar o resultado de seus cálculos. Escreva uma função hm que converta minutos em horas-e-minutos. A função recebe um inteiro mnts e os endereços de duas variáveis inteiras, digamos hm, e atribui valores a essas variáveis de modo que m seja menor que 60 e que 60*h + m seja igual a mnts.   Escreva também uma função main que use a função hm.
  3. Escreva uma função mm que receba um vetor inteiro v[0..n-1] e os endereços de duas variáveis inteiras, digamos minmax, e deposite nessas variáveis o valor de um elemento mínimo e o valor de um elemento máximo do vetor.   Escreva também uma função main que use a função mm.

Vetores e endereços

Os elementos de qualquer vetor (= array) têm endereços consecutivos na memória do computador.  [Na verdade, os endereços não são consecutivos, pois cada elemento do vetor pode ocupar vários bytes. Mas o compilador C acerta os detalhes internos de modo a criar a ilusão de que a diferença entre os endereços de elementos consecutivos vale 1.]  Por exemplo, depois da declaração

   int *v;
   v = malloc( 100 * sizeof (int));

o ponteiro v aponta o primeiro elemento de um vetor de 100 elementos. O endereço do segundo elemento do vetor é v+1 e o endereço do terceiro elemento é v+2.  Se i é uma variável do tipo int então

v + i

é o endereço do (i+1)-ésimo elemento do vetor.  A propósito, as expressões  v + i  e  &v[i]  têm exatamente o mesmo valor e portanto as atribuições

   *(v+i) = 87;
   v[i] = 87;

têm o mesmo efeito.  Analogamente, qualquer dos fragmentos de código abaixo pode ser usado para "carregar" o vetor v:

   for (i = 0; i < 100; ++i)  scanf( "%d", &v[i]);
   for (i = 0; i < 100; ++i)  scanf( "%d", v + i);

 

Todas essas considerações também valem se o vetor for alocado pela declaração

   int v[100];

mas nesse caso v é uma espécie de "ponteiro constante", cujo valor não pode ser alterado.

Exercícios

  1. Suponha que os elementos do vetor v são do tipo int e cada int ocupa 8 bytes no seu computador. Se o endereço de v[0] é 55000, qual o valor da expressão  v + 3?
  2. Suponha que v é um vetor declarado assim:
       int v[100];
    

    Descreva, em português, a sequência de operações que deve ser executada para calcular o valor da expressão

       &v[k + 9];
    
  3. Suponha que v é um vetor. Descreva a diferença conceitual entre as expressões  v[3]  e  v + 3.
  4. O que há de errado com o seguinte trecho de código?
    char *a, *b;
    a = "abacate";
    b = "uva";
    if (a < b)
       printf( "%s vem antes de %s no dicionário", a, b);
    else
       printf( "%s vem depois de %s no dicionário", a, b);
    
  5. Diga (sem usar o computador) qual o conteúdo do vetor a depois dos seguintes comandos.
    int a[99];
    for (i = 0; i < 99; ++i) a[i] = 98 - i;
    for (i = 0; i < 99; ++i) a[i] = a[a[i]];
    

 


Veja aula em video sobre ponteiros no AcademicEarth
Veja aula em video sobre aritmética de ponteiros no AcademicEarth

URL of this site: www.ime.usp.br/~pf/algoritmos/
Last modified: Thu Sep 5 08:07:56 BRT 2013
Paulo Feofiloff
IME-USP

Valid HTML 4.01 Transitional    Valid CSS!