Aeca

Capítulo 20 — Escopo: onde uma variável realmente existe

Série: Do zero ao jogo comercial com C++ e SFML

Capítulo 20 — Escopo: onde uma 
variável realmente existe
aeca

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:

day existe 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 if pode ser utilizada depois dele?

  • Por que uma variável criada dentro de for normalmente 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 playerHealth em 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 for podem 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 int local 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.

Subscreve "Aeca" para receber atualizações diretamente na tua caixa de entrada
aeca

Subscreve aeca para reagir

Subscrever

Comentários

Ainda não há comentários. Sê o primeiro a comentar!

Subscreve Aeca para receber atualizações diretamente na tua caixa de entrada