O que é um ponteiro em C++? Que bicho é esse?

foto do whos that pokemon com o texto O que é um ponteiro em C++? Que bicho é esse?

O que é um ponteiro em C++?

Os ponteiros são um assunto assustador para a maioria dos iniciantes na programação em C++ (e até mesmo para alguns programadores com certa experiência na linguagem), mas será que eles são realmente esse bicho que parecem ser? Acredito que não, e por isso mesmo decidi escrever esse artigo para tentar explicar-te o que são e como funcionam os ponteiros em C++.

Um ponteiro em C++ é um objeto de de tipo composto (assim como as referências) que armazena valores de endereços de outra variáveis, e permite acessá-las de maneira indireta.

Definição – Ponteiros

Um ponteiro em C++ é um objeto de tipo composto que armazena valores de endereços de outras variáveis, e permite acessá-las de maneira indireta.

Declaração de um ponteiro em C++

Para se declarar um ponteiro (daqui em diante, me referirei desta forma às variáveis que são ponteiros) é preciso utilizar o símbolo * entre o tipo do ponteiro (que deve ser o mesmo que o tipo da variável cujo endereço nele será armazenado, com algumas exeções que veremos em artigos futuros) e o seu nome. Vejamos abaixo alguns exemplos de declaração de ponteiros.

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #1
***********************************************************/

// Duas variáveis ponteiro declaradas, mas não inicializadas
int* ponteiroInt;
double* ponteiroDouble;
Dica – ponteiro para “tipo”.

Chama-se um ponteiro em C++ de um tipo qualquer de “ponteiro para o tipo”. Assim, uma variável ponteiro com tipo de base int, por exemplo, é chamada comumente de ponteiro para int.

No exemplo anterior, as variáveis ponteiro ponteiroInt e ponteiroDouble foram ambas declaradas, mas não foram inicializadas. Sim, mas e daí? Bom, ao contrário das referências que precisam ser inicializadas durante sua declaração, pois jamais poderão ser “re-ligadas” a um outro objeto, os ponteiros não possuem tal restrição por serem objetos com vida independente (ao contrário das referências), possuindo até mesmo seu próprio espaço dedicado na memória. Assim sendo, um ponteiro em C++ pode armazenar o endereço de variáveis diferentes durante sua vida no programa, podendo até mesmo guardar endereço nenhum.

Inicialização de um ponteiro em C++

Sabemos agora que um ponteiro pode ser declarado sem inicialização, mas como fazemos para inicializá-lo? Além disso, eu disse que os valores armazenados nos ponteiros são endereços de memória de outras variáveis, porém como se obtém esses endereços para inicializar os ponteiros?

Para se obter o endereço de uma variável qualquer, utiliza-se o operador & (operador de endereço, ou address operator em inglês). Sim, o mesmo símbolo utilizado para se declarar referências também é aquele utilizado para se obter endereços de variáveis: neste último caso ele é chamado de operador &, e no primeiro ele é apenas o identificador de que a variável é uma referência. Então, com a ajuda do operador & podemos inicializar um ponteiro como mostrado a seguir.

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #2
***********************************************************/

meuChar = 'a';
// Inicialização do ponteiro meuCharPtr
char* meuCharPtr = &meuChar;
Dica – usos do operador &

Para lembrar-se quando o operador & serve para obter o endereço de uma variável, basta saber que normalmente quando ele é utilizado com seu outro propósito (o de declarar uma variável de tipo referência), ele normalmente vem à esquerda de um símbolo de igualdade. Por exemplo:

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #3
***********************************************************/
using namespace std;

int notaNaProva = 10;
// & utilizado para declarar uma referência - à esquerda do =
int& refInt = notaNaProva;

string nomeDoAluno = "Carlos";
// & utilizado para se obter o endereço de memória de nomeDoAluno
cout << "Endereço de memória da variável que contém o nome " << nomeDoAluno << ": " << &nomeDoAluno << endl;
Resultado da execuçao do código acerca do uso do operador & no contexto de ponteiros em C++
Resultado da execução do código

