Capítulo 21 — Lifetime: existir não é a mesma coisa que estar acessível
Série: Do zero ao jogo comercial com C++ e SFML
Escopo nos ensinou onde um nome pode ser utilizado. Agora precisamos entender durante quanto tempo o objeto associado a esse nome realmente existe.
No capítulo anterior estudamos escopo.
Aprendemos que uma variável como:
int playerHealth = 100;
não fica automaticamente disponível em todo o programa.
Seu nome pertence a determinado contexto.
Vimos exemplos como:
if (playerCoins < itemPrice)
{
int missingCoins = itemPrice - playerCoins;
std::cout << missingCoins << std::endl;
}
Dentro do bloco:
missingCoins
pode ser utilizado.
Fora dele, não.
Também vimos que funções possuem seus próprios nomes locais:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
O newHealth dessa função não pode ser utilizado diretamente dentro de main().
Naquele capítulo nossa pergunta principal foi:
Onde esse nome pode ser encontrado e utilizado?
Agora precisamos fazer outra pergunta:
Durante quanto tempo o objeto associado a esse nome realmente existe durante a execução do programa?
Essa pergunta nos leva a um conceito central de C++:
lifetime, ou tempo de vida.
1. Escopo e lifetime não são a mesma coisa
Vamos começar pela distinção mais importante do capítulo.
De maneira simplificada:
SCOPE
↓
onde um nome pode ser utilizado
Enquanto:
LIFETIME
↓
durante qual período da execução
o objeto existe
Esses conceitos frequentemente caminham juntos em exemplos simples.
Mas eles não são sinônimos.
Essa diferença será fundamental quando chegarmos a:
referências
ponteiros
objetos
construtores
destrutores
RAII
memória dinâmica
recursos
Por enquanto começaremos com variáveis locais simples.
2. Começando pelo exemplo mais simples
Considere:
#include <iostream>
int main()
{
int playerCoins = 10;
std::cout << playerCoins << std::endl;
return 0;
}
Temos:
int playerCoins = 10;
Durante a execução de main(), chega um momento em que esse objeto é criado e inicializado.
Podemos representar:
entramos em main()
↓
playerCoins é inicializado
↓
playerCoins existe
↓
utilizamos playerCoins
↓
saímos de main()
↓
playerCoins deixa de existir
Para esse exemplo simples, escopo e lifetime parecem quase a mesma coisa.
Mas precisamos observar melhor.
3. O objeto não existia antes da declaração
Considere:
int main()
{
std::cout << "Before" << std::endl;
int playerCoins = 10;
std::cout << playerCoins << std::endl;
return 0;
}
Conceitualmente:
entra em main()
↓
"Before"
↓
playerCoins é criado e inicializado
↓
playerCoins existe
↓
playerCoins é utilizado
Antes da inicialização:
playerCoins ainda não possui seu lifetime iniciado
Para nossos tipos simples, podemos pensar didaticamente:
a vida útil começa quando o objeto é criado e sua inicialização é concluída.
Mais tarde veremos detalhes mais precisos para tipos complexos.
4. Saindo do bloco
Agora:
#include <iostream>
int main()
{
std::cout << "Before block" << std::endl;
{
int reward = 10;
std::cout << "Reward: " << reward << std::endl;
}
std::cout << "After block" << std::endl;
return 0;
}
Temos:
Before block
Reward: 10
After block
Conceitualmente:
entra no bloco
↓
reward é criado
↓
reward existe
↓
reward é utilizado
↓
sai do bloco
↓
lifetime de reward termina
Para essa variável local simples, sair do bloco encerra sua vida útil.
5. O nome deixa de estar disponível e o objeto deixa de existir
No exemplo:
{
int reward = 10;
}
depois das chaves não podemos fazer:
std::cout << reward << std::endl;
No capítulo anterior explicamos isso pelo escopo.
Agora adicionamos outra informação.
Para essa variável local comum, ao sair do bloco:
o nome deixa de estar no escopo
+
o objeto também encerra seu lifetime
Nos exemplos simples que estamos utilizando, os dois acontecimentos coincidem.
Mas não devemos concluir que isso sempre acontecerá em qualquer situação de C++.
6. Por que insistir nessa diferença?
Porque existem situações em que um objeto pode continuar existindo mesmo quando determinada forma de acessá-lo ficou escondida.
Considere o shadowing que vimos:
#include <iostream>
int main()
{
int value = 10;
{
int value = 20;
std::cout << value << std::endl;
}
std::cout << value << std::endl;
return 0;
}
Resultado:
20
10
Durante o bloco interno, a variável externa com valor 10 não deixou de existir.
Ela apenas ficou ocultada pelo nome interno naquela resolução simples de value.
Esse é um excelente exemplo de que:
estar inacessível por determinado nome naquele ponto não significa necessariamente ter deixado de existir.
7. Visualizando o shadowing com lifetime
Podemos imaginar:
value externo nasce
value externo = 10
│
│ entra no bloco
│ ↓
│ value interno nasce
│ value interno = 20
│ ↓
│ "value" encontra o interno
│ ↓
│ value interno morre
│
"value" volta a encontrar o externo
│
value externo continua existindo
│
fim de main
↓
value externo morre
O objeto externo permaneceu vivo durante todo o bloco interno.
Apenas não era o objeto encontrado quando escrevíamos:
value
naquele contexto.
Aqui a diferença entre visibilidade do nome e lifetime do objeto fica muito mais clara.
8. Lifetime de variáveis locais
Nos exemplos que utilizamos até agora:
int playerHealth = 100;
float playerSpeed = 4.5f;
bool isRunning = true;
são variáveis locais simples.
Quando são criadas dentro de um bloco, normalmente podemos pensar:
entrada na região apropriada
↓
execução chega à declaração
↓
objeto é inicializado
↓
lifetime começa
↓
objeto é utilizado
↓
saímos do bloco
↓
lifetime termina
Essa é a primeira regra mental que precisamos dominar.
9. Variável dentro de if
Considere:
if (playerHealth <= 0)
{
int penalty = 10;
playerCoins -= penalty;
}
O que acontece se a condição for falsa?
O bloco nem é executado.
Então:
int penalty = 10;
não é alcançado.
Nesse caminho de execução, aquele objeto local não chega a ser criado.
Temos:
condição false
↓
bloco não executa
↓
penalty não nasce
10. E quando a condição é verdadeira?
Se:
playerHealth <= 0
for verdadeiro:
entra no bloco
↓
penalty nasce
↓
penalty = 10
↓
é utilizado
↓
sai do bloco
↓
penalty morre
Lifetime depende do caminho real que a execução percorre.
Isso é importante.
O código fonte define possibilidades.
A execução concreta determina quais objetos realmente são criados naquele momento.
11. Variáveis dentro de loops
Agora considere:
for (int frame = 1; frame <= 3; frame++)
{
int reward = 5;
std::cout << reward << std::endl;
}
Temos três iterações.
Uma pergunta interessante:
existe um único
rewarddurante todo o loop?
Para a variável local declarada dentro do corpo:
int reward = 5;
podemos pensar que a cada iteração a execução entra novamente naquele bloco e cria um novo objeto local para aquela iteração.
12. Visualizando as iterações
Temos:
ITERAÇÃO 1
↓
reward nasce
reward = 5
↓
usa reward
↓
reward morre
ITERAÇÃO 2
↓
novo reward nasce
reward = 5
↓
usa reward
↓
reward morre
ITERAÇÃO 3
↓
novo reward nasce
reward = 5
↓
usa reward
↓
reward morre
Isso será extremamente importante quando começarmos a trabalhar com objetos que executam comportamento durante construção e destruição.
Por enquanto estamos usando int, então o processo não é visível no console.
13. Podemos tornar a criação conceitualmente visível
Vamos adicionar mensagens:
#include <iostream>
int main()
{
for (int frame = 1; frame <= 3; frame++)
{
std::cout << "Iteration " << frame << " started." << std::endl;
int reward = frame * 10;
std::cout << "Reward: " << reward << std::endl;
std::cout << "Iteration " << frame << " finished." << std::endl;
}
return 0;
}
Resultado:
Iteration 1 started.
Reward: 10
Iteration 1 finished.
Iteration 2 started.
Reward: 20
Iteration 2 finished.
Iteration 3 started.
Reward: 30
Iteration 3 finished.
Conceitualmente, cada reward pertence àquela iteração.
14. O contador do for possui outro lifetime
Observe:
for (int frame = 1; frame <= 3; frame++)
{
int reward = frame * 10;
}
O:
frame
não é criado novamente da mesma maneira no início de cada execução do corpo.
Ele controla a estrutura inteira do for.
Já:
reward
é declarado dentro do corpo.
Temos lifetimes diferentes dentro da mesma estrutura.
Conceitualmente:
frame nasce
│
├── iteração 1
│ └── reward nasce e morre
│
├── iteração 2
│ └── reward nasce e morre
│
├── iteração 3
│ └── reward nasce e morre
│
frame morre ao terminar o for
15. Parâmetros também possuem lifetime
Considere:
void ShowPlayerHealth(int playerHealth)
{
std::cout << playerHealth << std::endl;
}
Quando fazemos:
ShowPlayerHealth(100);
o parâmetro:
playerHealth
passa a existir para aquela chamada da função.
Podemos pensar:
chamada começa
↓
parâmetro é inicializado
↓
corpo executa
↓
função termina
↓
parâmetro encerra seu lifetime
16. Cada chamada possui sua própria execução
Imagine:
ShowPlayerHealth(100);
ShowPlayerHealth(50);
ShowPlayerHealth(25);
Não existe um único parâmetro playerHealth sendo permanentemente alterado entre todas essas chamadas.
Cada chamada estabelece seu próprio contexto de execução.
Conceitualmente:
CALL 1
playerHealth = 100
↓
termina
CALL 2
playerHealth = 50
↓
termina
CALL 3
playerHealth = 25
↓
termina
Isso ajuda a compreender por que funções podem ser reutilizadas com valores diferentes.
17. Variáveis locais da função também vivem durante a chamada
Considere:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
Durante uma chamada:
CalculateHealthAfterDamage(100, 20);
podemos visualizar:
chamada começa
↓
playerHealth nasce com 100
damage nasce com 20
↓
newHealth nasce com 80
↓
return newHealth
↓
newHealth encerra seu lifetime
playerHealth encerra seu lifetime
damage encerra seu lifetime
↓
chamada termina
Mas aí aparece uma pergunta importante.
Se:
newHealth
morre quando a função termina...
como conseguimos receber o valor 80 fora dela?
18. Retornar uma variável local por valor é válido
Temos:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
E:
int playerHealth = CalculateHealthAfterDamage(100, 20);
Isso é válido.
Por quê?
Porque estamos retornando o valor.
Não estamos pedindo ao chamador para continuar utilizando o objeto local newHealth depois que ele deixou de existir.
Conceitualmente:
newHealth = 80
↓
return devolve o valor 80
↓
resultado da chamada recebe esse valor
↓
objeto local newHealth encerra seu lifetime
19. O nome local não sobrevive, mas o valor retornado pode continuar
Essa distinção é extremamente importante.
Dentro da função:
int newHealth = 80;
Depois:
return newHealth;
Fora da função:
int playerHealth = CalculateHealthAfterDamage(...);
Não estamos transportando para fora o nome:
newHealth
Estamos produzindo um resultado.
O chamador possui seu próprio objeto:
playerHealth
inicializado com o valor devolvido.
20. Não precisamos imaginar uma cópia física obrigatória em todos os casos
Didaticamente podemos pensar:
valor sai da função
↓
é entregue ao chamador
Em C++ moderno, o compilador pode realizar otimizações que evitam cópias intermediárias em diferentes situações.
Não precisamos entrar agora em:
copy elision
move semantics
RVO
NRVO
Esses assuntos aparecerão muito mais tarde.
A regra útil para agora é:
retornar um valor local por valor é uma operação normal e segura.
21. Isso será diferente de referências e ponteiros
Futuramente poderemos encontrar construções que tentam fornecer acesso ao objeto local em si, em vez de simplesmente devolver seu valor.
Aí lifetime se tornará crítico.
Por exemplo, existirá uma grande diferença entre:
devolver o valor
e:
devolver alguma forma de acesso ao objeto que está prestes a morrer
Ainda não estudamos referências ou ponteiros.
Não vamos antecipar a sintaxe.
Mas já podemos entender o risco conceitual.
22. Uma analogia simples
Imagine uma função como uma pequena sala temporária.
Dentro dela existe:
newHealth
escrito em um quadro.
Antes de sair da sala, alguém anota o número:
80
e entrega essa informação para você.
Depois a sala é fechada.
O quadro desaparece.
Mas o número que você recebeu continua com você.
Isso representa razoavelmente bem o retorno por valor no nosso estágio atual.
23. Escopo e lifetime em uma função
Considere:
int CalculateReward(int baseCoins, int bonusCoins)
{
int reward = baseCoins + bonusCoins;
return reward;
}
Temos:
baseCoins
bonusCoins
reward
Scope
Esses nomes são utilizados dentro do contexto apropriado da função.
Lifetime
Para cada chamada, os objetos correspondentes existem durante seus respectivos períodos dentro daquela execução.
Quando a chamada termina, os locais automáticos deixam de existir.
O valor retornado pode ser usado pelo chamador.
24. Variáveis externas vivem mais que variáveis internas
Considere:
int main()
{
int playerCoins = 0;
if (true)
{
int reward = 10;
playerCoins += reward;
}
std::cout << playerCoins << std::endl;
return 0;
}
Temos:
playerCoins nasce
│
│ reward nasce
│ playerCoins += reward
│ reward morre
│
playerCoins continua existindo
│
playerCoins é mostrado
│
playerCoins morre
O objeto externo possui lifetime maior.
25. Isso explica por que podemos modificar playerCoins usando um valor temporário
Dentro:
{
int reward = 10;
playerCoins += reward;
}
Depois que:
reward
deixa de existir, o efeito da operação:
playerCoins += reward;
já aconteceu.
playerCoins possui seu próprio lifetime e continua com:
10
Não existe dependência contínua entre os dois objetos.
26. O resultado de uma expressão não precisa continuar "ligado" às entradas
Considere:
int reward = 10;
playerCoins += reward;
Se depois reward desaparece:
playerCoins
não volta automaticamente ao valor anterior.
Já calculamos e armazenamos um novo estado.
Isso parece óbvio agora, mas será importante quando começarmos a diferenciar valores de referências a objetos.
27. Lifetime e nosso mini Game Loop
Voltemos ao código:
while (isRunning)
{
int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
playerCoins += coinsReward;
}
O:
playerCoins
foi criado antes do loop.
Então permanece existindo durante as diversas iterações.
Já:
coinsReward
é criado dentro do corpo do loop.
Temos conceitualmente:
playerCoins nasce
│
├── frame 1
│ └── coinsReward nasce e morre
│
├── frame 2
│ └── coinsReward nasce e morre
│
├── frame 3
│ └── coinsReward nasce e morre
│
...
│
loop termina
│
playerCoins continua
│
fim de main
↓
playerCoins morre
Isso é uma diferença real de lifetime.
28. Estado persistente versus estado temporário
Dentro de nosso mini Game Loop já podemos identificar duas categorias didáticas.
Estado que precisa atravessar várias iterações
int playerHealth = 100;
int playerCoins = 0;
int frame = 0;
Essas variáveis precisam continuar existindo entre uma passagem e outra pelo loop.
Estado temporário de uma iteração
int coinsReward = CalculateCoinsReward(...);
Esse valor só precisa existir durante aquela atualização.
A posição da declaração ajuda a representar essa intenção.
29. Isso começa a influenciar design
Se declarássemos:
int coinsReward = 0;
while (isRunning)
{
coinsReward = CalculateCoinsReward(...);
playerCoins += coinsReward;
}
funcionaria.
Mas agora coinsReward vive em uma região maior do que o necessário.
Se ele só representa um resultado temporário da iteração, faz mais sentido:
while (isRunning)
{
int coinsReward = CalculateCoinsReward(...);
playerCoins += coinsReward;
}
Escopo e lifetime menores podem comunicar melhor a responsabilidade.
30. Não mantenha objetos vivos sem necessidade
Esse princípio ficará muito mais importante quando nossos objetos deixarem de ser apenas:
int
float
bool
Imagine futuramente objetos que possuam:
texturas
arquivos
buffers
áudio
conexões
memória
Manter algo vivo significa potencialmente manter recursos associados a ele vivos também.
Ainda não chegamos lá.
Mas a base conceitual começa aqui:
um objeto deveria viver pelo período necessário para cumprir sua responsabilidade.
31. Lifetime será central para RAII
Mais tarde estudaremos uma das ideias mais importantes de C++:
RAII
Resource Acquisition Is Initialization.
Sem antecipar o capítulo, a ideia dependerá profundamente de sabermos:
quando um objeto nasce?
quando ele morre?
o que acontece nesses momentos?
Construtores e destrutores se conectarão diretamente ao lifetime.
Por isso estamos estudando esse conceito antes de memória avançada.
32. Objetos simples escondem parte do processo
Quando temos:
int value = 10;
não vemos nenhuma mensagem aparecer quando o objeto começa ou termina sua vida.
Então lifetime pode parecer abstrato.
Mais tarde, uma classe poderá executar código em:
construção
destruição
e o momento ficará observável.
Por enquanto precisamos construir o modelo mental sem depender disso.
33. Um bloco artificial para controlar lifetime
C++ permite criar um bloco simplesmente com:
{
}
Por exemplo:
#include <iostream>
int main()
{
std::cout << "Before" << std::endl;
{
int temporaryValue = 10;
std::cout << temporaryValue << std::endl;
}
std::cout << "After" << std::endl;
return 0;
}
Neste caso as chaves não pertencem a:
if
while
for
Ainda assim criam uma região de bloco.
Isso pode ser usado para limitar escopo e lifetime.
34. Não vamos criar blocos desnecessários
Só porque aprendemos que:
{
}
pode limitar lifetime, não significa que devemos escrever:
{
int a = 10;
}
{
int b = 20;
}
sem motivo.
Assim como em toda a série:
nova ferramenta
≠
usar em todo lugar
Utilize um bloco adicional quando houver uma razão real de organização ou controle de lifetime.
35. O caso da variável declarada em if
Observe esta variação:
if (playerHealth <= 0)
{
int penalty = 10;
playerCoins -= penalty;
}
Talvez você pense:
se
penaltysó existe durante o bloco, por que não escrever diretamente10?
Poderíamos:
playerCoins -= 10;
Mas:
int penalty = 10;
pode dar significado ao valor.
A decisão não deve ser baseada apenas no lifetime.
Também pensamos em:
clareza
intenção
legibilidade
36. Lifetime de um parâmetro por valor
Vamos observar novamente:
void ShowPlayerHealth(int playerHealth)
{
std::cout << playerHealth << std::endl;
}
Com:
int playerHealth = 100;
ShowPlayerHealth(playerHealth);
Temos dois objetos diferentes para nosso modelo didático:
playerHealth de main
↓
continua existindo antes, durante e depois da chamada
Enquanto:
parâmetro playerHealth
↓
existe durante aquela chamada
Quando a função termina, o parâmetro termina seu lifetime.
O original em main() continua vivo.
37. Isso reforça a passagem por valor
Podemos representar:
MAIN
playerHealth = 100
│
│ continua vivo
│
├──────────────┐
│ │
│ FUNCTION CALL
│ parâmetro nasce com 100
│ ↓
│ função executa
│ ↓
│ parâmetro morre
│
playerHealth original continua 100
│
fim de main
↓
original morre
Isso explica visualmente o comportamento observado no Capítulo 18.
38. E quando retornamos um valor?
Agora:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
e:
playerHealth = CalculateHealthAfterDamage(playerHealth, 20);
Temos:
original = 100
│
├── chamada começa
│ ├── parâmetro playerHealth nasce com 100
│ ├── damage nasce com 20
│ ├── newHealth nasce com 80
│ ├── valor 80 é retornado
│ └── locais encerram lifetime
│
resultado 80 é atribuído ao playerHealth original
│
original agora = 80
O fluxo começa a ficar muito mais claro.
39. Erro proposital — usar variável após o bloco
Teste:
#include <iostream>
int main()
{
{
int reward = 10;
std::cout << reward << std::endl;
}
std::cout << reward << std::endl;
return 0;
}
Faça Build.
O compilador rejeita o segundo uso por causa do escopo.
Ao mesmo tempo, para nossa variável local simples, também sabemos que o objeto já terminou seu lifetime ao sair do bloco.
Temos dois problemas conceitualmente relacionados no mesmo ponto.
40. Erro proposital — variável de uma iteração
Teste:
#include <iostream>
int main()
{
for (int frame = 1; frame <= 3; frame++)
{
int reward = frame * 10;
std::cout << reward << std::endl;
}
std::cout << reward << std::endl;
return 0;
}
Novamente, o nome não está disponível depois do loop.
Além disso, não existe um único reward local do corpo que permaneceu vivo depois de todas as iterações.
Cada iteração criou seu objeto temporário.
41. Erro conceitual — querer que uma variável temporária preserve estado entre frames
Imagine:
while (isRunning)
{
int playerCoins = 0;
playerCoins++;
std::cout << playerCoins << std::endl;
}
Talvez você espere:
1
2
3
4
...
Mas a variável é criada dentro do corpo a cada iteração e começa novamente com:
0
Então veremos repetidamente:
1
1
1
1
...
Encontramos um bug relacionado ao posicionamento e ao lifetime do estado.
42. Estado que precisa persistir deve nascer fora da iteração
Se queremos acumular:
int playerCoins = 0;
while (isRunning)
{
playerCoins++;
std::cout << playerCoins << std::endl;
// condição de saída...
}
Agora:
playerCoins
vive durante todas as iterações.
A cada passagem:
0 → 1 → 2 → 3 → ...
Esse é um exemplo extremamente importante para Game Engineering.
43. O estado do jogo precisa sobreviver entre frames
Imagine futuramente:
Player Position
Health
Inventory
Current Scene
Game Time
Se esses estados fossem recriados do zero a cada iteração do Game Loop, perderíamos a evolução anterior.
O próprio conceito de jogo depende de determinadas informações continuarem existindo entre frames.
Isso conecta diretamente lifetime a estado.
44. Nem todo estado precisa sobreviver
Ao mesmo tempo, um cálculo temporário como:
int coinsReward = CalculateCoinsReward(...);
não precisa sobreviver.
Temos então uma primeira distinção arquitetural:
PERSISTENT GAME STATE
↓
precisa atravessar atualizações
versus:
TEMPORARY CALCULATION STATE
↓
só precisa existir durante uma operação
Essa distinção crescerá muito ao longo do projeto.
45. Código final do capítulo
Vamos tornar essa diferença observável:
#include <iostream>
int CalculateHealthAfterDamage(int playerHealth, int damage);
int CalculateCoinsReward(int baseCoins, int bonusCoins);
void ShowPlayerState(int frame, int playerHealth, int playerCoins);
int main()
{
const int TOTAL_FRAMES = 3;
const int DAMAGE_PER_FRAME = 20;
const int BASE_COINS_REWARD = 1;
const int BONUS_COINS_REWARD = 2;
int playerHealth = 100;
int playerCoins = 0;
std::cout << "=== LIFETIME SIMULATION START ===" << std::endl;
for (int frame = 1; frame <= TOTAL_FRAMES; frame++)
{
int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
playerCoins += coinsReward;
ShowPlayerState(frame, playerHealth, playerCoins);
}
std::cout << "=== LIFETIME SIMULATION FINISHED ===" << std::endl;
std::cout << "Final Health: " << playerHealth << std::endl;
std::cout << "Final Coins: " << playerCoins << std::endl;
return 0;
}
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
if (newHealth < 0)
{
newHealth = 0;
}
return newHealth;
}
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
int reward = baseCoins + bonusCoins;
return reward;
}
void ShowPlayerState(int frame, int playerHealth, int playerCoins)
{
std::cout << "Frame: " << frame << std::endl;
std::cout << "Health: " << playerHealth << std::endl;
std::cout << "Coins: " << playerCoins << std::endl;
std::cout << std::endl;
}
Resultado esperado:
=== LIFETIME SIMULATION START ===
Frame: 1
Health: 80
Coins: 3
Frame: 2
Health: 60
Coins: 6
Frame: 3
Health: 40
Coins: 9
=== LIFETIME SIMULATION FINISHED ===
Final Health: 40
Final Coins: 9
46. Mapeando os lifetimes do código final
Antes do loop:
int playerHealth = 100;
int playerCoins = 0;
Esses objetos precisam persistir durante toda a simulação.
Conceitualmente:
playerHealth nasce
playerCoins nasce
│
├── frame 1
├── frame 2
├── frame 3
│
simulação termina
│
ainda podem ser utilizados no resumo
│
fim de main
↓
ambos encerram lifetime
47. Lifetime do frame
Temos:
for (int frame = 1; frame <= TOTAL_FRAMES; frame++)
O contador pertence à execução daquela estrutura de repetição.
Depois que o for termina, não precisamos dele.
Por isso seu lifetime fica naturalmente associado ao loop.
48. Lifetime de coinsReward
Dentro:
{
int coinsReward = CalculateCoinsReward(...);
}
temos:
Frame 1
↓
coinsReward nasce com 3
↓
é usado
↓
morre
Frame 2
↓
outro coinsReward nasce com 3
↓
é usado
↓
morre
Frame 3
↓
outro coinsReward nasce com 3
↓
é usado
↓
morre
Não precisamos preservar coinsReward entre frames.
49. Lifetime dentro de CalculateCoinsReward
A cada chamada:
CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD)
a função cria seu próprio contexto.
Temos:
baseCoins
bonusCoins
reward
durante aquela execução.
Depois:
return reward;
o valor é devolvido.
Os locais da função encerram seus lifetimes.
O chamador continua com o resultado.
50. Isso começa a criar um modelo mental de execução
Agora conseguimos olhar para:
int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
e imaginar:
chamada
↓
parâmetros nascem
↓
reward nasce
↓
resultado é calculado
↓
valor retorna
↓
locais da função morrem
↓
coinsReward no chamador é inicializado
↓
chamador continua
Esse modelo será extremamente útil nos próximos níveis de C++.
51. Teste: colocando estado persistente no lugar errado
Faça uma experiência temporária.
Mova:
int playerCoins = 0;
para dentro do loop:
for (int frame = 1; frame <= TOTAL_FRAMES; frame++)
{
int playerCoins = 0;
playerCoins += CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
std::cout << playerCoins << std::endl;
}
Esperado:
3
3
3
Em vez de:
3
6
9
Explique por quê.
A variável está sendo recriada em cada iteração.
52. Teste: variável temporária fora do loop
Agora faça o inverso.
Coloque:
int coinsReward = 0;
antes do loop.
Dentro:
coinsReward = CalculateCoinsReward(...);
O comportamento pode continuar correto.
Mas pergunte:
coinsRewardprecisa realmente continuar existindo depois de cada iteração?
Se não, mantê-lo fora aumenta seu lifetime sem necessidade.
A questão não é apenas:
funciona?
É também:
esse lifetime representa corretamente a responsabilidade?
53. Testando funções repetidas
Faça:
int rewardA = CalculateCoinsReward(1, 2);
int rewardB = CalculateCoinsReward(10, 5);
Temos duas chamadas diferentes.
Durante cada chamada existem seus próprios:
baseCoins
bonusCoins
reward
Resultado:
rewardA = 3
rewardB = 15
As variáveis locais de uma chamada não permanecem sendo reutilizadas pela próxima.
54. Boas práticas que já podemos aplicar
Neste estágio podemos adotar alguns princípios simples:
declare objetos o mais perto possível de onde sua responsabilidade começa;
não mantenha temporários vivos por regiões maiores sem necessidade;
estados que precisam atravessar iterações devem existir fora do corpo que é recriado a cada repetição;
parâmetros por valor vivem durante a chamada correspondente;
variáveis locais de função pertencem àquela execução;
retornar um valor local por valor é normal;
não confunda invisibilidade de um nome com inexistência de um objeto;
não crie acesso de longa duração a objetos locais sem compreender profundamente lifetime.
Esse último ponto será especialmente importante quando chegarmos a referências e ponteiros.
55. O que ainda não estamos estudando
Lifetime em C++ é um assunto muito maior.
Ainda veremos:
automatic storage duration
static storage duration
dynamic storage duration
temporaries
references
pointers
constructors
destructors
RAII
smart pointers
ownership
move semantics
containers
resource lifetime
Não vamos colocar tudo no Capítulo 21.
Nosso objetivo atual é construir a base correta.
56. Uma precisão importante: lifetime não é simplesmente "estar alocado"
Às vezes uma explicação simplificada diz:
lifetime é o tempo em que a memória existe.
Isso pode ser enganoso.
Memória de armazenamento e lifetime de um objeto são conceitos relacionados, mas não idênticos.
Para o nível atual, prefira:
Lifetime é o período durante a execução em que um objeto existe como aquele objeto.
Mais tarde estudaremos armazenamento e memória com muito mais precisão.
57. Outra precisão: fim de escopo e fim de lifetime nem sempre coincidem
Nos exemplos de variáveis locais automáticas simples:
{
int value = 10;
}
o fim do bloco também encerra o lifetime daquele objeto.
Mas não transforme isso em:
scope acabou
=
qualquer objeto necessariamente morreu
como regra universal da linguagem.
Já vimos um exemplo simples de separação conceitual com shadowing:
int value = 10;
{
int value = 20;
}
O objeto externo continuou vivo mesmo enquanto seu nome simples estava ocultado.
Mais tarde veremos casos ainda mais interessantes.
58. Lifetime é especialmente importante porque C++ permite controle fino
Em algumas linguagens muitos detalhes de gerenciamento são mais escondidos.
C++ nos oferece controle muito maior sobre:
objetos
memória
recursos
destruição
ownership
Esse poder exige que entendamos bem:
quem existe?
onde?
até quando?
É uma das razões pelas quais lifetime será um tema recorrente durante toda a série.
59. Relação com Game Engineering
Imagine futuramente:
Texture
SoundBuffer
Window
Scene
Player
Enemy
Particle
Projectile
UI Element
File
Network Connection
Todos esses objetos terão:
momento de criação
↓
período de uso
↓
momento de destruição
Se destruirmos cedo demais:
algo ainda pode precisar deles
Se mantivermos vivos por tempo demais:
podemos conservar recursos desnecessariamente
Se alguém tentar acessar um objeto depois do fim de seu lifetime:
podemos entrar em bugs muito perigosos
Hoje estamos apenas construindo o vocabulário para compreender isso.
60. Nosso Game Loop já depende de lifetime correto
Veja:
int playerHealth = 100;
while (isRunning)
{
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
}
playerHealth precisa sobreviver às várias iterações.
Se fosse recriado dentro:
while (isRunning)
{
int playerHealth = 100;
playerHealth = CalculateHealthAfterDamage(playerHealth, damage);
}
cada frame começaria novamente com:
100
Nosso gameplay quebraria.
Lifetime não é apenas teoria de linguagem.
Ele já afeta o comportamento do jogo.
61. Checklist de compreensão
Antes de avançar, tente responder sem consultar o artigo:
O que é lifetime?
Qual diferença existe entre lifetime e escopo?
Quando uma variável local simples começa a existir?
O que acontece quando saímos de seu bloco?
Uma variável declarada dentro de um
ifexiste quando o bloco não é executado?O que acontece com uma variável declarada dentro do corpo de um loop a cada iteração?
O contador do
fore uma variável dentro do corpo possuem exatamente o mesmo lifetime?Quanto tempo vive um parâmetro por valor?
Quanto tempo vive uma variável local de função?
Como podemos retornar o valor de uma variável local se ela morre ao final da função?
O objeto local retornado continua existindo fora da função?
Por que um estado como
playerCoinsprecisa existir fora do corpo do Game Loop?Por que um valor temporário como
coinsRewardpode existir apenas durante uma iteração?Como shadowing mostra que estar inacessível por um nome não significa necessariamente deixar de existir?
Por que lifetime será importante para referências e ponteiros?
Se consegue responder essas perguntas com suas próprias palavras, construímos a base necessária.
62. Estado atual da nossa jornada
Terminamos uma sequência muito importante de fundamentos:
Capítulo 17
Funções
↓
comportamentos nomeados
Capítulo 18
Parâmetros
↓
dados entram
Capítulo 19
Valores de retorno
↓
resultados saem
Capítulo 20
Escopo
↓
onde nomes podem ser utilizados
Capítulo 21
Lifetime
↓
por quanto tempo objetos existem
Isso nos dá uma base muito mais sólida para continuar evoluindo em C++.
63. Estrutura do projeto
Ainda permanecemos:
CppGameEngine/
└── main.cpp
Isso continua proposital.
Ainda não precisamos criar arquitetura artificial.
Mas agora nosso programa possui:
funções
estado persistente
temporários
parâmetros
retornos
escopos
lifetimes diferentes
Nosso modelo mental está ficando preparado para estruturas de dados mais interessantes.
64. Git
Depois de validar os exercícios:
git status
Adicione:
git add CppGameEngine/main.cpp
Ajuste o caminho se sua estrutura local for diferente.
Commit sugerido:
git commit -m "refactor: clarify object lifetime in simulation"
A ideia principal deste commit é tornar evidente a diferença entre:
estado persistente
e:
temporários de cada iteração
65. Atualizando o CHANGELOG
Em [Unreleased]:
### Refactored
- Kept persistent player state outside repeated simulation scopes.
- Limited temporary reward values to individual loop iterations.
- Clarified the lifetime of local calculation state.
### Added
- Added examples of local object lifetime inside blocks.
- Added loop iteration lifetime demonstrations.
- Added parameter lifetime examples.
- Added return-by-value lifetime explanations.
- Added scope versus lifetime comparison exercises.
Depois:
git add CHANGELOG.md
git commit -m "docs: update changelog with lifetime concepts"
O ROADMAP técnico ainda não precisa ser alterado neste ponto.
66. O que aprendemos?
No capítulo anterior perguntamos:
Onde esse nome pode ser utilizado?
Hoje perguntamos:
Durante quanto tempo esse objeto existe?
Aprendemos que:
scope e lifetime são conceitos relacionados, mas diferentes;
scope trata da região em que um nome pode ser utilizado;
lifetime trata do período durante a execução em que um objeto existe;
objetos locais simples normalmente iniciam seu lifetime quando são criados e inicializados;
sair do bloco encerra o lifetime de variáveis locais automáticas comuns daquele bloco;
um bloco que não é executado não cria seus objetos locais;
objetos locais no corpo de um loop podem nascer e morrer a cada iteração;
o contador do
forpode possuir lifetime diferente dos temporários de seu corpo;parâmetros por valor pertencem à execução daquela chamada;
variáveis locais de função encerram seu lifetime quando a execução apropriada termina;
retornar o valor de uma variável local por valor é seguro;
o nome local não sobrevive, mas o resultado retornado pode continuar no chamador;
shadowing mostra que um objeto pode continuar vivo mesmo quando outro nome esconde seu acesso simples;
estado que precisa persistir entre frames precisa possuir lifetime compatível com essa necessidade;
temporários não precisam permanecer vivos além de sua responsabilidade;
lifetime será essencial para referências, ponteiros, objetos, RAII e gerenciamento de recursos.
Conclusão
Começamos nossa jornada com uma linha aparentemente simples:
int playerHealth = 100;
Durante muitos capítulos nossa preocupação principal era o valor:
100
Depois começamos a perguntar:
Qual é o tipo?
Pode mudar?
Onde pode ser acessado?
Como vai para uma função?
Como volta?
Agora adicionamos uma pergunta ainda mais importante:
Quando esse objeto começa a existir e quando sua existência termina?
Isso muda nossa forma de enxergar código.
Considere:
while (isRunning)
{
int coinsReward = CalculateCoinsReward(...);
playerCoins += coinsReward;
}
Agora enxergamos duas histórias diferentes.
Uma:
playerCoins
↓
vive durante várias iterações
↓
acumula estado
Outra:
coinsReward
↓
nasce em uma iteração
↓
é usado
↓
deixa de existir
Essa diferença é uma das bases de qualquer aplicação de jogo.
O mundo do jogo precisa preservar determinados estados:
Health
Coins
Position
Inventory
Current State
ao longo do tempo.
Enquanto milhares de cálculos temporários podem surgir e desaparecer continuamente.
Compreender isso agora evitará muita confusão quando chegarmos à parte de C++ em que lifetime deixa de ser apenas um conceito didático e passa a determinar se o programa é seguro ou possui bugs difíceis de reproduzir.
Com esse capítulo encerramos nossa primeira grande fase de fundamentos.
Agora estamos prontos para começar a lidar com dados de maneira mais estruturada.
Até agora trabalhamos com textos apenas de forma pontual:
std::cout << "Player defeated." << std::endl;
Mas um jogo precisa armazenar texto.
Por exemplo:
Player Name
NPC Name
Item Name
Dialogue
Location
Quest Description
Messages
Precisamos representar esses textos como dados do programa.
E esse será nosso próximo passo.
Próximo capítulo
Capítulo 22 — Strings e texto
Série: Do zero ao jogo comercial com C++ e SFML
No próximo artigo iniciaremos a Fase 2 — Dados e Estruturas Fundamentais.
Partiremos de textos que até agora aparecem diretamente no código:
std::cout << "Player defeated." << std::endl;
e evoluiremos para informações armazenadas:
std::string playerName = "Alex";
Mas não vamos apenas apresentar std::string como uma linha para copiar.
Vamos entender:
por que texto precisa de uma representação própria;
diferença inicial entre um caractere e uma sequência de caracteres;
por que
"Alex"e'A'não representam a mesma coisa;como incluir
<string>;como declarar e inicializar
std::string;atribuição de novos textos;
concatenação;
tamanho de uma string;
strings vazias;
comparação básica;
como texto começará a representar nomes e mensagens do nosso futuro jogo;
e por que precisamos compreender strings antes de avançar para coleções como arrays,
std::arrayestd::vector.
Até agora nossos principais estados foram numéricos ou booleanos.
No próximo capítulo nosso programa começará a armazenar e transformar texto como dado real.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!