Capítulo 20 — Escopo: onde uma variável realmente existe
Série: Do zero ao jogo comercial com C++ e SFML
Nossas funções já recebem dados e devolvem resultados. Agora precisamos entender onde cada nome pode realmente ser utilizado.
No capítulo anterior nossas funções finalmente aprenderam a devolver resultados.
Criamos:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
if (newHealth < 0)
{
newHealth = 0;
}
return newHealth;
}
E utilizamos:
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
Também criamos:
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
Agora observe algo interessante.
Temos:
int main()
{
int playerHealth = 100;
}
e também:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
}
Nos dois lugares existe um identificador chamado:
playerHealth
Eles representam a mesma variável?
Não.
Por que uma função não consegue simplesmente acessar todas as variáveis declaradas dentro de main()?
Por que uma variável criada dentro de um if não pode ser utilizada depois das chaves?
Por que podemos reutilizar alguns nomes em regiões diferentes do programa?
Essas perguntas nos levam a um conceito fundamental:
escopo.
1. Começando pelo problema
Considere:
#include <iostream>
int main()
{
if (true)
{
int playerCoins = 50;
std::cout << "Coins: " << playerCoins << std::endl;
}
std::cout << "Coins: " << playerCoins << std::endl;
return 0;
}
A primeira utilização funciona:
std::cout << playerCoins;
porque está dentro do mesmo bloco onde a variável foi declarada.
Mas a segunda:
std::cout << playerCoins;
está fora daquele bloco.
Faça Build.
O compilador reclamará.
Por quê?
Porque o nome:
playerCoins
não está disponível naquele ponto.
2. O que é escopo?
Para nosso estágio atual, podemos utilizar esta definição:
Escopo é a região do programa em que determinado nome pode ser encontrado e utilizado.
Se declaramos:
{
int playerCoins = 50;
}
o nome:
playerCoins
está disponível dentro daquele contexto apropriado.
Depois que saímos dele, tentar utilizar esse nome daquela declaração deixa de ser válido.
Neste capítulo nosso foco será principalmente:
onde o nome pode ser usado?
No próximo falaremos mais profundamente sobre:
por quanto tempo o objeto existe?
Essas ideias estão relacionadas, mas não são exatamente a mesma coisa.
3. As chaves começam a ganhar outro significado
Até aqui utilizamos:
{
}
em vários lugares:
int main()
{
}
if (condition)
{
}
while (condition)
{
}
for (...)
{
}
int CalculateSomething()
{
}
As chaves delimitam blocos de código.
E blocos frequentemente introduzem escopos.
Isso significa que uma variável declarada dentro de determinado bloco pode ficar restrita àquele contexto.
4. Escopo dentro de main()
Considere:
int main()
{
int playerHealth = 100;
int playerCoins = 25;
std::cout << playerHealth << std::endl;
std::cout << playerCoins << std::endl;
return 0;
}
As variáveis:
playerHealth
playerCoins
estão declaradas dentro do corpo de:
main()
Então podemos utilizá-las posteriormente naquele escopo, respeitando as regras da linguagem e a posição das declarações.
5. Uma variável precisa ter sido declarada antes do uso
Isto funciona:
int playerHealth = 100;
std::cout << playerHealth << std::endl;
Mas isto não:
std::cout << playerHealth << std::endl;
int playerHealth = 100;
Quando o compilador chega à primeira linha, ainda não encontrou uma declaração apropriada para:
playerHealth
Escopo não significa apenas estar dentro das mesmas chaves.
A posição da declaração também importa.
6. Escopo dentro de if
Considere:
int main()
{
int playerCoins = 25;
if (playerCoins > 0)
{
int bonusCoins = 10;
std::cout << "Bonus: " << bonusCoins << std::endl;
}
return 0;
}
bonusCoins foi declarada dentro:
if (...)
{
}
Então podemos utilizá-la ali.
Agora tente:
std::cout << bonusCoins << std::endl;
depois do if.
Não funciona.
O nome pertence ao escopo interno daquele bloco.
7. Visualizando os escopos
Podemos imaginar:
main
│
├── playerCoins
│
└── if
│
└── bonusCoins
Dentro do if, podemos enxergar:
bonusCoins
e também valores visíveis do escopo externo, como:
playerCoins
Mas fora do if, o nome interno:
bonusCoins
não fica disponível.
8. Escopos podem ser aninhados
Observe:
int main()
{
int playerHealth = 100;
if (playerHealth > 0)
{
int damage = 20;
if (damage > 10)
{
int penalty = 5;
std::cout << playerHealth << std::endl;
std::cout << damage << std::endl;
std::cout << penalty << std::endl;
}
}
return 0;
}
Temos vários níveis.
Conceitualmente:
main
│
├── playerHealth
│
└── if
│
├── damage
│
└── if
│
└── penalty
O escopo mais interno consegue utilizar nomes disponíveis dos escopos externos apropriados.
Mas o contrário não acontece automaticamente.
9. De fora para dentro não é igual a de dentro para fora
Dentro do if mais interno:
std::cout << playerHealth << std::endl;
std::cout << damage << std::endl;
std::cout << penalty << std::endl;
os três nomes estão disponíveis.
Mas depois que saímos dele:
std::cout << penalty << std::endl;
não funciona.
E depois que saímos do primeiro if:
std::cout << damage << std::endl;
também não funciona.
O escopo limita onde cada nome é visível.
10. Escopo em while
Considere:
int frame = 0;
while (frame < 5)
{
int frameCoins = 3;
std::cout << frameCoins << std::endl;
frame++;
}
A variável:
frame
foi declarada fora do loop.
Por isso está disponível dentro dele.
Mas:
frameCoins
foi declarada dentro do bloco do while.
Depois do loop:
std::cout << frameCoins << std::endl;
não é válido.
11. Escopo em for
Temos:
for (int day = 1; day <= 5; day++)
{
std::cout << "Day " << day << std::endl;
}
Aqui:
int day = 1
é declarado na própria estrutura do for.
O contador pertence ao contexto daquele loop.
Depois:
std::cout << day << std::endl;
não deve ser utilizado esperando acessar aquele mesmo day.
Esse é um dos motivos pelos quais declarar o contador dentro do for é tão conveniente.
12. O contador fica perto de onde é necessário
Compare:
int day = 1;
for (; day <= 5; day++)
{
}
com:
for (int day = 1; day <= 5; day++)
{
}
Na segunda versão, comunicamos:
dayexiste para controlar este loop.
Não precisamos deixar esse nome visível em uma região maior do que o necessário.
Isso nos leva a uma boa prática importante:
prefira o menor escopo que ainda permita implementar a responsabilidade com clareza.
13. Por que reduzir o escopo?
Imagine:
int temporaryValue = 10;
// 200 linhas de código...
std::cout << temporaryValue << std::endl;
Durante toda essa região o nome:
temporaryValue
está potencialmente disponível.
Se ele só fosse necessário dentro de um pequeno cálculo, manter o nome disponível por uma região tão grande aumenta a quantidade de estado que precisamos acompanhar mentalmente.
Escopos menores podem tornar o código mais previsível.
14. Declarar perto do uso
Considere:
int missingCoins = 0;
// várias operações...
if (playerCoins < itemPrice)
{
missingCoins = itemPrice - playerCoins;
std::cout << missingCoins << std::endl;
}
Se missingCoins só existe para aquele caminho, podemos declarar:
if (playerCoins < itemPrice)
{
int missingCoins = itemPrice - playerCoins;
std::cout << missingCoins << std::endl;
}
Agora o nome existe exatamente na região em que faz sentido.
15. Escopo de função
Cada função possui seu próprio contexto.
Observe:
void ShowPlayerHealth(int playerHealth)
{
std::cout << playerHealth << std::endl;
}
O parâmetro:
playerHealth
é um nome disponível dentro da função.
Agora temos:
int main()
{
int playerHealth = 100;
ShowPlayerHealth(playerHealth);
return 0;
}
Também existe um:
playerHealth
em main().
São identificadores iguais em escopos diferentes.
16. Os dois playerHealth não são a mesma variável
Temos:
int main()
{
int playerHealth = 100;
}
e:
void ShowPlayerHealth(int playerHealth)
{
}
O parâmetro da função recebe um valor quando ela é chamada.
Não estamos criando um acesso mágico à variável de main().
Conceitualmente:
main::playerHealth
│
│ argumento
▼
ShowPlayerHealth(...)
│
▼
parâmetro local chamado playerHealth
Essa distinção explica parte do comportamento que observamos no capítulo anterior.
17. Funções diferentes podem reutilizar o mesmo nome
Podemos ter:
void ShowPlayerHealth(int playerHealth)
{
std::cout << playerHealth << std::endl;
}
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
Cada função possui seu próprio parâmetro chamado:
playerHealth
Esses nomes não entram em conflito apenas porque são iguais.
Eles vivem em contextos diferentes.
18. Variáveis locais em funções
Considere:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
Dentro dessa função temos:
playerHealth
damage
newHealth
Depois que voltamos para main(), não podemos fazer:
std::cout << newHealth << std::endl;
porque o nome:
newHealth
pertence ao escopo da função.
19. Isso é bom
Talvez pareça inicialmente uma limitação.
Mas imagine se qualquer variável interna de qualquer função pudesse ser acessada livremente de qualquer lugar.
Teríamos um programa em que detalhes internos ficariam expostos para todos.
O escopo ajuda a criar fronteiras.
Uma função pode possuir detalhes internos como:
int newHealth
sem obrigar o resto do programa a conhecer esse nome.
20. O retorno é a forma explícita de enviar o resultado para fora
Temos:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
Embora:
newHealth
não seja visível em main(), seu valor pode sair da função por:
return newHealth;
Então:
int playerHealth = 100;
playerHealth = CalculateHealthAfterDamage(playerHealth, 25);
O chamador recebe o valor.
Não precisa acessar diretamente a variável interna.
21. Escopo ajuda a esconder detalhes de implementação
Observe:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
if (newHealth < 0)
{
newHealth = 0;
}
return newHealth;
}
Quem chama precisa saber:
CalculateHealthAfterDamage(playerHealth, damage);
Mas não precisa manipular diretamente:
newHealth
Esse nome é um detalhe interno da função.
Esse é um primeiro contato muito simples com uma ideia poderosa:
diferentes partes do programa podem possuir responsabilidades e detalhes internos próprios.
22. Sombreamento de nomes
Agora chegamos a um comportamento que pode causar bastante confusão.
Considere:
int value = 10;
{
int value = 20;
std::cout << value << std::endl;
}
Dentro do bloco interno existe uma nova variável também chamada:
value
Ela pode ocultar o nome externo naquele contexto.
Esse fenômeno é chamado de shadowing, ou sombreamento.
23. Observando o shadowing
Programa:
#include <iostream>
int main()
{
int value = 10;
std::cout << "Outer: " << value << std::endl;
{
int value = 20;
std::cout << "Inner: " << value << std::endl;
}
std::cout << "Outer Again: " << value << std::endl;
return 0;
}
Resultado:
Outer: 10
Inner: 20
Outer Again: 10
Temos duas variáveis diferentes.
24. Visualizando o shadowing
Podemos pensar:
escopo externo
value = 10
┌───────────────────┐
│ escopo interno │
│ │
│ value = 20 │
│ │
│ "value" encontra │
│ o nome interno │
└───────────────────┘
fora novamente
↓
value = 10
A variável externa não foi alterada apenas porque criamos outra com o mesmo nome.
25. Shadowing é sempre errado?
A linguagem permite determinados casos de sombreamento.
Mas isso não significa que devemos utilizá-los indiscriminadamente.
Compare:
int playerCoins = 50;
if (condition)
{
int playerCoins = 10;
}
Agora precisamos perguntar:
qual playerCoins estou usando?
Código assim pode aumentar o risco de confusão.
Em nossos exemplos, evitaremos sombreamento acidental.
26. Parâmetros podem ter nomes iguais aos argumentos porque estão em escopos diferentes
Isto é diferente:
int playerHealth = 100;
ShowPlayerHealth(playerHealth);
com:
void ShowPlayerHealth(int playerHealth)
{
}
Nesse caso, usar o mesmo nome pode ser bastante natural.
O parâmetro representa o mesmo conceito:
playerHealth
dentro da função.
Não estamos criando arbitrariamente uma segunda variável concorrente dentro do mesmo fluxo local.
O contexto deixa a intenção clara.
27. Erro proposital — variável do if usada fora
Teste:
#include <iostream>
int main()
{
int playerCoins = 30;
int itemPrice = 50;
if (playerCoins < itemPrice)
{
int missingCoins = itemPrice - playerCoins;
std::cout << "Missing: " << missingCoins << std::endl;
}
std::cout << missingCoins << std::endl;
return 0;
}
Faça Build.
Observe o diagnóstico.
Depois remova o uso externo.
A variável pertence ao contexto em que foi declarada.
28. Erro proposital — variável do for
Teste:
#include <iostream>
int main()
{
for (int day = 1; day <= 5; day++)
{
std::cout << "Day " << day << std::endl;
}
std::cout << day << std::endl;
return 0;
}
Compile.
O day declarado no for não está disponível dessa forma depois do loop.
Isso é exatamente o comportamento que queremos para um contador que pertence apenas à repetição.
29. Erro proposital — variável de outra função
Considere:
int CalculateReward()
{
int reward = 10;
return reward;
}
int main()
{
std::cout << reward << std::endl;
return 0;
}
reward é um detalhe local de:
CalculateReward()
O main() precisa utilizar:
int reward = CalculateReward();
ou:
std::cout << CalculateReward() << std::endl;
O valor pode ser devolvido.
O nome interno não precisa escapar.
30. O problema das variáveis globais
Existe uma maneira aparentemente fácil de tornar um nome disponível em muitos lugares:
int playerHealth = 100;
void ApplyDamage()
{
playerHealth -= 20;
}
void ShowHealth()
{
std::cout << playerHealth << std::endl;
}
Agora várias funções conseguem acessar:
playerHealth
porque a variável foi declarada fora delas.
Essa é uma variável em escopo global.
Parece conveniente.
Mas traz custos.
31. Por que não queremos tornar tudo global?
Considere:
int playerHealth = 100;
int playerCoins = 0;
bool isRunning = true;
GameState currentState = GameState::Menu;
Agora qualquer função apropriada nesse contexto poderia potencialmente depender desses valores.
Quando vemos:
Update();
não sabemos imediatamente se ela modifica:
Health?
Coins?
State?
isRunning?
tudo?
As dependências ficaram menos explícitas.
32. Globais aumentam o estado compartilhado
Quanto mais partes do programa conseguem alterar um mesmo valor, mais precisamos raciocinar sobre:
quem pode modificar?
quando?
em qual ordem?
por quê?
Em sistemas pequenos isso pode parecer simples.
Conforme o código cresce, pode aumentar bastante a complexidade.
Por isso nossa estratégia continuará sendo:
não criar variáveis globais apenas para evitar parâmetros e retornos.
33. Isso significa que globais nunca existem em software?
Não.
Existem usos legítimos para diferentes formas de estado e objetos com armazenamento mais amplo.
C++ também possui conceitos relacionados a:
namespace scope
static storage
constantes
objetos globais
Mas não precisamos transformar este capítulo em uma discussão completa sobre storage duration e linkage.
Nosso objetivo atual é muito mais direto:
compreender os limites de visibilidade dos nomes que estamos utilizando.
34. const também respeita escopo
Considere:
int main()
{
const int MAX_PLAYER_HEALTH = 100;
std::cout << MAX_PLAYER_HEALTH << std::endl;
return 0;
}
A constante foi declarada dentro de main().
Ela não se torna automaticamente global apenas por utilizar:
const
Constância responde principalmente a:
o valor pode ser modificado por esse nome?
Escopo responde a:
onde esse nome pode ser utilizado?
São conceitos diferentes.
35. enum class também possui escopo
Já escrevemos:
enum class GameState
{
Menu,
Playing,
GameOver
};
E utilizamos:
GameState::Playing
O:
::
já nos mostrava que nomes podem pertencer a determinados contextos.
Playing está qualificado pelo tipo:
GameState
Nosso estudo de escopo começa a conectar várias ideias que já apareceram na série.
36. Um exemplo integrado
Vamos montar:
#include <iostream>
int CalculateHealthAfterDamage(int playerHealth, int damage);
void ShowPlayerHealth(int playerHealth);
int main()
{
const int DAMAGE = 25;
int playerHealth = 100;
if (playerHealth > 0)
{
int healthBeforeDamage = playerHealth;
std::cout << "Before Damage: " << healthBeforeDamage << std::endl;
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE);
}
ShowPlayerHealth(playerHealth);
return 0;
}
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
if (newHealth < 0)
{
newHealth = 0;
}
return newHealth;
}
void ShowPlayerHealth(int playerHealth)
{
std::cout << "Current Health: " << playerHealth << std::endl;
}
Resultado:
Before Damage: 100
Current Health: 75
Agora podemos mapear os escopos.
37. Mapa de escopo do exemplo
Temos:
main
│
├── DAMAGE
├── playerHealth
│
└── if
│
└── healthBeforeDamage
Depois:
CalculateHealthAfterDamage
│
├── playerHealth
├── damage
└── newHealth
E:
ShowPlayerHealth
│
└── playerHealth
Temos três identificadores diferentes chamados:
playerHealth
em contextos distintos.
38. O nome igual não cria compartilhamento automático
Isso merece repetição.
int main()
{
int playerHealth = 100;
}
não significa que:
void ShowPlayerHealth(int playerHealth)
está diretamente utilizando a variável original.
O parâmetro recebe um valor durante a chamada.
A existência do mesmo nome ajuda semanticamente, mas não remove a fronteira entre os escopos.
39. Microdesafio — escopo do if
Crie:
int playerCoins = 20;
int itemPrice = 50;
Dentro:
if (playerCoins < itemPrice)
{
int missingCoins = itemPrice - playerCoins;
std::cout << missingCoins << std::endl;
}
Depois tente utilizar:
missingCoins
fora do bloco.
Observe o erro.
Explique:
o nome foi declarado dentro do bloco do
if.
40. Microdesafio — escopo do for
Crie:
for (int frame = 1; frame <= 3; frame++)
{
std::cout << frame << std::endl;
}
Depois tente:
std::cout << frame << std::endl;
fora.
Investigue o diagnóstico e depois remova o acesso inválido.
41. Microdesafio — shadowing
Execute:
#include <iostream>
int main()
{
int value = 10;
std::cout << value << std::endl;
{
int value = 20;
std::cout << value << std::endl;
}
std::cout << value << std::endl;
return 0;
}
Antes de executar, escreva a saída esperada.
Depois valide.
Esperado:
10
20
10
Explique por que o primeiro value não se tornou 20.
42. Microdesafio — variável local de função
Implemente:
int CalculateReward(int baseCoins, int bonusCoins)
{
int reward = baseCoins + bonusCoins;
return reward;
}
Depois em:
main()
tente usar diretamente:
reward
Não funciona.
Corrija para:
int reward = CalculateReward(10, 5);
Agora main() possui sua própria variável chamada:
reward
que recebeu o valor retornado.
43. Código final do capítulo
Vamos reorganizar nosso mini Game Loop mantendo o foco em escopo:
#include <iostream>
enum class GameState
{
Menu,
Playing,
GameOver
};
int CalculateHealthAfterDamage(int playerHealth, int damage);
int CalculateCoinsReward(int baseCoins, int bonusCoins);
bool IsPlayerDefeated(int playerHealth);
void ShowPlayerState(int frame, int playerHealth, int playerCoins);
void ShowGameSummary(int frame, int playerHealth, int playerCoins);
int main()
{
const int DAMAGE_PER_FRAME = 20;
const int BASE_COINS_REWARD = 1;
const int BONUS_COINS_REWARD = 2;
bool isRunning = true;
int frame = 0;
int playerHealth = 100;
int playerCoins = 0;
GameState currentState = GameState::Menu;
std::cout << "=== MINI GAME LOOP START ===" << std::endl;
while (isRunning)
{
frame++;
if (currentState == GameState::Menu)
{
std::cout << "Starting game." << std::endl;
currentState = GameState::Playing;
}
if (currentState == GameState::Playing)
{
int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
playerHealth = CalculateHealthAfterDamage(playerHealth, DAMAGE_PER_FRAME);
playerCoins += coinsReward;
}
if (IsPlayerDefeated(playerHealth))
{
currentState = GameState::GameOver;
}
ShowPlayerState(frame, playerHealth, playerCoins);
if (currentState == GameState::GameOver)
{
std::cout << "Player defeated." << std::endl;
isRunning = false;
}
}
std::cout << "=== MINI GAME LOOP FINISHED ===" << std::endl;
ShowGameSummary(frame, playerHealth, playerCoins);
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;
}
bool IsPlayerDefeated(int playerHealth)
{
return playerHealth <= 0;
}
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;
}
void ShowGameSummary(int frame, int playerHealth, int playerCoins)
{
std::cout << "Frames: " << frame << std::endl;
std::cout << "Final Health: " << playerHealth << std::endl;
std::cout << "Final Coins: " << playerCoins << std::endl;
}
44. Onde cada variável está disponível?
Em main():
bool isRunning
int frame
int playerHealth
int playerCoins
GameState currentState
estão disponíveis no escopo apropriado de main() depois de suas declarações.
Dentro:
if (currentState == GameState::Playing)
{
int coinsReward = ...;
}
temos um nome adicional:
coinsReward
que só é necessário durante aquele bloco.
Depois que saímos:
std::cout << coinsReward;
não é válido.
Isso é bom.
45. coinsReward não precisa existir fora do Update
Observe:
if (currentState == GameState::Playing)
{
int coinsReward = CalculateCoinsReward(BASE_COINS_REWARD, BONUS_COINS_REWARD);
playerCoins += coinsReward;
}
O propósito de:
coinsReward
é temporário.
Ele representa a recompensa daquela atualização.
Não precisamos do nome depois.
Então declarar dentro daquele escopo comunica exatamente essa intenção.
46. Dentro de CalculateCoinsReward
Temos:
int CalculateCoinsReward(int baseCoins, int bonusCoins)
{
int reward = baseCoins + bonusCoins;
return reward;
}
Os nomes:
baseCoins
bonusCoins
reward
pertencem ao contexto da função.
Quando ela termina, main() não precisa saber que internamente utilizamos uma variável chamada:
reward
Ele só recebe o valor devolvido.
47. Escopo começa a ajudar a organização do raciocínio
Agora conseguimos analisar uma função isoladamente:
int CalculateCoinsReward(int baseCoins, int bonusCoins)
e considerar:
quais dados entram?
quais nomes locais existem?
qual resultado sai?
Sem precisar acompanhar todo o estado do programa.
Essa capacidade de limitar o contexto necessário para raciocinar sobre uma parte do software é extremamente valiosa.
48. Validando a implementação
Salve:
Ctrl + S
Faça Build:
Ctrl + Shift + B
Confirme:
Build succeeded
Execute:
Ctrl + F5
Com:
Health inicial = 100
Damage = 20
Reward = 3
esperamos:
Frame 1 → Health 80 / Coins 3
Frame 2 → Health 60 / Coins 6
Frame 3 → Health 40 / Coins 9
Frame 4 → Health 20 / Coins 12
Frame 5 → Health 0 / Coins 15
Resultado final:
Frames: 5
Final Health: 0
Final Coins: 15
49. Testes negativos de escopo
Também queremos verificar o que não deve compilar.
Teste individualmente e depois desfaça.
Caso 1
Usar coinsReward fora do if.
std::cout << coinsReward << std::endl;
Caso 2
Usar newHealth dentro de main().
std::cout << newHealth << std::endl;
Caso 3
Usar day depois de:
for (int day = 1; day <= 5; day++)
{
}
Esses erros ajudam a tornar o conceito observável.
50. Boas práticas que já podemos aplicar
Sem transformar isso em regras absolutas:
declare variáveis perto de onde serão utilizadas;
prefira escopos menores quando isso melhorar a clareza;
evite manter nomes temporários disponíveis por regiões enormes;
evite shadowing acidental;
não transforme estado em global apenas para facilitar acesso;
use parâmetros e retornos para tornar dependências explícitas;
permita que funções mantenham detalhes internos realmente internos.
51. O que ainda não estudamos profundamente?
Neste capítulo estamos respondendo principalmente:
onde o nome pode ser utilizado?
Mas existe outra pergunta.
Considere:
if (condition)
{
int value = 10;
}
Sabemos que depois das chaves não podemos mais utilizar o nome:
value
Mas quando exatamente o objeto associado àquela variável começa a existir?
Quando é destruído?
E uma variável dentro de uma função?
E uma variável criada a cada iteração de um loop?
E objetos que futuramente possuírem construtores e destrutores?
Agora começamos a entrar no conceito de:
lifetime.
52. Escopo e lifetime não são sinônimos
Esta distinção será o ponto de partida do próximo capítulo.
Por enquanto:
SCOPE
→ onde um nome pode ser utilizado
Enquanto lifetime estará relacionado a:
LIFETIME
→ durante qual período de execução um objeto existe
Frequentemente essas duas ideias caminham juntas em nossos exemplos simples.
Mas não devemos tratá-las como sinônimos.
Essa diferença se tornará muito importante quando avançarmos em C++.
53. Checklist de compreensão
Antes de avançar, tente responder sem consultar o artigo:
O que é escopo?
Qual a relação entre blocos e escopo?
Uma variável declarada dentro de um
ifpode ser utilizada depois dele?Por que uma variável criada dentro de
fornormalmente não está disponível depois do loop?Uma função consegue acessar automaticamente variáveis locais de outra função?
Parâmetros possuem qual relação com o escopo da função?
Por que dois parâmetros chamados
playerHealthem funções diferentes não são a mesma variável?O que é shadowing?
Por que shadowing pode prejudicar a legibilidade?
Por que declarar variáveis perto de onde são utilizadas pode ser útil?
Qual problema aparece quando colocamos estado demais em variáveis globais?
Por que parâmetros e retornos deixam dependências mais explícitas?
Qual diferença inicial existe entre escopo e lifetime?
Se consegue explicar essas respostas com suas próprias palavras, estamos prontos para continuar.
54. Estrutura do projeto
Continuamos:
CppGameEngine/
└── main.cpp
Ainda não precisamos separar arquivos.
Mas nosso conhecimento agora já inclui:
funções
parâmetros
retorno
escopo
Isso será fundamental quando começarmos a trabalhar com:
headers
source files
classes
objetos
referências
ponteiros
RAII
mais adiante.
55. Git
Depois da validação:
git status
Adicione:
git add CppGameEngine/main.cpp
Ajuste o caminho se necessário.
Commit sugerido:
git commit -m "refactor: reduce variable scope in game loop"
A mudança principal deste capítulo é estrutural:
variáveis temporárias passam a existir somente nas regiões em que realmente são necessárias.
56. Atualizando o CHANGELOG
Em [Unreleased]:
### Refactored
- Reduced temporary variable scope inside the mini Game Loop.
- Moved reward calculation state closer to its point of use.
- Kept function-local calculation details isolated from `main()`.
- Preserved explicit data flow through parameters and return values.
### Added
- Added block, loop, and function scope examples.
- Added variable shadowing demonstrations.
- Added negative compilation exercises for out-of-scope variable access.
Depois:
git add CHANGELOG.md
git commit -m "docs: update changelog with scope concepts"
O ROADMAP técnico não precisa ser alterado neste capítulo.
57. O que aprendemos?
Nosso código já possuía muitos nomes:
playerHealth
playerCoins
frame
coinsReward
newHealth
damage
baseCoins
bonusCoins
Hoje descobrimos que esses nomes não existem indistintamente em todo o programa.
Aprendemos que:
escopo define a região onde um nome pode ser utilizado;
blocos podem introduzir novos escopos;
escopos podem ser aninhados;
nomes externos podem ser utilizados em determinados escopos internos;
nomes internos não ficam automaticamente disponíveis externamente;
parâmetros pertencem ao contexto de suas funções;
variáveis locais de funções permanecem detalhes internos;
variáveis de controle do
forpodem ficar restritas ao próprio loop;declarar perto do uso reduz a região em que precisamos acompanhar determinado estado;
shadowing cria um novo nome que pode ocultar outro nome visível;
nomes iguais em funções diferentes não significam automaticamente a mesma variável;
variáveis globais ampliam o estado compartilhado e podem esconder dependências;
parâmetros e retornos continuam sendo nossa forma preferida de tornar o fluxo de dados explícito neste estágio;
escopo e lifetime são conceitos relacionados, mas diferentes.
Conclusão
No início da série nossas variáveis pareciam bastante simples:
int playerHealth = 100;
A pergunta era apenas:
qual valor está armazenado?
Depois começamos a perguntar:
qual tipo?
pode mudar?
como transformar?
como enviar para uma função?
como devolver um resultado?
Agora adicionamos outra pergunta essencial:
onde esse nome pode ser utilizado?
Considere:
int CalculateHealthAfterDamage(int playerHealth, int damage)
{
int newHealth = playerHealth - damage;
return newHealth;
}
Temos uma pequena fronteira.
Dentro dela existem:
playerHealth
damage
newHealth
Fora dela, o restante do programa não precisa manipular diretamente esses nomes.
O resultado atravessa a fronteira de maneira explícita:
return newHealth;
E os dados entram também explicitamente:
CalculateHealthAfterDamage(playerHealth, damage);
Estamos começando a perceber que organização de software não depende apenas de:
pastas
classes
frameworks
patterns
Ela começa em coisas muito menores:
onde um nome existe?
quem precisa vê-lo?
quem pode modificá-lo?
quais detalhes devem permanecer internos?
Mas ainda falta uma pergunta fundamental.
Se uma variável deixa de estar acessível ao sair de um bloco, o que acontece com o objeto associado a ela?
Considere:
{
int reward = 10;
}
Quando entramos no bloco:
reward aparece
Quando saímos:
o nome deixa de poder ser utilizado
Mas existe um conceito mais profundo que apenas visibilidade.
Precisamos entender:
por quanto tempo aquele objeto realmente existe durante a execução do programa?
E é exatamente esse o próximo passo.
Próximo capítulo
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
No próximo artigo vamos partir de exemplos simples:
{
int playerCoins = 10;
}
e:
int CalculateReward()
{
int reward = 10;
return reward;
}
para separar duas ideias que frequentemente são confundidas:
SCOPE
↓
onde o nome pode ser usado
e:
LIFETIME
↓
durante quanto tempo o objeto existe
Vamos investigar:
quando uma variável local começa sua existência;
o que acontece ao sair de um bloco;
variáveis criadas em cada iteração;
parâmetros e duração da chamada;
retorno de valores locais;
por que retornar um
intlocal funciona;por que lifetime será tão importante quando chegarmos a referências e ponteiros;
como erros de lifetime podem se tornar muito mais perigosos em C++;
e como tudo isso prepara nossa jornada para memória, objetos, RAII e gerenciamento seguro de recursos.
Hoje aprendemos onde podemos enxergar um nome.
No próximo capítulo começaremos a entender por quanto tempo o objeto representado por esse nome permanece vivo.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!