Assim como muitas outras linguagens, o C++ tem ponteiros. Você já sabe que referências e ponteiros são parecidos, mas pense neles como um nível mais próximo do funcionamento interno do seu computador. Os ponteiros são variáveis que guardam endereços de objetos. Eles são usados para interagir diretamente com objetos, o que permite a alocação dinâmica de memória e a manipulação eficiente de dados em C++.
Se você está começando com ponteiros, pode achar que eles têm algo de misterioso, mas depois que você se acostuma, são bem simples.
Eles são uma parte crucial do C++, então dedique um tempo para entendê-los de verdade. A versão mais básica, apresentada neste conceito, também é chamada de ponteiro burro ou ponteiro bruto. Com o C++ moderno, também existem os ponteiros inteligentes; o tipo básico não é nada inteligente e você precisa cuidar de todo o trabalho manualmente.
Antes de entrar nos detalhes, vale a pena entender para que servem os ponteiros. Os ponteiros são uma forma de compartilhar o endereço de um objeto com outras partes do nosso programa, o que é útil por dois motivos principais:
Uma declaração de ponteiro em C++ envolve especificar o tipo de dado para o qual o ponteiro aponta, seguido de um asterisco (*) e do nome do ponteiro.
Quando os ponteiros são declarados, eles não são inicializados automaticamente.
Sem uma atribuição explícita, um ponteiro normalmente guarda um valor indeterminado, muitas vezes chamado de "endereço de lixo".
Embora alguns compiladores possam inicializar ponteiros com nullptr, esse comportamento não é garantido em todos os compiladores, então é essencial não contar com isso.
A melhor prática é inicializar explicitamente os ponteiros brutos e verificar se eles não são nulos antes de usá-los, para evitar possíveis 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, você usa o operador de endereço (&).
A desreferenciação de um ponteiro é feita 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 desreferenciação precisa ser feita explicitamente, enquanto as referências simplesmente funcionavam como um alias.
A aritmética de ponteiros permite realizar operações aritméticas com ponteiros, o que é especialmente útil quando você trabalha com arrays. Somar um número inteiro a um ponteiro faz com que ele 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 se manifestar como resultados inesperados do programa, travamentos ou até vulnerabilidades de segurança. Um exemplo famoso das consequências do comportamento indefinido aconteceu na explosão do foguete Ariane 5, em 1996, quando uma exceção de software causada pela conversão de um número de ponto flutuante de 64 bits para um inteiro com sinal de 16 bits levou a uma falha catastrófica.
Em C++, o operador -> é usado para acessar os membros de um objeto por meio de um ponteiro para esse objeto.
É uma forma abreviada que simplifica o acesso aos membros de objetos apontados por ponteiros.
Por exemplo, se ptr é um ponteiro para um objeto com uma variável membro x, em vez de usar (*ptr).x, você pode usar diretamente ptr->x.
Esse operador melhora a legibilidade do código e reduz a verbosidade quando você trabalha com ponteiros para objetos.
Veja 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 acessar a variável membro superpower, mostrando o icônico "Lasso of Truth" 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;
Ponteiros e referências permitem o acesso indireto a objetos, mas diferem em suas capacidades e nas questões de segurança. Os ponteiros oferecem a flexibilidade de mudar o objeto para o qual apontam e podem receber valor nulo. No entanto, essa flexibilidade traz riscos, como desreferenciar ponteiros nulos ou criar ponteiros pendentes. Já as referências não podem ser nulas e ficam vinculadas a objetos válidos no momento da criação, evitando esses riscos. Como são mais seguras, as referências devem ser preferidas em relação aos ponteiros, a menos que as funcionalidades extras oferecidas pelos ponteiros sejam necessárias.
Em C++, new é usado para alocar dinamicamente memória para objetos no heap durante a execução.
Ele retorna um ponteiro para o endereço de memória alocado, permitindo que os objetos tenham tempos de vida que não ficam presos a um escopo específico.
No entanto, é importante sempre combinar new com delete para desalocar a memória corretamente.
A alocação dinâmica de memória deve ser usada com cautela para evitar problemas comuns, como vazamentos de memória e acesso a memória já desalocada.
Os ponteiros também podem receber um valor especial chamado de ponteiro nulo, que indica que eles não apontam para um endereço válido. Ponteiros nulos são comumente usados para inicializar ponteiros antes de atribuí-los a 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, você só precisa testar se os ponteiros são nulos e pode assumir que qualquer ponteiro não nulo é válido.
Quando um objeto é destruído, quaisquer ponteiros para o objeto destruído ficarão pendentes (eles não serão definidos automaticamente como nullptr).
É sua responsabilidade detectar esses casos e garantir que esses ponteiros sejam definidos como nullptr em seguida.
Em código mais antigo, você pode encontrar duas alternativas ao nullptr.
Primeiro, o literal 0 é interpretado especificamente como um valor nulo para ponteiros, embora seja o único cenário 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 seu uso seja menos comum no código C++ moderno.