Aeca

Capítulo 02 — Entendendo o código que acabamos de executar

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

Capítulo 02 — Entendendo o código que acabamos de executar
aeca

Antes de adicionar novas funcionalidades, vamos descobrir o que realmente existe dentro do nosso primeiro main.cpp

No capítulo anterior aconteceu algo importante.

Partimos praticamente do zero, abrimos o Visual Studio Community 2022, criamos nosso projeto C++, adicionamos um main.cpp, fizemos o primeiro Build e finalmente vimos nosso programa sendo executado.

Nosso código era pequeno:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

E o resultado:

  Game Engineering com C++ começou!

Depois modificamos a mensagem, adicionamos novas linhas e comprovamos uma relação fundamental:

  Código
  ↓
Build
  ↓
Execução
  ↓
Comportamento

Funcionou.

Mas existe uma pergunta que propositalmente deixamos sem resposta:

O que exatamente acabamos de escrever?

O que significa #include?

Por que existe <iostream>?

O que é int?

Por que nosso programa possui algo chamado main?

O que significam ()?

Por que existem { e }?

De onde apareceu std::cout?

O que são aqueles dois sinais <<?

Por que terminamos algumas instruções com ;?

E o misterioso:

  return 0;

serve para quê?

Hoje não vamos adicionar uma Game Engine.

Não vamos instalar SFML.

Também não precisamos criar novas classes.

Vamos fazer algo igualmente importante para quem pretende trabalhar seriamente com C++:

parar de enxergar nosso primeiro programa como símbolos que precisam ser copiados e começar a compreender o que estamos dizendo ao computador.

1. Nosso laboratório continua pequeno

Continuaremos trabalhando apenas com:

  CppGameEngine/
└── main.cpp

Nosso código continua:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

E isso é suficiente.

Existe uma tendência comum quando começamos a estudar programação: querer escrever programas maiores rapidamente.

Mais linhas.

Mais arquivos.

Mais classes.

Mais funcionalidades.

Mas tamanho não significa compreensão.

Neste capítulo, aquelas poucas linhas serão nosso laboratório.

Vamos desmontá-las.

2. Primeiro: código-fonte é texto

Nosso arquivo:

  main.cpp

é, essencialmente, um arquivo contendo texto escrito segundo as regras da linguagem C++.

Quando escrevemos:

  int main()
{
    return 0;
}

o Windows não executa diretamente esses caracteres como nós os enxergamos no editor.

Nosso código precisa passar por um processo de construção antes de resultar no programa executável que utilizamos no capítulo anterior.

Por enquanto podemos manter este modelo simplificado:

  main.cpp
   ↓
Build
   ↓
CppGameEngine.exe
   ↓
Execução

Em breve desmontaremos também o que existe dentro de Build.

Hoje, porém, nosso foco está no lado esquerdo:

  main.cpp

Precisamos compreender o que escrevemos ali.

3. Começando pela primeira linha

Temos:

  #include <iostream>

Essa linha é diferente das demais.

Observe que ela começa com:

  #

Depois:

  include

E finalmente:

  <iostream>

Vamos separar:

  #include <iostream>
│        │
│        └── arquivo/header solicitado
│
└── diretiva de pré-processamento

Aqui encontramos nosso primeiro conceito novo.

4. O que é #include?

#include é uma diretiva de pré-processamento.

Ainda estudaremos o pré-processador de C++ com muito mais profundidade quando entrarmos no processo de compilação.

Por enquanto, podemos compreender a intenção da instrução:

incluir conteúdo necessário para que possamos utilizar determinadas funcionalidades.

Quando escrevemos:

  #include <iostream>

estamos solicitando acesso às declarações relacionadas ao mecanismo padrão de entrada e saída utilizado em nosso exemplo.

É graças a isso que podemos trabalhar, entre outras coisas, com:

  std::cout

Se removermos o #include <iostream> e continuarmos tentando utilizar std::cout, nosso programa terá um problema.

E daqui a pouco vamos experimentar exatamente isso.

5. O que significa iostream?

O nome:

  iostream

vem da ideia de:

  I/O Stream

onde:

  I/O = Input / Output

ou:

  Entrada / Saída

e:

  Stream = fluxo

Portanto, podemos pensar em iostream como parte da biblioteca padrão relacionada a fluxos de entrada e saída.

Nosso primeiro uso é saída.

Queremos enviar texto para o console.

É aí que entra:

  std::cout

6. Os sinais < e > em <iostream>

Observe:

  #include <iostream>

Neste contexto, os símbolos:

  <
>

não estão sendo utilizados como operadores de comparação.

Eles fazem parte da sintaxe da diretiva #include.

Por enquanto podemos interpretar:

  #include <iostream>

como:

procure o header iostream nos locais configurados para headers do sistema/bibliotecas e disponibilize seu conteúdo para esta unidade de tradução.