Portanto, para se incializar um ponteiro em C++ é preciso utilizar a seguinte sintaxe: [tipo da variável alvo] * [nome do ponteiro] = &[nome da variável para a qual se deseja apontar], onde o asterisco (*) é o identificador do ponteiro, e & é operador de endereço.

Inicialização de ponteiros em uma declaração múltipla

Assim como variáveis normais podem ser declaradas juntas em uma mesma linha (para relembrar os detalhes do assunto, visite o nosso artigo sobre declaração de variáveis), um ponteiro em C++ também pode compartilhar sua declaração (e inicialização) com outros ponteiros e variáveis. Contudo, para se declarar ponteiros juntos com outras variáveis quaisquer em uma única linha, é preciso atentar a duas regras:

  • Todas as variáveis declaradas na linha devem possuir o mesmo tipo de base (int, double, string etc.);
  • Cada ponteiro precisa ter seu nome precedido do caractere especial *, caso contrário a variável será declararda uma variável do tipo de base, e não um ponteiro.

Vejamos alguns exemplos das diversas possiblidades de declaração (e inicialização) múltipla de ponteiros.

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #4
***********************************************************/

// 1) Um ponteiro para double e uma variável de tipo double (temperatura)
double* ptrTemperatura, temperatura;
// 2) Uma variável int inicializada, outra não inicializada e um ponteiro para int não inicializado (respectivamente).
int idadeLimite = 10, idadeMinima, *ptrIdade;
// 3) Três ponteiros para int inicializados.
int* primeiroPonteiro = &idadeLimite, *segundoPonteiro = primeiroPonteiro, *terceiroPonteiro = nullptr;

No exemplo acima de número 1, há uma declaração de ponteiro para double sem inicialização, e também uma declaração de variável double (pois o nome temperatura não é precedido por um << * >> ). No exemplo número dois há uma declaração de ponteiro para int que segue outras declarações de variáveis normais de tipo int: o objetivo aqui é mostrar como funcionam as declarações de ponteiros quando o ponteiro não é a primeira variável da declaração. Note também que no exemplo 2 apenas a variável idadeLimite é inicializada. O exemplo 3, todavia, requer atenção especial.

No exemplo 3, há dois casos interessantes: uma inicialização de ponteiro a partir de outro ponteiro e uma inicialização de ponteiro com valor nulo, que valem a pena ser analisados separadamente.

Inicialização de um ponteiro em C++ a partir de outro ponteiro

Na declaração *segundoPonteiro = primeiroPonteiro, inicializamos a variável segundoPonteiro com o valor armazenado em primeiroPonteiro, ou seja, colocamos em segundoPonteiro o valor do endereço de idadeLimite, como se houvéssemos feito diretamente *segundoPonteiro = &idadeLimite. Note que não há necessidade de adicionar o operador de endereço (&) em frente ao nome primeiroPonteiro, pois queremos o valor que está armazenado nele (o endereço da variável idadeLimite), e não o seu endereço.

Inicialização de um ponteiro em C++ com valor nulo

No exemplo de número 3, o ponteiro terceiroPonteiro é inicializado com o valor especial nullptr. Essa palavra-chave serve para indicar que o ponteiro foi inicializado, mas que não guarda em si nenhum endereço válido – que ele é um ponteiro nulo (ou null pointer, em inglês). Entretanto, o nullptr nem sempre existiu: ele foi introduzido no padrão C++11 como a alternativa padrão aos modos precedentes de se indicar que um ponteiro era nulo; antes, utilizavam-se os valores 0 e NULL. Assim sendo, as três expressões a seguir são equivalentes.

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #5 - exemplo de inicialização de ponteiros nulos
***********************************************************/

// Formas equivalentes de se inicializar um null pointer
double* ponteiroNulo = nullptr;
double* ponteiroNuloDois = 0;
double* ponteiroNuloTres = NULL;

Diferença entre 0, NULL e nullptr

