Endereços e Ponteiros

Que tal hoje falarmos um pouco sobre ponteiros? Atualmente os relógios digitais ganharam espaço, mas os de ponteiro ainda estão na moda. Sim, eu sei foi sem graça. Enfim, se você já estudou ou trabalha com C/C++ sabe que ponteiros são uma daqueles assuntos que confundem um pouco no início, neste artigo vou tentar esclarecer melhor o assunto através de alguns conceitos e exemplos.

Com funciona

Vamos descer um pouco o nível, no bom sentido, claro. Acompanhe o raciocínio, um pente de memória é constituído de milhares de unidades chamadas de células, numa memória típica cada célula armazena um bit, se agruparmos 8 células logo teremos 8 bits ou um byte, esse grupo de 8 células representa uma posição de memória. Agora imagine a memória RAM como um conjunto com centenas de milhões de posições de memória, que armazenam “pedaços de dados” dos programas você executa. Como saber qual posição acessar em determinado momento? Simples, através de um endereço.

De maneira informal vamos dizer que “um ponteiro aponta para uma posição de memória por meio de seu endereço”.

Da mesma forma que o tipo int indica que deve ser alocado/reservado na memória um espaço para valores do tipo inteiro, string para uma cadeia de caracteres, char para um caractere e assim por diante, um ponteiro reserva espaço para armazenar uma referência (em hexadecimal) a outro endereço qualquer de memória. Se neste endereço referenciado existe um valor do tipo inteiro então teremos um ponteiro para um inteiro, se for double, um ponteiro para double e assim por diante.

 

pointers_memory

 

Na imagem um um trecho de 4 bytes da memória foi reservado para armazenar o inteiro 20 e outro trecho para o endereço que “aponta” para o endereço deste valor.

Como diz o ditado “após colocar os pingos nos is”, podemos seguir adiante.

Ponteiros na prática

Vamos ver como funciona na prática, mas antes anote estas 3 regrinhas:

  1. Para declarar um ponteiro basta colocar o tipo da variável para o qual ele “aponta” e o asterisco antes do nome da variável: int *p, char* pLetra, double *pSalario…. Note que tanto faz colocar o asterisco imediatamente antes do nome da variável ou logo após o tipo.
  2. Para saber qual o valor armazenado na variável apontada pelo ponteiro usamos o operador de dereferência + o nome do ponteiro: *p, *pAlgumValor…
  3. Para saber o endereço na memória usamos o operador de referência + o nome da variável: &nome, &data, &numero….

Aqui vai um exemplo:

#include <iostream>

using namespace std;

int main(int argc, char** argv) {

    int soma = 20 + 15; // Variável que armazena o resultado de uma soma

    int* pResultado = &soma; // Ponteiro para o endereço da variável soma

    cout << "Resultado: " << *pResultado << "n"; // Resultado: 35

    system("pause"); // Aguarda até que uma tecla seja pressionada para encerrar o programa

    return 0;

}

No código acima temos uma variável qualquer que armazena o resultado de uma soma, em seguida declaramos o ponteiro que vai armazenar o endereço de memória dessa variável e por fim exibimos o resultado com a função de saída cout da biblioteca iostream. Em *pResultado é como se disséssemos: aponte para o que existe no endereço apontado por pResultado.

Aritmética de ponteiros

Na aritmética de ponteiros existem apenas as operações de adição e subtração, porém não como estamos acostumados, pois ao somar ou subtrair uma unidade de um ponteiro por exemplo, ele sempre irá apontar para uma casa a mais ou a menos do seu tipo de dado.

Vejamos, seja ptr um ponteiro para um inteiro que, neste caso pode reserva até 4 bytes e seu endereço é 200 em decimal, se somarmos 1, o endereço que ptr irá armazenar não é 201, mas sim 204 porque agora aponta para o

endereço atual + total de bytes reservados pelo tipo,

de forma análoga se subtrairmos 1, não ficaremos com 199 e sim 196, pois ptr aponta para

endereço atual – total de bytes reservados pelo tipo,

sem segredos não é mesmo?

#include <iostream>

using namespace std;

int main() {

    int i = 123;
    int* p = &i;

    cout << "Endereço atual: " << p << "n";
    cout << "Endereço atual + 1: " << (p + 1) << "n";
    cout << "Endereço atual - 1: " << (p - 1) << "n";

    system("pause");

    return 0;

}

Ponteiros de vetores