“Unidade de tradução” ainda é um conceito avançado demais para precisarmos destrinchá-lo agora.

Mas guarde o termo.

Voltaremos a ele quando estudarmos compilação de verdade.

7. Por que não existe ; depois do #include?

Temos:

  #include <iostream>

e não:

  #include <iostream>;

Isso acontece porque #include não é uma instrução C++ comum como aquelas que veremos dentro da função main.

É uma diretiva processada em uma etapa anterior à compilação propriamente dita.

Essa diferença ficará muito mais clara quando estudarmos:

  Source Code
     ↓
Preprocessor
     ↓
Compiler
     ↓
Object File
     ↓
Linker
     ↓
Executable

Por enquanto, apenas observe:

nem toda linha de um arquivo C++ segue exatamente as mesmas regras sintáticas.

8. Agora chegamos ao main

Nossa próxima parte é:

  int main()

Essa pequena linha contém vários conceitos.

Vamos separá-la:

  int main()
│   │   │
│   │   └── parênteses
│   │
│   └── nome da função
│
└── tipo de retorno

Estamos diante de uma função.

Ainda teremos capítulos específicos sobre funções.

Hoje precisamos compreender apenas o suficiente para entender nosso programa.

9. O que é uma função?

Uma função é uma unidade de código que representa determinada operação ou comportamento.

Mais adiante criaremos funções como:

  updatePlayer();

ou:

  calculateDamage();

ou:

  saveGame();

Mas nosso programa já possui uma função especial:

  main()

Ela representa o ponto de entrada da aplicação C++ hospedada que estamos construindo.

Quando nosso programa começa sua execução, main possui um papel especial.

De maneira simplificada:

  Programa inicia
      ↓
    main()
      ↓
instruções executadas
      ↓
Programa termina

É por isso que nosso primeiro código está dentro dela.

10. Por que o nome precisa ser main?

Poderíamos escrever:

  int game()

e esperar que o programa começasse ali?

Não dessa maneira.

main possui significado especial para a implementação de uma aplicação C++ hospedada.

É o ponto de entrada definido pela linguagem para esse tipo de programa.

Mais tarde poderemos possuir dezenas ou centenas de funções:

  update()
render()
load()
save()
movePlayer()
calculateDamage()
playSound()

Mas continuaremos precisando de um ponto inicial para nossa aplicação.

Nesse modelo, ele será:

  main()

11. E o int antes de main?

Temos:

  int main()

int é um tipo.

Mais especificamente, representa um tipo inteiro.

Ainda teremos um capítulo inteiro sobre tipos.

Por enquanto, o importante é perceber que a função main precisa retornar um valor inteiro.

Por isso escrevemos:

  int

antes do nome.

Conceitualmente:

  int main()
│
└── esta função retorna um inteiro

E é justamente por isso que encontramos mais abaixo:

  return 0;

As duas coisas estão relacionadas.

12. Os parênteses ()

Depois de main temos:

  ()

Funções utilizam parênteses em sua declaração e chamada.

Eles também estão relacionados aos parâmetros que uma função pode receber.

Mais adiante poderemos encontrar algo semelhante a:

  calculateDamage(10);

Nesse exemplo hipotético, existe uma informação sendo enviada para a função.

Nosso main atual utiliza:

  main()

sem parâmetros nessa forma.

Por enquanto, pense:

  main
+
()
=
função

Essa simplificação será refinada mais adiante.

13. Então aparecem as chaves

Logo depois temos:

  {

e posteriormente:

  }

Juntas:

  {
    // código
}

As chaves delimitam um bloco.

No nosso caso:

  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

elas delimitam o corpo da função main.

Podemos visualizar:

  int main()
     │
     ▼
{
    tudo aqui pertence
    ao corpo da função
}

Essa ideia de bloco aparecerá inúmeras vezes em C++.

Quando chegarmos às decisões:

  if (...)
{
}

Nos loops:

  while (...)
{
}

Nas classes:

  class Player
{
};

E em vários outros contextos.

14. A indentação não é decoração

Observe:

  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

As linhas dentro das chaves estão deslocadas para a direita.

Isso é indentação.

Poderíamos escrever algo visualmente terrível como:

  int main()
{
std::cout << "Game Engineering com C++ começou!" << std::endl;
return 0;
}

Em vários casos o compilador ainda conseguiria compreender.

Nós, humanos, sofreríamos mais.

Código é lido muito mais vezes do que é escrito.

Portanto, legibilidade importa desde o primeiro capítulo.

Usaremos uma formatação consistente durante toda a série.

15. Chegamos ao std::cout

Agora encontramos:

  std::cout

Foi ele que utilizamos para enviar nossa mensagem ao console.

Podemos pensar inicialmente:

  std::cout
    ↓
