C++ Ponteiros

Na aula anterior você viu que toda variável mora num endereço, o número da casa, e que &x mostra esse número. Um ponteiro é uma variável que guarda um endereço: um papelzinho com o número de uma casa anotado. Com ele, você pode ir até a casa e ver ou trocar o que tem lá dentro. Esta aula vai devagar: uma ideia por seção, com desenhos.

Você vai aprender
  • Como criar um ponteiro: int* p = &x;.
  • Como "seguir" o ponteiro até a variável: *p (desreferenciar).
  • Como mudar uma variável pelo ponteiro e como fazer o ponteiro apontar para outra.
  • Por que um ponteiro não inicializado é perigoso (e o aviso real do g++).
  • As diferenças entre ponteiro e referência, numa tabela.
  • A ideia da seta ->, para usar um ponteiro para struct.

Antes, leia Como a Memória Funciona: esta aula usa a rua de casinhas numeradas o tempo todo. Os programas foram compilados na máquina fictícia da Ana (usuária ana, computador notebook, Linux de 64 bits), na pasta ~/cpp, com g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa. Os endereços que você vai ver no seu computador serão outros: eles mudam a cada execução.


A ideia: um papel com o endereço anotado

A Ana deixou um bolo na casa da avó e quer que o Bruno vá buscar. Ela não entrega a casa para o Bruno (impossível) nem faz uma cópia da casa: ela entrega um papel com o endereço, "Rua das Flores, 52". Com o papel, o Bruno pode:

  • ler o papel: saber qual é o endereço;
  • ir até lá: chegar à casa e ver o que tem dentro;
  • mexer lá dentro: trocar o bolo por uma torta;
  • apagar e escrever outro endereço no mesmo papel.

Um ponteiro é esse papel. Cada uma dessas quatro ações tem um jeito de escrever em C++, e esta aula mostra todas.


1. Criar um ponteiro

#include <iostream>

int main() {
    int pontos = 10;
    int* p = &pontos;    // p guarda o endereço de pontos

    std::cout << "pontos  = " << pontos  << "\n";
    std::cout << "&pontos = " << &pontos << "\n";
    std::cout << "p       = " << p       << "\n";
    std::cout << "*p      = " << *p      << "\n";
    std::cout << "&p      = " << &p      << "\n";
}
int*
Leia como "ponteiro para int". O * colado no tipo quer dizer: "esta variável não guarda um int, guarda o endereço de um int".
p
O nome do ponteiro. p, ptr ou p_pontos são nomes comuns.
= &pontos
O valor inicial: o endereço de pontos (o & da aula anterior). Dizemos que "p aponta para pontos".
p
Sozinho, numa expressão, é o conteúdo do papel: o endereço.
*p
Com * na frente, numa expressão: "vá até o endereço guardado em p e pegue o que está lá". Isso se chama desreferenciar o ponteiro.
&p
O endereço do próprio ponteiro. O papel também é uma variável, e também mora numa casa.
$ g++ -std=c++23 -Wall -Wextra primeiro.cpp -o primeiro
$ ./primeiro
pontos  = 10
&pontos = 0x7ffc8c55102c       « onde pontos mora
p       = 0x7ffc8c55102c       « o que está anotado no papel: o mesmo endereço
*p      = 10                   « seguindo o papel até a casa: o valor de pontos
&p      = 0x7ffc8c551030       « onde o papel (p) mora
EndereçoNomeTipoO que está guardado
0x7ffc8c55102cpontosint (4 bytes)10
0x7ffc8c551030pint* (8 bytes)0x7ffc8c55102c → aponta para pontos
Duas variáveis, duas casas. Dentro de p não tem 10: tem o número da casa de pontos. O *p segue esse número.

Guarde as três perguntas que você pode fazer a um ponteiro:

Você escrevePerguntaResposta no exemplo
p"O que está anotado no papel?"0x7ffc8c55102c
*p"O que tem na casa anotada?"10
&p"Onde o papel está guardado?"0x7ffc8c551030
O * e o & têm dois trabalhos cada

Este é o ponto que mais confunde, então vale uma tabela só para ele. O que decide o significado é onde o símbolo aparece:

SímboloColado num tipo (numa declaração)Na frente de uma variável (numa expressão)
*int* p: "p é um ponteiro para int"*p: "o que está no endereço guardado em p"
&int& r: "r é uma referência para int" (Referências)&x: "o endereço de x"

