Ponteiros e smart pointers
Quando definimos uma variável em um programa C++, ela é alocada em uma posição de memória do computador. Um "endereço" é a localização em memória virtual de uma variável.
Apenas para fins ilustrativos, imagine que toda a memória do seu computador é uma grande tabela. Cada item da tabela possui um endereço e um valor, como no exemplo abaixo. O código de exemplo captura o endereço da variável a
e o coloca como valor da variável b
: A linha 6 cria a variável b
, do tipo int*
, e inicializa essa variável com o endereço de memória da variável a
.
#include <iostream>
int main()
{
int a = 1;
int* b = &a;
std::cout << b << std::endl;
return 0;
}
| Endereço | Valor | Variável associada |
|--- |--- |--- |
| 0x7ff0 | 1 | a |
| 0x7ff4 | 0x7ff0 | b |
Os endereços na tabela estão em hexadecimal, pois é a forma comum de representar endereços de memória. São valores meramente ilustrativos, de forma que, caso você execute o programa no seu computador, irá ter resultados diferentes.
Apesar do uso do mesmo simbolo (&
), nesse caso &a
não é uma referência. Essa distinção é importante: Caso &
esteja ao lado de um tipo (int
, double
, Point
, etc...) tem-se uma referência. Por outro lado, caso &
esteja ao lado do nome de uma variável (como no caso acima), lê-se "endereço de..." (no exemplo, "endereço de a
").
O operador *
aplicado a uma variável cujo tipo é um ponteiro, retorna o valor apontado por aquela variável, da seguinte forma:
#include <iostream>
int main()
{
int a = 1;
int* b = &a;
*b = 9;
std::cout << a << std::endl;
// Saída: 9
std::cout << b << std::endl;
// Saída: um endereço de memória (como 0x7ffcf973efb8 por exemplo)
std::cout << *b << std::endl;
// Saída: 9
return 0;
}
As variáveis criadas até agora nos exemplos desse livro são todas criadas numa região de memória denominada stack. O gerenciamento dessa memória é feito automaticamente, ou seja, as variáveis são criadas e alocadas na região da stack ao entrar em determinado escopo e, ao sair do mesmo, as variáveis são automaticamente removidas.
Outra forma de alocar memória é por meio das funções malloc
e new
. No geral, malloc
é o jeito antigo de gerenciar memória, herdado do C. Em C++, sugere-se sempre utilizar new
para alocação dinâmica de memória, ao invés de malloc
, pois para instâncias de classes o new
chama o construtor, fazendo a inicialização da variável, como será visto em capitulos posteriores.
Variáveis alocadas com new
e malloc
são colocadas em uma região de memória denominada heap. Diferente da stack, essas variáveis não são removidas automaticamente ao saírem do escopo corrente. O gerenciamento do tempo de vida desse tipo de variável deve ser feito pelo programador. O programador deve devolver a memória ao sistema operacional após término de uso da variável por meio da função delete
, como no exemplo abaixo:
#include <iostream>
int main()
{
int* a = new int(3);
std::cout << "a = " << *a << std::endl;
delete a;
return 0;
}
A linha 5 faz a alocação dinâmica de uma variável do tipo int, inicializada com valor 3, e atribui o endereço dessa variável alocada para a variável a
de tipo int*
. A linha 6 mostra o valor apontado por a
. A linha 7 deleta (devolve) a memória alocada na linha 5. Não deve-se utilizar uma variável depois que foi deletada. No caso de sistemas operacionais de propósito geral, como Windows e Linux, toda a memória alocada que não foi deletada é automaticamente devolvida no momento que o programa termina.
Gerenciar memória da heap não é um problema trivial. O mau gerenciamento da memória pode ocasionar em uma excessiva utilização da memória RAM, como é o caso dos vazamentos de memória (memory leaks), que é quando o programador "esquece" de devolver a memória.
Smart pointers são estruturas de dados que auxiliam no gerenciamento de dados alocados na heap. Basicamente, a estrutura aloca a variável na heap e, ao não ser mais utilizada, a remove automaticamente. De um modo geral, é boa prática de C++ utilizar smart pointers em abundância.
O exemplo abaixo é trivial, e apenas mostra o funcionamento da infraestrutura, mas não consegue captar por completo o valor de usar smart pointers. O uso de ponteiros e alocação dinâmica é especialmente relevante nos casos de interfaceamento com bibliotecas de baixo nível (por exemplo, funções C), ou no caso de uso de polimorfismo em programação orientada a objetos em C++, tema que será discutido posteriormente nesse livro.
#include <iostream>
#include <memory>
int main()
{
auto a = std::make_unique<int>(7);
std::cout << "a = " << *a << std::endl;
// A variável `a` será deletada automaticamente quando o escopo terminar
}