C++ 101 – Parte 4 – Ponteiros (conceitos básicos)

Posted on 22/09/2011

5


Olá pessoal, tudo certo?!

Depois de uma discussão boa sobre a “estética” do C++ no twitter, resolvi voltar a abordar o tema aqui no blog. Talvez você ainda não saiba, mas comecei uma série “introdutória” para C++ há algum tempo.

No post de hoje, pretendo introduzir um tema “traumático” para quem utiliza C++: ponteiros.

O que é um ponteiro?

Cada posição de memória que usamos para armazenar um valor possui um endereço.

Um ponteiro é uma variável que armazena um endereço em memória que contém dados de um determinado tipo.

Um ponteiro tem um “nome de variável” e também tem um tipo que identifica que “tipo de dado” está armazenado no endereço que possui.

Um ponteiro que armazene o endereço de memória onde está armazenado um dado inteiro, por exemplo, é conhecido como “ponteiro para um inteiro”.

Declarando um ponteiro

A declaração para um ponteiro é parecida com aquela que estamos habituados a fazer para variáveis comuns, exceto que o nome do ponteiro deve ser prefixado com um asterisco. Considere:


              
#include 

int main()
{
        int *pnumber1;
        int* pnumber2;
        return 0;
}

            

O pequeno programa acima declara dois ponteiros. Repare que não há diferença entre manter o asterísco “perto” do nome da variável ou do tipo. Qualquer forma que você escolha, será aceita pelo compilador.

É comum encontrarmos ponteiros nomeados começando com um p.

O operador Address-Of

Já sabemos que ponteiros são utilizados para armazenar endereços em memória dos nossos dados. Então, como fazemos para obter o endereço de memória correspondentes aos dados armazenados em uma variável? Simples, usamos o operador “address-of” (&). Observe:


              
#include 

int main()
{
        int number = 10;
        int* pnumber;

        pnumber = &number;
        return 0;
}

            

O que esse pequeno código faz é criar uma variável do tipo inteiro, com o valor 10. Logo depois, atribuímos o “endereço” dessa variável em nosso ponteiro.

Podemos utilizar o operador “address-of” para obter o endereço de qualquer variável (de qualquer tipo). Entretanto, devemos sempre tomar o cuidado de utilizar o tipo apropriado de ponteiro.

Por exemplo, se desejamos armazenar o endereço de uma variável do tipo double, devemos utilizar um ponteiro para double (double *).

Usando o ponteiro

Já sabemos como obter o endereço de uma variável. Também já sabemos como armazenar esse endereço em um ponteiro. Agora, vamos ver como “acessar” o valor que está na posição do ponteiro, para isso, usamos o operador de indireção (*).

Observe:


              
#include 

int main()
{
        int number = 10;
        int* pnumber;

        pnumber = &number;

        std::cout << "Value-Of number variable  : " << number << std::endl
                          << "Address-Of number variable: " << pnumber << std::endl
                          << "Value-Of pnumber variable : " << pnumber << std::endl
                      << "Value-Of pnumber pointer  : " << *pnumber << std::endl;


        return 0;
}

            

Executando (na minha máquina):

image

Lindo!

Entenda que o ponteiro está “apontando” para o valor da variável. Repare:


              
#include 

int main()
{
        int number = 10;
        int* pnumber;

        pnumber = &number;
        *pnumber = 11;

        std::cout << "Value-Of number variable  : " << number << std::endl;

        return 0;
}

            

Esse código “altera” o valor de number através do ponteiro.

Por que usar ponteiros?

Se você é novo em C++, talvez esteja se perguntando o porquê usar ponteiros? Vamos a algumas justificativas:

  • como você verá em seguida, podemos utilizar a “notação” de ponteiros para manipular arrays, o que é frequentemente “mais rápido”;
  • ponteiros facilitam o acesso a grandes porções de dados;
  • ponteiros permitem a alocação dinâmica de memória;
  • se você vem de C#, ponteiros são os “delegates” do C++.

Inicializando ponteiros

Utilizar ponteiros que não foram inicializados é extremamente perigoso. Sem querer, podemos sobrescrever dados em áreas “aleatórias” de memória.

Já sabemos como inicializar um ponteiro para apontar para o “endereço” de uma variável.

Outra boa prática seria inicializar um ponteiro é atribuir nullptr que seria o equivalente a “sem endereço”. Observe:


              
#include 

int main()
{
        int *p1;
        int *p2(nullptr);

        if (p1 == nullptr)
                std::cout << "p1 does not points to anything" << std::endl;

        if (p2 == nullptr)
                std::cout << "p2 does not points to anything"  << std::endl;

        return 0;
}

            

A execução desse código deixa evidente que nullptr NÃO é o valor default

image

Entendido?!

Usando deslocamentos com um ponteiro

Ponteiros nos permitem “navegar” facilmente através de vetores. Considere:


              
#include 

int main()
{
        char* name = "Elemar Jr";
        int i = 0;

        while (name[i] != '\0')
                std::cout << name[i++] << std::endl; 
}

            

O que gostaria de observar:

  1. quando iniciamos um ponteiro para char (char *) apontando para uma string, estamos, de fato, criando um pequeno “array de chars” que é encerrado pelo caractere nulo (‘’);
  2. Quando usamos um ponteiro, podemos avançar “posições de memória” utilizando um deslocamento (semelhante a array).

 

Outra versão para o mesmo código:


              
#include 

int main()
{
        char* name = "Elemar Jr";
        int i = 0;

        while (*name != '\0')
                std::cout << *(name++) << std::endl; 
}

            

O que estou fazendo?! No lugar de usar um “deslocamento”, estou indicando que desejo avançar para a próxima “posição” de memória, compatível com o tipo. Para isso, usei “aritmética de ponteiros”. Entretanto, esse é tema para outro post.

image

Por hoje, era isso.

Alegre

Etiquetado:,
Posted in: Sem categoria