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;
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:
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);
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; }
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:
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!
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.