fluxo padrão de saída
    ↓
console

O nome cout está associado à ideia de:

  character output

Em nosso programa, ele está sendo utilizado para produzir saída textual.

Mas por que existe:

  std::

antes dele?

16. O que é std?

std é o namespace utilizado pela biblioteca padrão de C++.

Namespace é um conceito que estudaremos com muito mais cuidado posteriormente.

Por enquanto, imagine que namespaces ajudam a organizar nomes e evitar conflitos.

Temos:

  std::cout

Podemos ler conceitualmente como:

cout pertencente ao namespace std.

Da mesma forma:

  std::endl

representa endl dentro de std.

O operador:

  ::

é chamado de scope resolution operator.

Ou operador de resolução de escopo.

Portanto:

  std::cout
│   │
│   └── cout
│
└── namespace std

17. Por que não escrever apenas cout?

Talvez você já tenha visto códigos assim:

  using namespace std;
  cout << "Hello";

Não utilizaremos isso automaticamente.

Nosso código continuará explícito:

  std::cout

e:

  std::endl

Existem implicações envolvendo namespaces, escopo e conflitos de nomes que estudaremos futuramente.

Neste momento, escrever explicitamente:

  std::

possui também uma vantagem didática.

Conseguimos enxergar claramente que estamos utilizando algo pertencente à biblioteca padrão.

18. Agora temos <<

Nossa instrução completa é:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

Depois de std::cout, encontramos:

  <<

Nesse contexto, estamos utilizando o operador de inserção com o stream de saída.

Uma leitura intuitiva pode ser:

  std::cout
   <<
"Game Engineering com C++ começou!"

como:

envie essa informação para std::cout.

Depois temos novamente:

  <<

para inserir:

  std::endl

no mesmo fluxo.

Podemos visualizar:

  std::cout
    │
    ├── << "Game Engineering com C++ começou!"
    │
    └── << std::endl

19. Os operadores podem significar coisas diferentes?

Sim.

E esse é um detalhe importante de C++.

Você futuramente encontrará:

  <<

em outros contextos com outro significado.

C++ permite que operadores tenham comportamentos associados aos tipos envolvidos.

Não precisamos entrar em operator overloading agora.

Seria antecipar um assunto que ainda não precisamos dominar.

Neste capítulo basta compreender:

quando utilizado dessa forma com std::cout, << insere dados no fluxo de saída.

20. Nossa primeira string

Agora chegamos a:

  "Game Engineering com C++ começou!"

O texto está entre:

  "
"

Isso representa um literal de string.

Podemos pensar inicialmente em string como texto.

Nos próximos capítulos trabalharemos com informações de vários tipos.

Por exemplo:

  100
25
83.5
true
"Alexander"
"Farm"

Esses valores não representam a mesma espécie de informação.

Hoje nosso foco está no texto.

As aspas dizem ao compilador que:

  "Game Engineering com C++ começou!"

representa conteúdo textual literal.

21. Experimente remover as aspas

Não faça apenas mentalmente.

Teste.

Transforme:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

em algo incorreto como:

  std::cout << Game Engineering com C++ começou! << std::endl;

Tente fazer Build.

O programa não deverá compilar corretamente.

O compilador não possui como simplesmente adivinhar:

“Ah, provavelmente ele queria mostrar essa frase.”

Código precisa obedecer às regras sintáticas da linguagem.

Depois do experimento, restaure:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

Faça Build novamente.

Confirme que voltou a funcionar.

Acabamos de usar um erro como ferramenta de aprendizado.

22. O que é std::endl?

Depois da mensagem temos:

  std::endl

Em nosso exemplo ele produz uma mudança de linha e também solicita o flush do stream.

Isso significa que:

  std::cout << "Linha 1" << std::endl;
std::cout << "Linha 2" << std::endl;

produzirá:

  Linha 1
Linha 2

Mas existe um detalhe técnico importante.

std::endl não significa simplesmente "quebra de linha".

Ele também força a descarga do buffer associado ao stream.

Para nosso pequeno programa isso não representa um problema relevante.

Mais adiante aprenderemos que, quando queremos apenas uma nova linha, frequentemente podemos utilizar:

  '\n'

Por exemplo:

  std::cout << "Linha 1\n";
std::cout << "Linha 2\n";

Não precisamos decidir hoje qual abordagem deve ser utilizada em todos os lugares.

O importante é evitar aprender a definição incorreta:

“std::endl é apenas quebra de linha."

Não é apenas isso.

23. O ponto e vírgula

Finalmente chegamos ao:

  ;

Nossa instrução:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

termina com ponto e vírgula.

Outra:

  return 0;

também.

O ; é utilizado em C++ para encerrar diversas declarações e instruções.

Não significa que toda linha visual do arquivo obrigatoriamente termina com ;.