Lembrando que vetores ou arrays representam um conjunto de valores do mesmo tipo armazenados na memória em sequência. Estes valores podem ser acessados através de um índice que indica determinada posição no vetor e o índice sempre começa a partir de zero.

Mas o que interessa mesmo para nós é saber que o nome de um vetor é um ponteiro e aponta para sua primeira posição, então não precisamos preceder com o caractere & para indicar seu endereço, veja:

    int vetor[5] = {1, 2, 3}; // Declara um vetor de 5 posições
    int* pVetor; // Ponteiro
    int x = 123; // Uma variável qualquer
    
    pVetor = vetor;
	
    cout << "Valor posicao 0: " << pVetor[0] << "n";
    cout << "Valor posicao 1: " << pVetor[1] << "nn";
    cout << "Valor posicao 2: " << pVetor[1] << "nn";

    pVetor = &x; // pVetor[0] passa a valer 123

    cout << "Valor posicao 0: " << pVetor[0] << "n";
    cout << "Valor posicao 1: " << pVetor[1] << "n";
    cout << "Valor posicao 2: " << pVetor[2] << "n";
    cout << "Valor posicao 3: " << pVetor[3] << "n";}

Nas primeiras linhas temos um vetor de 5 posições, um ponteiro e um inteiro com o valor 123. Mais abaixo fizemos o ponteiro apontar para o endereço do primeiro índice do vetor, isto seria o mesmo que fazer pVetor = vetor[0]. Depois exibimos o que existe nas 3 posições e alteramos o valor da primeira posição desta forma “deslocamos” o que a em cada posição uma casa adiante no vetor.

Ainda podemos percorrer o vetor utilizando a aritmética de ponteiros para indicar o próximo endereço de memória:

cout << "Valor posicao 0: " << *(pVetor) << "n";
cout << "Valor posicao 1: " << *(pVetor + 1) << "n";
cout << "Valor posicao 2: " << *(pVetor + 2) << "n";

Ponteiros para Ponteiros

Um ponteiro que aponta para outro, que ainda pode apontar para um ponteiro diferente e assim por diante, basta guardar o endereço de memória um do outro criando assim níveis de apontamento, para isso aumentamos o número de asteriscos. Veja:

  • *ptr1 aponta para o conteúdo da região final
  • **ptr2 aponta para o endereço de ptr1
  • ***ptr3 aponta para o endereço de ptr2
#include <iostream>

using namespace std;

int main() {
	
    char*  ptr1;
    char** ptr2;
    char*** ptr3;
		 
    char letraA = 'A';
    char letraB = 'B';
		
    ptr1 = &letraA;
    ptr2 = &ptr1;
    ptr3 = &ptr2;
	
    cout << "Valor final: " << ***ptr3 << "n"; // Valor final: A
	
    ptr1 = &letraB; // Agora ptr1 aponta para o endereço da letraB
	
    cout << "Valor final: " << ***ptr3 << "n"; // Valor final: B

    system("pause"); 

    return 0;
}

Ponteiros para função

 Sim ainda existem ponteiros para funções, não acredita? Então olhe só:

#include <iostream>

using namespace std;

int max(int a, int b){
   if(a > b) return a;
   else return b;
}

int min(int a, int b){
   if(a < b) return a;
   else return b;
}

float abs(float a){
   if(a < 0) return -a;
   else return a;
}

int main() {
	
    int (*pMax)(int, int) = max;
    int (*pMin)(int, int) = min;
    float (*pAbs)(float) = abs;
	
    cout << pMax(2,35) << "n"; // 35
    cout << pMin(7,12) << "n"; // 7
    cout << pAbs(-8.5) << "n"; // 8.5

    // Aqui mudamos a função apontada de pMax para pMin
    pMax = pMin;
    cout << pMax(2,35) << "n"; // 2

    system("pause"); 

    return 0;
}

A sintaxe para declarar o ponteiro de uma função, como podemos ver acima, segue os parâmetros do cabeçalho dessa função, porém com o nome do ponteiro, ou seja, tipo de retorno + nome do ponteiro + parâmetros. Para mudar a função apontada fizemos uma atribuição, note que é necessário que as duas funções devem aceitar os mesmos parâmetros.

Isso é tudo, tentei dar uma noção sobre o assunto, uma vez que poderia ser escrito um livro sobre isso. Espero ter ajudado, até a próxima!

 

Deixe um comentário