Vendo o exemplo anterior, talvez você se pergunte para que, então, foi criado o nullptr, dado que já havim duas outras formas de se inicializar um ponteiro nulo? A vantagem do nullptr em relação ao 0 e ao NULL é que ele é um tipo especial de ponteiro que não pode ser confundido com um inteiro. Por que isto importa? Na maioria dos casos, a diferença entre as três formas de inicialização pode ser ignorada, mas ela se torna importante ao se utilizar sobrecargas de funções. Considere, por exemplo, o código a seguir: qual das duas funções será invocada em cada um dos casos, 1, 2 e 3?

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #6 - exemplo que mostra um impacto prático
* do uso do nullptr
***********************************************************/

// Versão A de funcaoSobrecarregada - parâmetro de entrada é um inteiro
void funcaoSobrecarregada(int iValor)
{...}

// Versão B de funcaoSobrecarregada - parâmetro de entrada é um ponteiro para double
void funcaoSobrecarregada(double* iValor)
{...}

// 1) chamada da função usando o valor 0
funcaoSobrecarregada(0);

// 2) chamada da função usando o valor NULL
funcaoSobrecarregada(NULL);

// 3) chamada da função usando o valor nullptr
funcaoSobrecarregada(nullptr);




  • Caso 1 – a versão A da função sobrecarregada é invocada, pois o valor 0 corresponde diretamente ao tipo do parâmetro de entrada int, enquanto que ele precisaria ser “reinterpretado” pelo compilador para tornar-se o inicializador de um ponteiro nulo.
  • Caso 2 – a versão A da função é invocada, pois a palavra-chave NULL nada mais é do que o valor 0 com outro nome. NULL é definido no header cstdlib (uma versão adaptada ao C++ do header stdlib da linguagem C) como a macro #define NULL 0, o que significa que durante a compilação, onde quer que haja a palavra NULL, ela será substituída pelo valor 0.
  • Caso 3 – a versão B de funcaoSobrecarregada é executada, pois nullptr não pode ser convertido para um inteiro (justamente para evitar problemas como este da escolha da melhor sobrecarga de funções)
Dica – prefira nullptr a 0 ou NULL.

Sempre que possível, utilize nullptr para inicializar ponteiros nulos, pois o seu uso consistente permite evitar problemas de escolha de funções sobrecarregadas (e alguns outros envolvendo templates), além de expressar mais claramente que se deseja criar um null pointer.

Ponteiros não inicializados

Assim como outras variáveis de tipos padrão, os ponteiros declarados em um escopo de bloco (fora do escopo global) que não forem inicializados permanecerão como tal, e usá-los resultará em um comportamento indefinido. Estes ponteiros “locais” que não são definidos são chamados de ponteiros inválidos: tentar acessá-los resulta normalmente em uma interrupção forçada do programa – em um crash.

Bom hábito – sempre que possível inicialize ponteiros durante sua declaração.

Um bom hábito a se adotar é o de sempre se declarar (e inicializar) os ponteiros após ter declarado a variável à qual eles apontarão. Desta forma se reduzem os riscos de utilização de um ponteiro inválido.

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #7 - exemplo de acesso a um ponteiro inválido
***********************************************************/

void funcaoX() {
    int* ponteiroInvalido;
    // A instrução abaixo tem comportamento indefinido e pode resultar em um crash
    int a = *ponteiroInvalido;
}

Como acessar objetos usando ponteiros?

Vimos no fim da seção anterior um exemplo de uso perigoso de um ponteiro não inicializado para inicializar uma variável. Entretanto – talvez os mais atentos tenham percebido a minha manobra -, utilizei um elemento do qual ainda não falei: o operador de desreferênciação * (a tradução não é das melhores; na verdade, nem é boa, mas na falta de uma melhor fiquemos com essa aí), do inglês dereference operator. Passemos a ele, então.

O operador * para ponteiros em C++

O operador * (me referirei a ele assim daqui em diante, para evitar de invocar a tradução assutadora do parágrafo anterior) utiliza o símbolo << * >>, o mesmo que é usado para se declarar ponteiros (é preciso ficar atento a essa diferença, assim como quando se utiliza o símbolo & para se declarar referências e obter endereços de objetos). Esse operador é usado para se acessar o objeto cujo endereço está armazenado no ponteiro. Ou seja, ele permite acessar o objeto para o qual o ponteiro aponta de maneira indireta (daí o fim da definição de ponteiros). A sintaxe para se utilizar o operador * é a seguinte: * [nome do ponteiro].