Observe:

  #include <iostream>

não termina.

  int main()

não termina.

  {

não termina.

  }

não termina nesse caso.

Portanto, não memorize:

“Toda linha de C++ termina com ponto e vírgula.”

Essa regra estaria errada.

Precisamos aprender quais construções exigem ;.

Isso virá naturalmente.

24. Vamos remover um ;

Agora faremos outro experimento.

Transforme:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

em:

  std::cout << "Game Engineering com C++ começou!" << std::endl

Faça Build.

Observe o erro.

Não corrija imediatamente.

Leia a mensagem.

Veja:

  • qual arquivo foi indicado;

  • qual linha aparece;

  • qual código de erro foi informado;

  • qual descrição foi apresentada.

Dependendo da versão/configuração do compilador, a mensagem exata poderá variar.

Nosso objetivo neste momento não é decorar códigos de erro.

É desenvolver o hábito:

quando o compilador reclamar, leia o que ele está dizendo.

Agora recoloque:

  ;

e faça Build novamente.

25. O compilador acabou de nos ensinar alguma coisa

Compare:

Código válido

  std::cout << "Game Engineering com C++ começou!" << std::endl;

Código inválido

  std::cout << "Game Engineering com C++ começou!" << std::endl

Mudamos um único caractere.

O comportamento foi de:

  Build succeeded

para:

  Build failed

Isso demonstra algo fundamental.

O compilador não interpreta nossa intenção.

Ele interpreta código segundo regras.

Programar envolve aprender a expressar nossa intenção de maneira suficientemente precisa para que essas regras possam ser processadas.

26. Agora o return

Chegamos à última instrução:

  return 0;

Já sabemos que nossa função foi declarada como:

  int main()

Ou seja:

  main retorna int

E temos:

  return 0;

Portanto:

  int main()
│
│
└───────────────┐
                │
                ▼
           return 0;

A função está devolvendo o valor inteiro:

  0

27. O que significa esse zero?

No contexto do retorno de main, zero convencionalmente indica que o programa terminou com sucesso.

Podemos pensar:

  0
↓
successful termination

Valores diferentes de zero podem ser utilizados para representar situações de falha ou códigos específicos definidos pela aplicação.

Isso será muito útil futuramente.

Imagine ferramentas internas da nossa Engine sendo executadas por scripts ou pipelines.

Um processo poderá terminar com:

  0

e comunicar:

execução concluída com sucesso.

Ou outro valor para indicar falha.

Aquela pequena linha:

  return 0;

tem conexão com sistemas muito maiores do que nosso console atual sugere.

28. Quem recebe esse valor?

O valor retornado por main é disponibilizado ao ambiente que iniciou o processo.

Isso significa que nosso programa não existe completamente isolado do sistema operacional.

Conceitualmente:

  Sistema operacional
        ↓
 inicia programa
        ↓
      main()return 0
        ↓
status de término
        ↓
Sistema operacional

Essa ideia será importante quando chegarmos a:

  • scripts;

  • automação;

  • ferramentas;

  • builds;

  • testes;

  • CI/CD;

  • pipelines.

Game Engineering não acontece apenas dentro da janela do jogo.

29. Então nosso código inteiro pode ser lido assim

Agora podemos voltar ao programa:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

E fazer uma primeira tradução conceitual:

  #include <iostream>
│
└── disponibiliza declarações de entrada/saída
    que precisaremos utilizar.
  int main()
│
├── função especial de entrada do programa;
└── retorna um valor inteiro.{
│
└── começa o corpo da função.std::cout
│
└── fluxo padrão de saída.<<
│
└── insere informação no fluxo."Game Engineering com C++ começou!"
│
└── literal textual.<< std::endl;
│
├── produz nova linha;
└── solicita flush do stream.return 0;
│
└── encerra main retornando sucesso.}
│
└── termina o corpo da função.

Agora nosso código começa a parecer menos misterioso.

30. Vamos observar a ordem de execução

Considere:

  #include <iostream>
  int main()
{
    std::cout << "Primeira linha" << std::endl;
    std::cout << "Segunda linha" << std::endl;
    std::cout << "Terceira linha" << std::endl;    return 0;
}

Execute.

Teremos:

  Primeira linha
Segunda linha
Terceira linha

Nesse exemplo simples, as instruções no corpo de main estão sendo executadas sequencialmente.

Podemos representar:

  main()
  ↓
Primeira instrução
  ↓
Segunda instrução
  ↓
Terceira instrução
  ↓
return 0
  ↓
fim

Acabamos de encontrar outro fundamento de programação:

sequência.

31. Programação começa com controle de fluxo

Neste momento nosso programa possui apenas um fluxo simples:

  Início
  ↓
Instrução
  ↓
Instrução
  ↓
Instrução
  ↓