E o * entre dois números continua sendo multiplicação: a * b.


2. Mudar a variável pelo ponteiro

Como *p é "a casa para onde p aponta", você pode escrever nela também:

#include <iostream>

int main() {
    int pontos = 10;
    int* p = &pontos;

    *p = 25;             // vai até a casa e troca o que tem lá dentro
    std::cout << "pontos = " << pontos << "\n";

    *p = *p + 5;         // lê pela seta, soma, guarda pela seta
    std::cout << "pontos = " << pontos << "\n";
}
$ g++ -std=c++23 -Wall -Wextra mudar.cpp -o mudar
$ ./mudar
pontos = 25
pontos = 30
  1. *p = 25; → segue o endereço anotado em p e guarda 25 lá. Quem mora lá é pontos.
  2. *p + 5 → segue o endereço, lê o 25 e soma: 30.
  3. *p = 30 → segue o endereço de novo e guarda 30.

Atenção ao *: sem ele, você mexe no papel, não na casa. p = 25; tentaria anotar "25" como endereço, e o g++ recusa (veja Quando dá errado).

Missão: sem escrever pontos =, use o ponteiro p para mudar pontos para 99. Rode e confira a saída pontos = 99.

#include <iostream>

int main() {
    int pontos = 10;
    int* p = &pontos;

    // mude pontos para 99 usando p

    std::cout << "pontos = " << pontos << "\n";
}
#include <iostream>

int main() {
    int pontos = 10;
    int* p = &pontos;

    *p = 99;

    std::cout << "pontos = " << pontos << "\n";
}

3. Apontar para outra variável

Aqui está a grande diferença em relação à referência: o ponteiro pode trocar de alvo. Basta anotar outro endereço no papel (atribuir sem *):

#include <iostream>

int main() {
    int ana = 7;
    int bruno = 3;
    int* p = &ana;               // p aponta para a ana

    std::cout << "*p = " << *p << "\n";

    p = &bruno;                  // agora p aponta para o bruno
    std::cout << "*p = " << *p << "\n";

    *p = 100;                    // muda o bruno
    std::cout << "ana = " << ana << ", bruno = " << bruno << "\n";
}
$ g++ -std=c++23 -Wall -Wextra alvo.cpp -o alvo
$ ./alvo
*p = 7
*p = 3
ana = 7, bruno = 100         « a ana ficou intacta
LinhaMexe emEfeito
p = &bruno;O papel (sem *)p passa a guardar o endereço do bruno.
*p = 100;A casa (com *)A casa do bruno recebe 100.

Compare com a aula Referências: lá, r = bruno; copiava o valor do bruno para a ana, porque a referência nunca troca de alvo.


4. O ponteiro não inicializado: lixo perigoso

Uma variável local que nasce sem valor guarda o que estava naquela casa antes, um "lixo" qualquer (veja Variáveis). Com um int, isso dá um número errado. Com um ponteiro, é pior: o lixo é tratado como endereço, e *p vai até uma casa desconhecida.

#include <iostream>

int main() {
    int* p;              // não inicializado: guarda um endereço qualquer
    *p = 5;              // escreve numa casa desconhecida
    std::cout << "Escrevi 5 em " << p << "\n";
}
$ g++ -std=c++23 -Wall -Wextra lixo.cpp -o lixo
lixo.cpp: In function ‘int main()’:
lixo.cpp:5:8: warning: ‘p’ is used uninitialized [-Wuninitialized]
    5 |     *p = 5;              // escreve numa casa desconhecida
      |     ~~~^~~
lixo.cpp:4:10: note: ‘p’ was declared here
    4 |     int* p;              // não inicializado: guarda um endereço qualquer
      |          ^
$ ./lixo
Falha de segmentação     (imagem do núcleo gravada) ./lixo
$ echo $?
139
warning: ‘p’ is used uninitialized
"p foi usado sem ter sido inicializado". É só um aviso: o programa foi gerado.
note: ‘p’ was declared here
A linha onde p nasceu sem valor.
Falha de segmentação
Desta vez o lixo era um endereço proibido, e o sistema matou o programa (código 139). Em inglês: Segmentation fault.