Veja abaixo um exemplo da utilização do operador *.

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #8 - exemplo de uso do operador *
***********************************************************/

#include <iostream>

using namespace std;

int main() {
    string meuNome = "Autor";
    // nomePtr aponta para meuNome;
    string* nomePtr = &meuNome;

    *nomePtr += " secreto";

    cout << "O meu nome é: " << meuNome << endl;
}
Resultado da execução do código: "meu nome é: Autor moderno"
Resultado da execução do código

No código do bloco anterior, o ponteiro para string nomePtr é inicializado com o endereço da variável meuNome, ou seja, ele aponta para essa variável. Em seguida, na linha 10, o operador * é utilizado para se acessar a variável meuNome através do ponteiro nomePtr: o valor daquela é modificado e a ele se adiciona a cadeia de caracteres ” secreto”. Logo, quando se imprime na tela o valor armazenado em meuNome (Iinha 12), vê-se que ele foi alterado para “Autor secreto”.

Acesso a valores de ponteiros nulos

Agora que sabemos como acessar os objetos “por trás” dos ponteiros, talvez nos perguntemos o seguinte: o que acontece se tentarmos acessar o objeto para o qual aponta um ponteiro nulo?

Como um ponteiro nulo não aponta para objeto nenhum (pois seu valor é 0 ou nullptr), tentar acessá-lo corresponde a tentar acessar uma área de memória inacessível ao programa, o que resulta no famoso erro de segmentation fault (falha de segmentação). Esse erro indica precisamente que o programa tentou acessar uma área de memória proibida. Os erros desse tipo são por vezes muito difíceis de se debugar (mesmo para programadores experientes), pois eles acontecem durante a execução do programa (ao invés de ocorrerem durante a compilação) e não dão nenhum indício do que os causou.

/***********************************************************
* O que é um ponteiro em C++
* Exemplo #9 - exemplo de acesso ao conteúdo 
* de um ponteiro nulo (segmentation fault)
***********************************************************/

double* ponteiroNulo = nullptr;
// Erro - segmentation fault
*nullptr;
Recapitulativo – classificações possíveis para ponteiros

Há quatro estados, ou categorias, segundo os quais um ponteiro em C++ pode ser classificado em um determinado ponto de um programa:

  • Ponteiros que apontam para um objeto – estes ponteiros armazenam o endereço de um objeto no programa.
  • Ponteiros nulos – estes são os ponteiros cujo valor é 0, NULL ou nullptr.
  • Ponteiros inválidos – ponteiros em escopos locais que não foram inicializados, e cuja utilização resulta em comportamento indefinido
  • Ponteiros que apontam para além de um objeto – este tipo de ponteiro, do qual não tratei aqui e abordarei em um artigo futuro, armazena o endereço de memória localizado imediatamente após o endereço de memória de um objeto no programa.

Conclusão

Neste artigo vimos o que são ponteiros em C++; como eles são declarados e inicializados; o que são ponteiros nulos e inválidos; as diferenças entre os valores 0, NULL e nullptr na inicialização de ponteiros; como acessar os objetos referidos pelos ponteiros e o operador *, além de algumas dicas e bons hábitos de programação. Espero, portanto, tê-los ajudado a compreender o que são os tão temidos ponteiros, e tê-los deixado curiosos acerca das funcionalidades um pouco mais avançadas desses objetos, como os ponteiros para void, ponteiros const e ponteiros para const, ponteiros para ponteiros, dentre outros. Esses, todavia, são assuntos para outros artigos. Até mais!

Foto de perfil de Emanoel

Sou apaixonado por tecnologia, literatura e também filosofia. O cultivo dessas paixões ao longo da minha trajetória me inspiraram a compartilhar aquilo que aprendo com os outros da maneira mais clara que eu possa. Sou formado em Engenharia Elétrica no Brasil, e também sou engenheiro formado na França. Trabalho atualmente como programador C++ em uma multinacional francesa, uma das maiores empresas de TI do mundo.

Leave a Reply

Your email address will not be published. Required fields are marked *