Fim

Mas futuramente teremos:

  Decisão

Depois:

  Repetição

Depois:

  Funções

Depois estados e sistemas cada vez mais sofisticados.

Boa parte da programação envolve controlar o que acontece, em qual ordem e sob quais condições.

Nosso pequeno main já é o primeiro passo.

32. Um programa não é apenas código que “roda de cima para baixo”

Precisamos tomar cuidado para não transformar nossa observação atual em uma regra universal.

Hoje vemos:

  std::cout << "A" << std::endl;
std::cout << "B" << std::endl;
std::cout << "C" << std::endl;

e observamos:

  A
B
C

Mas C++ possui:

  • funções;

  • condicionais;

  • loops;

  • chamadas;

  • exceções;

  • objetos;

  • threads;

  • callbacks;

  • eventos;

e muitos outros mecanismos capazes de alterar drasticamente o fluxo.

Portanto:

nosso programa atual possui fluxo sequencial simples.

Essa é uma afirmação melhor do que:

programas C++ sempre executam simplesmente de cima para baixo.

33. Vamos transformar nosso console em uma primeira tela conceitual de jogo

Ainda não temos SFML.

Mas podemos utilizar aquilo que já sabemos para começar a pensar no domínio de games.

Modifique temporariamente nosso programa:

  #include <iostream>
  int main()
{
    std::cout << "==============================" << std::endl;
    std::cout << "       FARM GAME PROTOTYPE     " << std::endl;
    std::cout << "==============================" << std::endl;
    std::cout << "Health: 100" << std::endl;
    std::cout << "Energy: 80" << std::endl;
    std::cout << "Coins: 25" << std::endl;
    std::cout << "==============================" << std::endl;    return 0;
}

Execute.

Teremos algo semelhante a:

  ==============================
       FARM GAME PROTOTYPE
==============================
Health: 100
Energy: 80
Coins: 25
==============================

Isso ainda é apenas texto.

Mas observe os conceitos começando a aparecer:

  Player
├── Health
├── Energy
└── Coins

Hoje esses valores estão presos dentro das strings.

Logo isso começará a incomodar.

Queremos que:

  Health
Energy
Coins

sejam informações reais manipuláveis pelo programa.

Esse problema nos levará diretamente a um dos próximos fundamentos:

variáveis.

34. Mas ainda não vamos criar variáveis

Poderíamos antecipar:

  int playerHealth = 100;

Mas esse não é o objetivo deste capítulo.

Existe valor em sentir primeiro a limitação.

Hoje temos:

  std::cout << "Health: 100" << std::endl;

O número 100 é apenas parte daquele texto.

Se quisermos alterar a vida do jogador durante o programa, precisamos de uma representação melhor.

Agora temos um problema concreto.

Quando variáveis chegarem, elas terão uma razão para existir.

Essa será nossa abordagem ao longo de toda a série.

35. Um detalhe sobre comentários

Enquanto estudamos código, você provavelmente verá exemplos contendo:

  // comentário

Por exemplo:

  int main()
{
    // Exibe uma mensagem no console.
    std::cout << "Game Engineering com C++ começou!" << std::endl;
      return 0;
}

Tudo depois de:

  //

naquela linha é um comentário.

Comentários servem para informações destinadas aos desenvolvedores, não como instruções executadas normalmente pelo programa.

Também existem comentários de bloco:

  /*
    Este é um comentário
    que ocupa várias linhas.
*/

Mas existe uma regra importante.

Não faça isso:

  // Usa cout para imprimir a mensagem.
std::cout << "Hello" << std::endl;

quando o comentário apenas repete literalmente o código.

Comentários úteis normalmente explicam por quê, contexto, restrições ou algo que não seja evidente pela própria implementação.

Nos primeiros capítulos utilizaremos alguns comentários didáticos quando ajudarem no aprendizado.

Em código de produção, comentários desnecessários podem virar ruído.

36. Código legível reduz a necessidade de comentários

Compare:

  int x = 100;

com:

  int playerHealth = 100;

Mesmo sem estudarmos variáveis formalmente ainda, provavelmente conseguimos inferir melhor o segundo exemplo.

Nomes carregam significado.

Mais adiante esse princípio aparecerá em:

  • variáveis;

  • funções;

  • classes;

  • arquivos;

  • módulos;

  • testes.

Clean Code não começa quando instalamos uma ferramenta.

Começa quando tentamos expressar intenção claramente.

37. Primeiro exercício — leia o código em português

Sem executar, tente explicar:

  #include <iostream>
  int main()
{
    std::cout << "Welcome to the farm!" << std::endl;
    return 0;
}

Uma possível leitura seria:

Incluímos o header necessário para utilizar recursos de entrada e saída. Declaramos a função principal do programa, que retorna um inteiro. Dentro dela enviamos uma mensagem para o fluxo padrão de saída e finalizamos retornando zero para indicar término bem-sucedido.