Neste caso, a queda foi a sorte. Se o lixo fosse o endereço de outra variável do programa, o 5 seria escrito em cima dela, sem erro nenhum, e o programa seguiria com um valor trocado em algum lugar. Esse tipo de defeito pode levar dias para ser encontrado. É comportamento indefinido. No editor do guia, que roda em WebAssembly, esse tipo de erro costuma passar calado, sem queda nenhuma; por isso a queda aparece só no terminal da Ana.

Regra: todo ponteiro nasce com um valor

Ou com o endereço de algo que já existe (int* p = &x;), ou com nullptr, o "papel em branco" que quer dizer "não aponta para nada": int* p = nullptr;. O nullptr tem aula própria, a próxima depois de Ponteiros e Arrays: nullptr.


5. Detalhes que pegam iniciantes

O tipo do ponteiro tem que combinar

Um double* só aceita o endereço de um double. O ponteiro precisa saber o tipo para ler o número certo de bytes na casa (4 para int, 8 para double):

#include <iostream>

int main() {
    int idade = 29;
    double* p = &idade;
    std::cout << *p << "\n";
}
$ g++ -std=c++23 -Wall -Wextra tipo.cpp -o tipo
tipo.cpp: In function ‘int main()’:
tipo.cpp:5:17: error: cannot convert ‘int*’ to ‘double*’ in initialization
    5 |     double* p = &idade;
      |                 ^~~~~~
      |                 |
      |                 int*

"Não dá para converter int* em double* na inicialização". O desenho embaixo do código mostra o tipo do que você entregou: &idade é um int*. Conserto: int* p = &idade;.

Um * por nome

Igual ao & das referências: numa declaração com vírgula, o * vale só para o primeiro nome.

#include <iostream>

int main() {
    int x = 1;
    int* a = &x, b = &x;   // b NÃO é ponteiro
    std::cout << *a << " " << b << "\n";
}
$ g++ -std=c++23 -Wall -Wextra virgula.cpp -o virgula
virgula.cpp: In function ‘int main()’:
virgula.cpp:5:22: error: invalid conversion from ‘int*’ to ‘int’ [-fpermissive]
    5 |     int* a = &x, b = &x;   // b NÃO é ponteiro
      |                      ^~
      |                      |
      |                      int*
virgula.cpp:5:22: note: possible fix: dereference with ‘*’
    5 |     int* a = &x, b = &x;   // b NÃO é ponteiro
      |                      ^~
      |                      *

O b é um int comum, e um endereço não cabe num int. A nota "possible fix" (conserto possível) sugere desreferenciar, mas aqui o conserto certo é outro: um ponteiro por linha, int* b = &x;.

Todo ponteiro tem o mesmo tamanho

Um endereço é só um número de casa, não importa quem mora lá. No Linux de 64 bits, todo ponteiro ocupa 8 bytes:

#include <iostream>

int main() {
    std::cout << "int*:    " << sizeof(int*) << " bytes\n";
    std::cout << "double*: " << sizeof(double*) << " bytes\n";
    std::cout << "char*:   " << sizeof(char*) << " bytes\n";
}
$ g++ -std=c++23 -Wall -Wextra tamanho.cpp -o tamanho
$ ./tamanho
int*:    8 bytes
double*: 8 bytes
char*:   8 bytes

O "64 bits" do nome do sistema é exatamente isso: endereços de 64 bits (8 bytes). No editor do guia, que roda em WebAssembly, os números podem ser outros.

Imprimir o endereço de um char

Como você viu em Como a Memória Funciona, std::cout << p mostra o endereço para quase todos os tipos, menos char*, que ele trata como texto. Para ver o endereço, use static_cast<void*>(p).


6. Ponteiros em funções

Antes de existirem referências (na linguagem C, avó do C++), o único jeito de uma função mudar a variável de quem a chamou era receber o endereço dela:

#include <iostream>

void dar_bonus(int* nota) {   // recebe um endereço
    *nota = *nota + 1;
}

int main() {
    int nota_ana = 8;
    dar_bonus(&nota_ana);       // manda o endereço
    std::cout << "Nota da Ana: " << nota_ana << "\n";
}
$ g++ -std=c++23 -Wall -Wextra funcao.cpp -o funcao
$ ./funcao
Nota da Ana: 9

