Tal como muitas outras linguagens, o C++ tem ponteiros. Já sabes que as referências e os ponteiros são semelhantes, mas pensa neles como estando um nível mais próximo do funcionamento interno do teu computador. Os ponteiros são variáveis que guardam endereços de objetos. São usados para interagir diretamente com objetos, permitindo a alocação dinâmica de memória e uma manipulação eficiente de dados em C++.
Se estás a começar com os ponteiros, podem parecer um pouco misteriosos, mas depois de te habituares a eles são bastante simples.
São uma parte fundamental do C++, por isso dedica algum tempo a compreendê-los mesmo bem. A versão mais básica apresentada neste conceito também é chamada de dumb pointer ou raw pointer. Com o C++ moderno existem também os smart pointers; o tipo básico não é nada inteligente e tens de fazer todo o trabalho manualmente.
Antes de entrares nos detalhes, vale a pena perceber para que servem os ponteiros. Os ponteiros são uma forma de partilhar o endereço de um objeto com outras partes do nosso programa, o que é útil por duas grandes razões:
Uma declaração de ponteiro em C++ implica especificar o tipo de dados para o qual o ponteiro aponta, seguido de um asterisco (*) e do nome do ponteiro.
Quando os ponteiros são declarados, não são inicializados automaticamente.
Sem uma atribuição explícita, um ponteiro guarda habitualmente um valor indeterminado, muitas vezes designado por «endereço de lixo».
Embora alguns compiladores possam inicializar os ponteiros a nullptr, esse comportamento não é garantido em todos os compiladores, por isso é essencial não contar com ele.
A boa prática é inicializar explicitamente os raw pointers e verificar se não são nulos antes de os utilizares, para evitar potenciais problemas.
int* ptr{nullptr}; // Declares a pointer and makes sure it is not invalid
Para atribuir o endereço de uma variável a um ponteiro, usas o operador de endereço (&).
A derreferenciação de um ponteiro faz-se com o operador de indireção (*).
std::string opponent{"Solomon Lane"};
// 'ethan' points to the address of the string opponent
std::string* ethan{&opponent};
// Instead of ethan's, the opponent's name address is given to the passPort
std::string passportName{*ethan};
Atenção: a derreferenciação tem de ser feita explicitamente, enquanto as referências funcionavam simplesmente como um alias.
A aritmética de ponteiros permite-te efetuar operações aritméticas sobre ponteiros, o que é especialmente útil quando trabalhas com arrays. Somar um número inteiro a um ponteiro faz com que este aponte para um elemento diferente.
// Stargate Coordinate Code
int gateCode[] = {462, 753, 218, 611, 977};
// 'ptr' points to the first element of 'gateCode'
int* ptr{&gateCode[0]};
// Accesses the third Stargate address through pointer arithmetic
int dialedAddress{*(ptr + 2)};
// Chevron encoded! Dialing Stargate address:
openStarGate(dialedAddress);
A aritmética de ponteiros em C++ pode facilmente levar a comportamento indefinido se não for tratada com cuidado. O comportamento indefinido pode manifestar-se em resultados inesperados do programa, falhas ou até vulnerabilidades de segurança. Um exemplo famoso das consequências do comportamento indefinido ocorreu na explosão do foguetão Ariane 5, em 1996, em que uma exceção de software causada pela conversão de um número de vírgula flutuante de 64 bits num inteiro com sinal de 16 bits levou a uma falha catastrófica.
Em C++, o operador -> é usado para aceder aos membros de um objeto através de um ponteiro para esse objeto.
É uma forma abreviada que simplifica o acesso aos membros de objetos apontados por ponteiros.
Por exemplo, se ptr for um ponteiro para um objeto com uma variável membro x, em vez de usares (*ptr).x podes usar diretamente ptr->x.
Este operador melhora a legibilidade do código e reduz a verbosidade quando trabalhas com ponteiros para objetos.
Eis um exemplo breve, com uma struct Superhero que tem uma variável membro superpower.
A função main cria um ponteiro dianaPrince para um objeto Superhero (que representa a Mulher-Maravilha).
O operador -> é usado para aceder à variável membro superpower, mostrando o icónico «Laço da Verdade» da Mulher-Maravilha.
struct Superhero {
std::string superpower;
};
Superhero wonder_woman{};
Superhero* dianaPrince = &wonder_woman;
dianaPrince->superpower = "Lasso of Truth";
// Using the -> operator to access member variable superpower:
std::cout << "Wonder Woman, possesses the mighty " << dianaPrince->superpower;
// Memory cleanup:
delete dianaPrince;
Tanto os ponteiros como as referências permitem o acesso indireto a objetos, mas diferem nas suas capacidades e nas considerações de segurança. Os ponteiros oferecem a flexibilidade de mudar o objeto para o qual apontam e podem receber o valor nulo. No entanto, essa flexibilidade introduz riscos, como derreferenciar ponteiros nulos ou criar ponteiros pendentes. As referências, por outro lado, não podem ser nulas e ficam ligadas a objetos válidos no momento da criação, o que evita esses riscos. Dada a sua natureza mais segura, deves preferir as referências aos ponteiros, a menos que as funcionalidades adicionais que os ponteiros oferecem sejam necessárias.
Em C++, o new é usado para alocar dinamicamente memória para objetos na heap durante o tempo de execução.
Devolve um ponteiro para o endereço de memória alocado, permitindo que os objetos tenham tempos de vida que não estão dependentes de um âmbito específico.
No entanto, é importante emparelhar sempre o new com o delete para libertar corretamente a memória.
A alocação dinâmica de memória deve ser usada com cautela, para evitar problemas comuns como fugas de memória e o acesso a memória já libertada.
Os ponteiros também podem receber um valor especial chamado ponteiro nulo, que indica que não apontam para um endereço válido. Os ponteiros nulos são habitualmente usados para inicializar ponteiros antes de lhes atribuir valores válidos.
// There is a dark mark bearer, but who is it?
std::string* darkMarkBearer{nullptr};
// It is Bellatrix!
darkMarkBearer = new std::string{"Bellatrix Lestrange"};
// She has been defeated
delete darkMarkBearer;
// Reset pointer to null, symbolizing there is no more darkMarkBearer at the moment
darkMarkBearer = nullptr;
Um ponteiro deve conter o endereço de um objeto válido ou ser definido como nullptr.
Assim, só precisas de testar se os ponteiros são nulos e podes assumir que qualquer ponteiro não nulo é válido.
Quando um objeto é destruído, todos os ponteiros para o objeto destruído ficam pendentes (não são automaticamente definidos como nullptr).
É tua responsabilidade detetar esses casos e garantir que esses ponteiros são posteriormente definidos como nullptr.
Em código mais antigo, podes encontrar duas alternativas ao nullptr.
Primeiro, o literal 0 é interpretado especificamente como um valor nulo para ponteiros, embora seja o único caso em que um literal inteiro pode ser atribuído a um ponteiro.
Segundo, a preprocessor macro NULL, herdada do C e definida no cabeçalho <cstddef>, é outra representação de um ponteiro nulo, embora o seu uso seja menos comum no código C++ moderno.