Não precisa decorar essa frase.

O objetivo é conseguir olhar para o código e começar a atribuir significado aos símbolos.

38. Segundo exercício — preveja antes de executar

Observe:

  #include <iostream>
  int main()
{
    std::cout << "Plant" << std::endl;
    std::cout << "Water" << std::endl;
    std::cout << "Harvest" << std::endl;    return 0;
}

Antes de executar, responda:

O que aparecerá no console?

Agora execute e compare sua previsão.

O objetivo é começar a criar um modelo mental do comportamento do programa.

39. Terceiro exercício — experimente std::endl

Comece com:

  #include <iostream>
  int main()
{
    std::cout << "Plant";
    std::cout << "Water";
    std::cout << "Harvest";    return 0;
}

Execute.

Observe.

Agora altere para:

  #include <iostream>
  int main()
{
    std::cout << "Plant" << std::endl;
    std::cout << "Water" << std::endl;
    std::cout << "Harvest" << std::endl;    return 0;
}

Execute novamente.

Compare os resultados.

Não precisamos apenas acreditar que std::endl afeta a saída.

Nós conseguimos observar.

40. Quarto exercício — quebre o #include

Pegue:

  #include <iostream>

e transforme temporariamente em:

  #include <iostreamx>

Faça Build.

Observe o erro.

O header solicitado não existe nos caminhos pesquisados pelo compilador.

Depois restaure:

  #include <iostream>

e faça Build novamente.

A sequência é importante:

  Código válido
    ↓
Alteração intencional
    ↓
Erro
    ↓
Observação
    ↓
Hipótese
    ↓
Correção
    ↓
Build válido novamente

É assim que começaremos a desenvolver capacidade de debugging.

41. Quinto exercício — remova iostream

Agora remova completamente:

  #include <iostream>

mas mantenha:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

Faça Build.

Observe os erros.

Depois recoloque:

  #include <iostream>

e compile novamente.

O objetivo é estabelecer uma relação:

  iostream
    ↓
declarações necessárias
    ↓
std::cout / std::endl

Mais adiante entenderemos tecnicamente o mecanismo com muito mais profundidade.

42. Sexto exercício — experimente as chaves

Faça uma cópia mental do código válido antes de alterar qualquer coisa.

Agora remova temporariamente a chave final:

  }

Tente compilar.

Observe.

Depois restaure.

Esses experimentos parecem pequenos, mas começam a ensinar uma coisa extremamente valiosa:

sintaxe possui estrutura.

O compilador não enxerga nosso código apenas como uma coleção de palavras.

43. Não tente provocar todos os erros possíveis

Nosso objetivo não é transformar o arquivo em caos.

Estamos fazendo alterações controladas.

Uma por vez.

Isso é importante.

Se quebrarmos cinco coisas simultaneamente:

  erro A
erro B
erro C
erro D
erro E

fica difícil descobrir qual alteração produziu qual consequência.

Uma estratégia melhor:

  Estado funcionando
       ↓
Uma alteração
       ↓
Observar
       ↓
Compreender
       ↓
Restaurar

Esse método continuará útil quando nosso projeto possuir milhares de linhas.

44. Uma alteração pode gerar vários erros

Esse ponto merece atenção.

Às vezes removemos um único:

  ;

e o compilador apresenta mais de uma mensagem.

Isso não significa necessariamente que criamos vários problemas independentes.

Um erro inicial pode fazer o compilador interpretar incorretamente aquilo que vem depois.

Portanto, futuramente desenvolveremos uma prática importante:

começar investigando os primeiros erros relevantes, não tentar corrigir dezenas de mensagens aleatoriamente.

Isso será particularmente importante em C++.

45. Error List e Output não são a mesma coisa

No Visual Studio você poderá acompanhar problemas através de áreas como:

  Error List

e:

  Output

A Error List oferece uma visão organizada dos erros e warnings detectados.

A janela Output, especialmente durante Build, permite observar mensagens produzidas pelo processo de construção.

Ao aprender debugging e troubleshooting, acostume-se a consultar ambas.

Não dependa apenas do sublinhado vermelho no editor.

Nosso objetivo é aprender a interpretar aquilo que a toolchain realmente informa.

46. Erro de compilação não é erro de execução

Se removermos:

  ;

e o código não compilar, temos uma categoria de problema.

Se o código compilar e o programa executar, mas produzir resultado incorreto, temos outra.

Mais adiante teremos ainda:

  Compiler Error
Linker Error
Runtime Error
Logic Error

Não vamos estudar todas hoje.

Mas já podemos evitar chamar tudo simplesmente de:

“deu erro no C++.”

Classificar o problema ajuda a investigar.