Funciona, mas tem mais símbolos para errar (& na chamada, * em cada uso) e a função precisaria se proteger de receber um endereço inválido. Em C++, para "mudar a variável de quem chamou", prefira a referência (void dar_bonus(int& nota), veja Por Valor × Por Referência). Você vai encontrar a versão com ponteiro em código antigo e em bibliotecas escritas em C.


7. Ponteiro × referência

Referência (int& r = x;)Ponteiro (int* p = &x;)
O que éOutro nome para a mesma variávelUma variável que guarda um endereço
AnalogiaUm apelido ("Beto")Um papel com o endereço anotado
Precisa nascer ligada?Sim, sempre (erro se não)Não (mas deveria: aviso -Wuninitialized)
Pode ficar "vazio"?NãoSim: nullptr
Pode trocar de alvo?NãoSim: p = &y;
Como ler o valorr*p
Ocupa memória própria?Na prática, não aparece para vocêSim, 8 bytes (é uma variável)
Pode andar pela memória?NãoSim: p + 1 (Ponteiros e Arrays)
Use quando…Quase sempre: parâmetros, for, apelidosPrecisa de "pode não ter nada" ou de trocar o alvo; código C

Qual usar: se uma referência resolve, use referência. Ela é mais segura porque nunca está vazia e nunca muda de alvo. Ponteiros entram quando você realmente precisa dessas duas coisas. E, para memória do heap, o C++ moderno usa ponteiros inteligentes, que são ponteiros que se cuidam sozinhos.


8. A seta ->: ponteiro para struct

Uma struct junta várias informações numa variável só, e você acessa cada uma com ponto: caneta.preco. Com um ponteiro para a struct, seria preciso escrever (*p).preco: primeiro segue o ponteiro, depois pega o campo. Como isso é muito comum, existe um atalho, a seta p->preco:

#include <iostream>
#include <string>

struct Produto {
    std::string nome;
    double preco;
};

int main() {
    Produto caneta{"caneta", 2.50};
    Produto* p = &caneta;

    std::cout << (*p).nome << " custa " << (*p).preco << "\n";
    p->preco = 3.00;             // o mesmo que (*p).preco = 3.00
    std::cout << p->nome << " custa " << p->preco << "\n";
}
$ g++ -std=c++23 -Wall -Wextra seta.cpp -o seta
$ ./seta
caneta custa 2.5
caneta custa 3

A regra é curta: com a variável, ponto (caneta.preco); com um ponteiro, seta (p->preco). Os parênteses em (*p).preco são obrigatórios, porque o ponto vem antes do * na precedência. Se você usar ponto num ponteiro, o g++ até sugere a seta:

#include <iostream>
#include <string>

struct Produto {
    std::string nome;
    double preco;
};

int main() {
    Produto caneta{"caneta", 2.50};
    Produto* p = &caneta;
    std::cout << p.preco << "\n";
}
$ g++ -std=c++23 -Wall -Wextra seta-erro.cpp -o seta-erro
seta-erro.cpp: In function ‘int main()’:
seta-erro.cpp:12:20: error: request for member ‘preco’ in ‘p’, which is of pointer type ‘Produto*’ (maybe you meant to use ‘->’ ?)
   12 |     std::cout << p.preco << "\n";
      |                    ^~~~~

"Pedido do membro preco em p, que é do tipo ponteiro Produto* (talvez você quisesse usar ->?)". Você vai usar muito a seta com ponteiros inteligentes e com this.


Quando dá errado

"invalid conversion from ‘int’ to ‘int*’": esqueceu o *
#include <iostream>

int main() {
    int pontos = 10;
    int* p = &pontos;
    p = 20;
    std::cout << pontos << "\n";
}
$ g++ -std=c++23 -Wall -Wextra esqueceu.cpp -o esqueceu
esqueceu.cpp: In function ‘int main()’:
esqueceu.cpp:6:9: error: invalid conversion from ‘int’ to ‘int*’ [-fpermissive]
    6 |     p = 20;
      |         ^~
      |         |
      |         int

O que aconteceu: sem o *, p = 20 tenta anotar "20" no papel, como se fosse um endereço. O C++ não deixa transformar um número comum em endereço.

Como resolver: para mudar a variável apontada, use *p = 20;. Para apontar para outra variável, use p = &outra;.

