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.
- 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 umint, guarda o endereço de umint". - p
- O nome do ponteiro.
p,ptroup_pontossão nomes comuns. - = &pontos
- O valor inicial: o endereço de
pontos(o&da aula anterior). Dizemos que "paponta parapontos". - 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 empe 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ço | Nome | Tipo | O que está guardado |
|---|---|---|---|
0x7ffc8c55102c | pontos | int (4 bytes) | 10 |
0x7ffc8c551030 | p | int* (8 bytes) | 0x7ffc8c55102c → aponta para pontos |
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ê escreve | Pergunta | Resposta 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 |
* 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ímbolo | Colado 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
*p = 25;→ segue o endereço anotado empe guarda 25 lá. Quem mora lá épontos.*p + 5→ segue o endereço, lê o 25 e soma: 30.*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
| Linha | Mexe em | Efeito |
|---|---|---|
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
- "
pfoi usado sem ter sido inicializado". É só um aviso: o programa foi gerado. - note: ‘p’ was declared here
- A linha onde
pnasceu 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.
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(¬a_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ável | Uma variável que guarda um endereço |
| Analogia | Um 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ão | Sim: nullptr |
| Pode trocar de alvo? | Não | Sim: p = &y; |
| Como ler o valor | r | *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ão | Sim: p + 1 (Ponteiros e Arrays) |
| Use quando… | Quase sempre: parâmetros, for, apelidos | Precisa 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
*
#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;.
$ 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;.
$ ./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.
$ 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ódigo | O 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). |
p | O endereço guardado. |
*p | O valor que está nesse endereço (desreferenciar). |
*p = 5; | Guarda 5 na variável apontada. |
p = &y; | Faz p apontar para y. |
&p | O endereço do próprio ponteiro. |
p->campo | O 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:
2. Desreferenciar
Complete para guardar 7 na variável para onde p aponta:
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 comnullptr. - O tipo precisa combinar (
int*paraint), 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.