Aula 3

1  Resumo da matéria

1.1  Ponteiros

Um ponteiro é uma variável que contém o endereço de um objecto de um dado tipo, normalmente outra variável.  A declaração de um ponteiro não reserva memória para a variável endereçada.  Um ponteiro, quando não inicializado, contém um endereço arbitrário, geralmente inválido. Usar o conteúdo correspondente ao endereço contido num ponteiro não inicializado é um erro.

A definição/declaração de ponteiros faz-se tal como para as outras variáveis, mas antecedendo o nome da variável de *:

int *p;
A inicialização do valor do ponteiro p, fazendo com que aponte para a variável i definida anteriormente, faz-se à custa do operador endereço &:
int i;
int *p;
p = &i;
O acesso ao valor da variável apontada por p (ou seja, ao conteúdo do endereço), faz-se à custa do operador conteúdo (ou desreferenciação) *:
cout << *p;
O mesmo se passa para tipos definidos pelo utilizador.  Por exemplo, a declaração de um ponteiro para uma variável da classe Aluno:
class Aluno {
    ...
};

Aluno *p;

Não se deve confundir a definição/declaração de um ponteiro para uma variável com a utilização do operador conteúdo.  Por exemplo:
int i = 10;
int *p = &i; // definição de um ponteiro para int inicializado com o endereço de i.
cout << *p; // acesso ao conteúdo do endereço em p para a escrita do valor de i.

1.2  Matrizes e ponteiros

Os elementos numa matriz estão sempre em posições de memória sucessivas.  Por exemplo, dada a matriz
int m[20];
então o endereço do elemento 4 (o quinto elemento) é igual ao endereço do elemento 3 somado de uma unidade.  Ou seja, &m[4] = &m[3] + 1.  Claro está que, ocupando os inteiros quatro bytes, estes endereços na realidade diferem de quatro unidades, mas isso é transparente para o utilizador: o compilador encarrega-se de calcular o tamanho das instâncias do tipo referenciado pelo endereço, neste caso int, de uma forma automática.  De qualquer forma, o operador sizeof pode ser usado para saber quantos bytes ocupa cada instância dum dado tipo.

Numa expressão, o nome de uma matriz é sempre interpretado como um ponteiro constante para o respectivo primeiro elemento, excepto quando o nome ocorra como operando do operador sizeof.  Sendo

int m[20];
a ocorrência de m numa qualquer expressão é interpretada como significando o mesmo que &m[0].  Por outro lado, todas as operações de indexação do tipo m[i] são sempre interpretadas como significando *(m + i).
Por exemplo, dadas as seguintes definições:
int m[20];
int *p = m; // ou, o que é o mesmo, int *p = &m[0].
ambas as instruções
p[19] = 1;
m[19] = 1;
são válidas e têm o mesmo efeito.

1.3  Aritmética de ponteiros

Os ponteiros podem conter: endereços de variáveis simples dum tipo apropriado, endereços de quaisquer elementos de matrizes dum tipo apropriado, sendo considerados endereços válidos os endereços do elemento fictício imediatamente depois do último elemento das matrizes, endereços de zonas de memória dinâmica (a ver em aulas posteriores) e o valor especial nulo 0 (zero), que é garantido nunca ser um endereço válido de qualquer variável, elemento de matriz, etc.

A soma de ponteiros com inteiros e a subtracção de inteiros de ponteiros, incluindo os operadores de incrementação e decrementação, estão bem definidos para ponteiros, desde que o endereço resultante se refira a um elemento (incluindo o fictício final) da mesma matriz (que pode ser dinâmica).  Por exemplo, sendo

int m[20];
int *p = m; // ou, o que é o mesmo, int *p = &m[0].
p += 3;
ambas as instruções
p[16] = 1;
m[19] = 1;
são válidas e têm o mesmo efeito.

A subtracção de ponteiros também está bem definida desde que ambos se refiram a elementos válidos (incluindo o fictício final) da mesma matriz (que pode ser dinâmica).  Dado o código atrás

cout << p - m << endl;
tem como resultado aparecer
3
no ecrã.

1.4  Passagem de ponteiros e matrizes como argumento

As matrizes, quando forem parâmetros de uma função, são sempre interpretadas como ponteiros (não constantes).  Isto é fundamental porque, sendo os argumentos duma chamada a uma função expressões, quaisquer nomes de matrizes que nelas ocorram são interpretados como ponteiros para  o respectivo elemento.

Isto significa que é sempre possível passar um ponteiro como argumento, na chamada a uma função, no lugar de um parâmetro especificado como uma matriz.  Por exemplo, a função

int soma(int m[], int n);
pode ser chamada da seguinte forma:
int matriz[] = {1, 2, 3, 4};
int *p = matriz; // ou seja, p = &matriz[0].
int z = soma(p, 4);
A função soma() poderia ser implementada como se segue:
int soma(int m[], int n) {
    int r = 0;
    for(; n != 0; n--) {
        r += *m;
        m++;
    }
}
A utilização de ponteiros tem algumas semelhanças com a autilização de referências, nomeadamente porque permite a alteração do valor referenciado pelo ponteiro.  Por exemplo, a funçao
void troca(int *x, int *y) {
    int aux = *x;
    *x = *y;
    *y = aux;
}
permite trocar os valores referenciados por dois ponteiros.  O resultado de
int a = 10, b = 20;
troca(&a, &b);
cout << a << ' ' << b << endl;
seria aparecer
20 10
no ecrã.  Repare-se que a chamada da função se faz usando o operador & explicitamente.

A função também pode ser usada com se segue:

int a = 10, b = 20;
int *pa, *pb;
pa = &a;
pb = &b;
troca(pa, pb);
cout << *pa << ' ' << *pb << endl;
Finalmente, note-se bem a diferença face à utilização de referências propriamente ditas na versão alternativa abaixo:
void troca(int& x, int& y) {
    int aux = x;
    x = y;
    y = aux;
}

...

int a = 10, b = 20;
troca(a, b);
cout << a << ' ' << b << endl;

(Repare-se que, tal com o símbolo *, também o símbolo & tem um significado muito diferente numa expressão e numa declaração ou definição!)

2  Exercícios

1.  Escreva o seguinte programa e execute-o.  Interprete e comente o resultado.
int main() {
    int *p1;
    int *p2;
    int i = 2;
    p1 = p2 = &i;
    *p1 = 3;
    cout << *p1 << " " << *p2 << " " << i << endl;
}
2.  Escreva o seguinte programa e execute-o.  Interprete o resultado e comente profusamente o programa.
#include <iostream>
using namespace std;

void mostraMatriz(int m[], int n) {
    for(int *p = m; p != m + n; p++) {
        cout << *p;
        if(p != m + n - 1)
            cout << ' ';
    }
    cout << endl;
}

int main() {
    int a = 1;
    int m[] = {1, 2, 3, 4, 5};
    int b = 2;

    mostraMatriz(m,5);
    cout << "Tamanho de um int: " << sizeof(int) << endl;
    cout << "a: " << a << endl;
    cout << "b: " << b << endl;

    cout << "&a:    " << &a << endl;
    cout << "&b:    " << &b << endl;
    cout << "&m[0]: " << m  << endl;

    m[5] = 10;
    m[6] = 20;

    cout << "a: " << a << endl;
    cout << "b: " << b << endl;
}

3.  Reescreva a função mostraMatriz() sem usar quaisquer variáveis locais (lembre-se que uma matriz, quando parâmetro de uma função, é na realidade um ponteiro).