"cannot convert ‘int*’ to ‘double*’"
$ g++ -std=c++23 -Wall -Wextra tipo.cpp -o tipo
tipo.cpp: In function ‘int main()’:
tipo.cpp:5:17: error: cannot convert ‘int*’ to ‘double*’ in initialization
    5 |     double* p = &idade;
      |                 ^~~~~~
      |                 |
      |                 int*

O que aconteceu: o tipo do ponteiro não combina com o tipo da variável.

Como resolver: declare o ponteiro com o mesmo tipo da variável (int* para int). Se precisa do valor como double, converta o valor: double d = idade;.

"‘p’ is used uninitialized" e o programa cai
$ ./lixo
Falha de segmentação     (imagem do núcleo gravada) ./lixo
$ echo $?
139

O que aconteceu: o ponteiro foi usado sem receber um endereço. O compilador avisou com -Wuninitialized.

Como resolver: inicialize sempre: int* p = &x; ou int* p = nullptr; e confira antes de usar (nullptr). Nunca ignore esse aviso.

"request for member … which is of pointer type"
$ g++ -std=c++23 -Wall -Wextra seta-erro.cpp -o seta-erro
seta-erro.cpp: In function ‘int main()’:
seta-erro.cpp:12:20: error: request for member ‘preco’ in ‘p’, which is of pointer type ‘Produto*’ (maybe you meant to use ‘->’ ?)
   12 |     std::cout << p.preco << "\n";
      |                    ^~~~~

O que aconteceu: você usou ponto com um ponteiro.

Como resolver: troque por seta: p->preco.


Referência rápida

CódigoO que faz
int* p = &x;Cria p, ponteiro para int, apontando para x.
int* p = nullptr;Cria um ponteiro que não aponta para nada (nullptr).
pO endereço guardado.
*pO valor que está nesse endereço (desreferenciar).
*p = 5;Guarda 5 na variável apontada.
p = &y;Faz p apontar para y.
&pO endereço do próprio ponteiro.
p->campoO mesmo que (*p).campo, para ponteiro de struct ou classe.
sizeof(int*)8 bytes num sistema de 64 bits (qualquer tipo de ponteiro).
static_cast<void*>(p)Mostra o endereço de um char* em vez do texto.

Exercícios

1. Declarar

Complete para criar p, um ponteiro para double que aponta para altura:

double p = altura;

2. Desreferenciar

Complete para guardar 7 na variável para onde p aponta:

= 7;

3. Depois de int a = 1, b = 2; int* p = &a; p = &b; *p = 9;, quanto valem a e b?

4. Com int x = 5; int* p = &x;, o que mostra std::cout << p;?

5. Qual destas afirmações é verdadeira?

Missão final: um ponteiro, dois cofres

Missão: a Ana tem dois cofres. Usando só o ponteiro p (sem escrever cofre1 = nem cofre2 =), deposite 10 no cofre1, depois faça p apontar para o cofre2 e deposite 20 nele. A saída deve ser:

cofre1 = 60
cofre2 = 120
#include <iostream>

int main() {
    int cofre1 = 50;
    int cofre2 = 100;
    int* p = &cofre1;

    // deposite 10 no cofre1 usando p

    // faça p apontar para o cofre2

    // deposite 20 no cofre2 usando p

    std::cout << "cofre1 = " << cofre1 << "\n";
    std::cout << "cofre2 = " << cofre2 << "\n";
}
#include <iostream>

int main() {
    int cofre1 = 50;
    int cofre2 = 100;
    int* p = &cofre1;

    *p = *p + 10;
    p = &cofre2;
    *p = *p + 20;

    std::cout << "cofre1 = " << cofre1 << "\n";
    std::cout << "cofre2 = " << cofre2 << "\n";
}

Resumo

  • Um ponteiro é uma variável que guarda um endereço: int* p = &x;.
  • p é o endereço; *p é o valor que está lá (desreferenciar); *p = 5; muda a variável apontada.
  • p = &y; troca o alvo. A referência não pode fazer isso.
  • Ponteiro sem valor guarda lixo: usar é comportamento indefinido (-Wuninitialized, falha de segmentação). Inicialize sempre, nem que seja com nullptr.
  • O tipo precisa combinar (int* para int), e o * vale para um nome só.
  • Com ponteiro de struct, use a seta: p->preco.
  • Prefira referências; use ponteiros quando precisar de "nada" ou de trocar o alvo. Próxima aula: Ponteiros e Arrays.