Capítulo 02 — Entendendo o código que acabamos de executar
Série: Do zero ao jogo comercial com C++ e SFML
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
iostreamnos 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:
coutpertencente ao namespacestd.
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.
Comentários
Ainda não há comentários. Sê o primeiro a comentar!