47. O começo do nosso vocabulário técnico

Depois deste capítulo, algumas palavras já devem começar a fazer parte do nosso vocabulário:

  source code
header
preprocessor directive
function
main
return type
block
statement
namespace
scope resolution operator
stream
insertion operator
string literal
build
compiler error
exit status

Não se preocupe se ainda não consegue definir cada uma perfeitamente.

Estamos construindo conhecimento em camadas.

Encontraremos esses conceitos repetidamente.

48. Como isso se conecta ao nosso futuro jogo?

Pode parecer que estamos muito distantes de Game Engineering.

Mas imagine alguns meses no futuro.

Talvez tenhamos algo conceitualmente semelhante a:

  int main()
{
    Application application;
    return application.run();
}

Ou outra organização que ainda descobriremos.

Naquele momento nosso main.cpp poderá estar muito menor que alguns sistemas internos.

Mas você saberá de onde ele veio.

Não começamos copiando:

  Application application;

sem saber por quê.

Começamos com:

  std::cout << "Game Engineering com C++ começou!" << std::endl;

E vamos deixar o crescimento do software criar a necessidade de novas abstrações.

49. Ainda não existe uma classe Game

É tentador fazer agora:

  class Game
{
};

Afinal, estamos desenvolvendo um jogo.

Mas responda:

Qual problema atual essa classe resolveria?

Nosso programa:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

possui duas instruções relevantes no corpo de main.

Não temos complexidade suficiente para justificar uma classe Game.

Então não criaremos uma.

Isso é parte da Engenharia de Software que queremos aprender.

50. Também não precisamos de Engine

Da mesma forma, ainda não precisamos:

  class Engine
{
};

Nem:

  class Application
{
};

Nem:

  class GameManager
{
};

Nem:

  class SceneManager
{
};

Uma arquitetura profissional não é aquela que possui mais classes.

É aquela em que as responsabilidades existentes estão organizadas adequadamente para o problema atual e para sua evolução previsível.

Hoje nosso problema é minúsculo.

Nossa arquitetura também pode ser.

51. O estado do projeto continua simples

Depois deste capítulo:

  07_Game_Engineering_na_Pratica_CPP_SFML/
│
├── CppGameEngine/
│   └── main.cpp
│
├── README.md
├── CHANGELOG.md
├── CONTRIBUTING.md
└── ROADMAP.md

Nenhuma nova pasta precisa surgir.

Nenhuma nova classe precisa surgir.

O que mudou foi outra coisa:

nosso conhecimento sobre o código aumentou.

Isso também é progresso.

52. O que sabemos sobre nosso main.cpp agora?

No capítulo anterior enxergávamos:

  #include <iostream>
  int main()
{
    std::cout << "Game Engineering com C++ começou!" << std::endl;
    return 0;
}

como uma receita necessária para produzir uma mensagem.

Agora começamos a enxergar:

  #include
│
└── pré-processamento
  <iostream>
│
└── recursos de I/Oint
│
└── tipo de retornomain
│
└── entrada do programa()
│
└── função / parâmetros{}
│
└── blocostd
│
└── namespace da biblioteca padrão::
│
└── resolução de escopocout
│
└── fluxo padrão de saída<<
│
└── inserção no stream"..."
│
└── literal textualendl
│
└── nova linha + flush;
│
└── terminador necessário para essas instruçõesreturn 0
│
└── término bem-sucedido

Esse é um avanço enorme para apenas algumas linhas de código.

53. Microdesafio final

Crie um programa que produza:

  ================================
       GAME ENGINEERING
================================
Project: CppGameEngine
Language: C++
Standard: C++20
Current Stage: Fundamentals
================================
The journey has begun.

Você só pode utilizar conhecimentos apresentados até aqui.

Não use:

  • variáveis;

  • loops;

  • funções próprias;

  • arrays;

  • classes;

  • bibliotecas externas.

Apenas nosso main, std::cout, texto e aquilo que já aprendemos.

54. Uma possível solução

Tente antes de olhar.

Uma implementação possível:

  #include <iostream>
  int main()
{
    std::cout << "================================" << std::endl;
    std::cout << "       GAME ENGINEERING" << std::endl;
    std::cout << "================================" << std::endl;
    std::cout << "Project: CppGameEngine" << std::endl;
    std::cout << "Language: C++" << std::endl;
    std::cout << "Standard: C++20" << std::endl;
    std::cout << "Current Stage: Fundamentals" << std::endl;
    std::cout << "================================" << std::endl;
    std::cout << "The journey has begun." << std::endl;    return 0;
}

Faça Build.

Execute.

Depois altere alguma informação por conta própria.

O objetivo não é decorar.

É desenvolver confiança para manipular código conhecido.

55. Precisamos fazer commit?

Neste capítulo não introduzimos necessariamente uma nova funcionalidade permanente.

Boa parte das alterações realizadas foi experimental.

Removemos ;.

Alteramos headers.

Removemos chaves.

Mudamos mensagens.

Esses experimentos não precisam virar commits individuais.

Isso nos ensina outra prática importante:

nem toda tecla pressionada durante o desenvolvimento precisa aparecer na história permanente do Git.

Se o estado final do main.cpp permanecer igual ao Capítulo 01, não existe motivo para criar um commit artificial apenas porque publicamos um novo artigo.

Se decidirmos manter uma melhoria real no código, aí sim registramos a mudança correspondente.

O histórico deve representar a evolução do software, não a quantidade de capítulos publicados.

56. E o CHANGELOG?

A mesma lógica vale para:

  CHANGELOG.md

Se nenhuma mudança relevante permaneceu no software, não precisamos adicionar algo como:

  - Studied std::cout.

O CHANGELOG acompanha mudanças relevantes do projeto.

Ele não é nosso diário de aulas.

Essa separação será importante:

  Medium
│
└── evolução educacional da série
  Git
│
└── evolução do códigoCHANGELOG
│
└── mudanças relevantes do softwareROADMAP
│
└── direção técnica do projeto

Esses elementos estão relacionados.

Mas não possuem a mesma responsabilidade.

57. O problema que está começando a nascer

Observe novamente:

  std::cout << "Health: 100" << std::endl;
std::cout << "Energy: 80" << std::endl;
std::cout << "Coins: 25" << std::endl;

Agora imagine que nosso jogador compre alguma coisa.

Queremos mudar:

  Coins: 25

para:

  Coins: 20

Poderíamos simplesmente alterar o texto manualmente:

  std::cout << "Coins: 20" << std::endl;

Mas isso aconteceu antes do programa executar.

Nosso software não calculou nada.

Não armazenou nada.

Não transformou nada.

O número ainda é apenas parte de uma frase fixa.

Estamos chegando a uma necessidade fundamental:

nosso programa precisa ser capaz de lembrar informações durante sua execução.

Esse problema nos levará às variáveis.

Mas ainda existem alguns passos importantes antes de chegarmos lá.

Primeiro precisamos aprender a observar melhor o que acontece quando nosso código está errado.

Conclusão

No Capítulo 01 conseguimos executar nosso primeiro programa.

Hoje começamos a compreendê-lo.

Descobrimos que:

  #include <iostream>

está relacionado ao pré-processamento e disponibiliza declarações necessárias para nosso uso de I/O.

Descobrimos que:

  int main()

declara a função especial que funciona como ponto de entrada da aplicação que estamos construindo.

Entendemos que:

  {
}

delimita o corpo da função.

Conhecemos:

  std::cout

como nosso fluxo padrão de saída.

Vimos:

  <<

funcionando como operador de inserção nesse contexto.

Identificamos:

  "Game Engineering com C++ começou!"

como um literal de string.

Descobrimos que:

  std::endl

não apenas muda de linha, mas também solicita flush do stream.

Começamos a entender o papel de:

  ;

e relacionamos:

  int main()

com:

  return 0;

Mais importante: começamos a quebrar pequenas partes do código propositalmente.

Nosso ciclo está evoluindo:

  VER
 ↓
FAZER
 ↓
QUEBRAR
 ↓
OBSERVAR
 ↓
ENTENDER
 ↓
CORRIGIR
 ↓
EVOLUIR

Ainda temos somente:

  main.cpp

Mas agora aquele arquivo possui muito mais significado para nós.

E antes de adicionarmos novas funcionalidades, vamos aprofundar justamente a habilidade que começamos a exercitar hoje:

errar de maneira controlada e aprender a investigar o que aconteceu.

Próximo capítulo

Capítulo 03 — Quebrando nosso programa de propósito

No próximo artigo vamos transformar erros em material de estudo.

Vamos provocar problemas controladamente e começar a diferenciar:

  Código correto
      ↓
Compiler
      ↓
Build succeeded

de:

  Código incorreto
      ↓
Compiler
      ↓
Diagnóstico
      ↓
Build failed

Vamos trabalhar com:

  • erros de sintaxe;

  • mensagens do compilador;

  • Error List;

  • Output;

  • arquivo e linha do problema;

  • códigos de diagnóstico;

  • erros em cascata;

  • diferença entre causa e consequência;

  • uma alteração por vez;

  • leitura antes da correção;

  • hipóteses;

  • investigação;

  • correção;

  • validação.

E começaremos a construir uma habilidade que nos acompanhará até o último capítulo desta série:

não ter medo do erro — saber investigá-lo.

Porque fazer um programa funcionar é importante.

Mas entender por que ele deixou de funcionar é uma das habilidades que começam a transformar alguém que apenas escreve código em um engenheiro de software.

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