A Linguagem de Programação C++

Qual o objetivo deste livro?

Este livro virtual gratuito é uma iniciativa da comunidade C/C++ Brasil que pretende trazer uma fonte moderna, confiável e completa para o estudo de C++. Sendo um livro da comunidade, você leitor também pode contribuir com correções e melhorias. Basta participar da nossa comunidade do Telegram e abrir um pull request no nosso repositório. Toda ajuda é bem vinda! :)

O que é C++?

C++ é uma linguagem de programação de propósito geral, compilada, estaticamente tipada, criada por Bjarne Stroustrup na década de 1980. A linguagem sofreu várias atualizações no decorrer dos anos, sendo C++20 a versão mais recente.

Ferramentas necessárias

Apesar de este livro focar no ensino de C++ moderno, é necessário se preocupar com algumas questões técnicas relacionadas ao ferramental. A princípio, é possível utilizar qualquer editor de texto ou IDE (Integrated Development Environment - Ambiente Integrado de Desenvolvimento) para escrever os códigos. É necessário ter em mãos algum compilador para traduzir o código-fonte para código executável. Existem diferentes implementações de compiladores para C++. Na maior parte dos casos, não há distinção de uso entre os compiladores. Nos raros casos onde isso não é verdade, este livro deixará claro no texto as eventuais diferenças.

Fica a cargo do leitor procurar e escolher as ferramentas de preferência para seguir com a leitura. Em ambiente Linux, os compiladores mais comuns são o GCC e o Clang. Em ambiente Windows, os compiladores mais comuns são o Microsoft Visual C++ e o Intel C++ Compiler.

Onde conseguir ajuda

Somos uma comunidade aberta e ativa no Telegram com mais de 700 membros. Novos membros são bem vindos para discussões técnicas sobre C/C++, bibliotecas e ferramentas relacionadas. Temos também nossa organização no Github, que conta com um repositório atualizado com fontes para material extra de aprendizado. Novos conteúdos são bem vindos, e podem ser sugeridos por meio de pull requests.

Capitulo 1: Um programa simples em C++

Olá mundo: Uma versão pragmática

O objetivo deste capítulo é introduzir vários conceitos básicos de programação C++ para iniciantes, ao mesmo tempo. Estes mesmos conceitos serão melhor elaborados mais pra frente. Iremos iniciar com o clássico código "Olá mundo", que segue abaixo. Apesar de simples, esse código tem muito a ensinar, como veremos. Copie o código abaixo para um arquivo de nome ola_mundo.cpp, e salve em algum diretório do seu sistema.

#include <iostream>

int main()
{
   std::cout << "Olá Mundo!\n";
   return 0;
}

Em primeiro lugar, a diretiva #include é a forma de incluir em nosso código as declarações necessárias para utilizar código externo. Ao incluir o header iostream, temos acesso à função cout, dentro do namespace std, que serve para escrever dados na tela durante a execução do programa. Seguindo, encontramos a função main. Essa função define o ponto de partida de qualquer programa C++. Dentro da função main, encontramos a instrução std::cout seguida da mensagem "Olá Mundo", que será mostrada na linha de comando. Por fim, temos o retorno da função main. É importante retornar o valor 0, que significa, por convenção, que o programa terminou com sucesso.

Para executar o programa, é preciso utilizar um programa chamado compilador (compiler), que executará um processo chamado compilação (compilation). Este processo é a tradução do código fonte para "código de máquina". No caso do C++, é a tradução do código escrito na linguagem para código executável por nossos computadores. Tente modificar o programa, trocando a mensagem, removendo partes do código e recompilando, verificando os erros que acontecem. Não tenha medo de errar e nem de gerar erros de compilação. Acredite, eles vão acontecer mesmo quando você estiver usando a linguagem com fluência.

Esta etapa é dependente do ambiente de trabalho que você está utilizando. Apenas para fins ilustrativos, os comandos abaixo mostram o processo de compilação em ambiente Linux, utilizando o GCC.

$ g++ -o ola_mundo ola_mundo.cpp
$ ./ola_mundo
Olá Mundo!

Olá mundo: Uma versão completa

Apesar do capítulo anterior ser suficiente para um primeiro contato com C++, talvez não fique claro outros aspectos ortogonais à linguagem, tais como escolhas de organização do código e do aspecto cultural do código. Desta forma, nosso primeiro projeto começa utilizando a linha de comando para instalar as ferramentas necessárias para a criação de um projeto. Os comandos aqui utilizados serão focados na command line interface (cli) do Unix. Caso você utilize Windows, o Mingw pode ser utilizado.

Configurações de ambiente

Nesta parte serão abordados os programas necessários para ter um "Olá mundo" funcional. O primeiro programa que precisamos instalar é o git - um sistema de versionamento de código. Seu propósito é manter um registro facilmente acessável das alterações feitas em um projeto, de modo que não precisemos perder tempo no futuro buscando informação sobre o código em e-mails, Google Docs ou pendrives (por exemplo). Iniciaremos um repositório git na pasta de projetos e iremos criar um projeto em C++ utilizando o CMake.

Se você utiliza:

  • Mac
    • Instale o XCode Command Line Tools
  • Windows
    • vá em www.git-scm.com e baixe o instalador para Windows
  • Linux / BSD
    • Utilize o gerenciador de pacotes da sua distribuição para instalar o git
      • Debian, Ubuntu e derivados: apt
      • Fedora e derivados: dnf
      • openSUSE e derivados: zypper
      • Arch e derivados: pacman
      • Outras: Verifique na documentação de sua distribuição como fazer a instalação

Após ter o git instalado, precisamos abrir uma área para nosso projeto. Abra um terminal, ou prompt de comandos. No Linux você tem as opções de konsole, gnome-terminal, xterm, urxvt e muitos outros. No Windows o programa vem junto com a instalação do git. Já no Mac, o programa se chama terminal, mas você pode também instalar um substituto melhor chamado iterm2.

Este livro segue a seguinte notação:

  • Linhas iniciando por $: Usuário não-administrador
  • Linhas iniciando por #: Usuário administrador
  • ~: Pasta 'inicial' (home) do usuário

Alguns comandos básicos para o terminal:

  • pwd: Retorna o caminho da pasta em que você está nesse momento
  • cd: Muda de pasta ("Change Directory")
  • mkdir: Cria uma pasta ("Make Directory")
  • touch: Cria um arquivo
  • ls: Lista os arquivos da pasta atual
  • tree: Exibe as pastas em forma de árvore

Sugere-se criar uma pasta de projetos dentro da pasta inicial do seu usuário, conforme exemplo:

$ mkdir Projetos
$ mkdir Projetos/OlaMundo
$ cd Projetos/OlaMundo
$ pwd
/home/user/Projetos/OlaMundo

Utiliza-se o git para criar um novo repositório dentro da pasta recém-criada (veja abaixo). Você pode verificar a existência de uma nova pasta chamada .git utilizando o comando ls. As opções -al servem para exibir arquivos ocultos (-a) e para exibir os arquivo em uma lista vertical (-l).

$ pwd
/home/user/Projetos/OlaMundo
$ git init .
Initialized empty Git repository in /home/user/Projetos/OlaMundo/.git/
$ ls -al
total 12
drwxr-xr-x 3 user user 4096 Nov  9 16:53 .
drwxr-xr-x 3 user user 4096 Nov  9 16:48 ..
drwxr-xr-x 7 user user 4096 Nov  9 16:51 .git

Neste momento, é importante iniciar uma estrutura básica de projeto, com arquivos que não necessariamente compõem o código-fonte. Mesmo sendo um código exemplo, algumas padronizações são importantes em qualquer código para que outros programadores saibam por onde começar a olhar o seu projeto. Crie o arquivo README.md (o nome README.md é reconhecido por diversas ferramentas de gerenciamento de código). O formato .md significa Markdown e é um formato de texto puro que tem alguma informação sobre a formatação do conteúdo.

$ touch README.md
$ ls -la
total 12
drwxr-xr-x 3 user user 4096 Nov  9 17:11 .
drwxr-xr-x 3 user user 4096 Nov  9 16:48 ..
drwxr-xr-x 7 user user 4096 Nov  9 17:01 .git
-rw-r--r-- 1 user user    0 Nov  9 17:11 README.md

Utilize algum "editor de texto pra programação" pra escrever algo no arquivo criado. Evite editores como Microsoft Word, Microsoft Office, LibreOffice ou Wordpad, pois esses não são editores de texto cru, e portanto não trabalham normalmente com arquivos de texto simples. Também evite editores de texto simples demais, como o Notepad (Bloco de notas). Alguns editores sugeridos:

O arquivo deve conter informações básicas do projeto, como seu titulo, uma breve descrição, autor, informações de como compilar o projeto, bibliotecas e ferramentas dependentes, e qualquer outras informações relevantes para outros desenvolvedores. Exemplo:

# Projeto: Olá mundo

Esse projeto serve como esboço do que é necessário para começar
a trabalhar com C++ no mundo real.

o # cria uma linha de cabeçalho, que é o título do arquivo, e parágrafos são espaçados com uma linha em branco. Este livro está sendo escrito em markdown, e o resultado final é o que você está lendo! Adicione o arquivo ao seu repositório:

$ pwd
/home/user/Projetos/OlaMundo
$ git status
On branch master

No commits yet

Untracked files:
  (use "git add <file>..." to include in what will be committed)
        README.md

nothing added to commit but untracked files present (use "git add" to track)

Perceba a linha Untracked files:, essa é a lista de arquivos que o git ainda não está gerenciando. Precisamos adicionar o arquivo no índice de arquivos gerenciados pelo git:

$ pwd
/home/user/Projetos/OlaMundo
$ git add README.md

$ git status
On branch master

No commits yet

Changes to be committed:
  (use "git rm --cached <file>..." to unstage)
        new file:   README.md

O comando executado não adicionou nada ao repositório, apenas adicionou o arquivo à Staging Area, que é a área onde os arquivos ficam preparados para serem adicionados em uma revisão. A seguir, commitamos o arquivo com uma mensagem descrevendo o que foi feito, da seguinte forma:

$ git commit -m "Adicionado arquivo Readme."
[master (root-commit) 478f2b6] Adicionado arquivo Readme.
 1 file changed, 6 insertions(+)
 create mode 100644 README.md

A configuração do projeto pode ser feita utilizando o CMake. O CMake é um gerenciador padrão para projetos em C++, e serve para traduzir as informações do código-fonte, a sua organização em arquivos, e quais subprojetos você está fazendo dentro de seu código, para as ferramentas da linguagem - como compiladores, depuradores (debuggers), IDEs. Assim, prepare um arquivo chamado CMakeLists.txt juntamente com uma pasta chamada src (source), onde ficará o código-fonte do projeto.

$ pwd
/home/user/Projetos/OlaMundo
$ touch CMakeLists.txt
$ mkdir src
$ touch src/main.cpp
$ touch src/CMakeLists.txt

O sistema de arquivos no momento é esse:

.
├── CMakeLists.txt
├── README.md
└── src
    ├── CMakeLists.txt
    └── main.cpp

Perceba que existem 2 arquivos CMakeLists.txt. O que está na pasta raiz do projeto irá definir as configurações de base, e irá adicionar a pasta src no projeto. O conteúdo do arquivo na pasta raiz deve ser conforme segue:

cmake_minimum_required(VERSION 3.19)
project(OlaMundo CXX)

add_subdirectory(src)

Onde a primeira linha define a versão minima de CMake necessária para trabalhar com o projeto. A segunda linha especifica o nome do projeto para o CMake. A última linha adiciona a pasta src ao projeto. Nessa pasta, encontra-se outro arquivo CMakeLists.txt, contendo configurações de compilação.

add_executable(HelloWorld)

target_sources(
    HelloWorld
    PRIVATE
        main.cpp
)

target_compile_features(
    HelloWorld
    PRIVATE
        cxx_std_17
)

O comando add_executable determina que esse projeto irá gerar um executável HelloWorld. Esse executável será gerado a partir da compilação dos arquivos determinados em target_sources (ou seja, o arquivo main.cpp). Por fim, target_compile_features especifica as "features" necessárias para produzir o executável. No exemplo, utiliza-se a versão C++17 para a compilação do projeto. Sugere-se verificar a tabela encontrada no cppreference para verificar as features disponíveis em cada versão do C++, para os compiladores mais comuns. Por fim, o conteúdo do arquivo src/main.cpp é mostrado abaixo.

#include <iostream>
#include <cstdlib>

int main() {
    std::cout << "Olá mundo!\n";
    return EXIT_SUCCESS;
}

A única diferenca deste código para milhares de códigos de outros livros é a adição do header cstdlib para o EXIT_SUCCESS. cstdlib possui EXIT_SUCCESS e EXIT_FAILURE para indicar quando algo deu errado ou não, e mesmo que não modifique o funcionamento do código (EXIT_SUCCESS é definido como 0), é uma forma de explicitar o valor de retorno ao leitor.

Para determinar como o CMake irá configurar nossos projetos, é sugerido criar uma configuração base. A partir da versão 3.19, o CMake possui um comando --preset que utiliza uma configuração predeterminada. Crie um arquivo chamado CMakePresets.json, na pasta raiz do projeto:

{
    "version": 1,
    "cmakeMinimumRequired": {
      "major": 3,
      "minor": 19,
      "patch": 0
    },
    "configurePresets": [
        {
            "name": "debug",
            "displayName": "Compilacao Debug",
            "description": "Compila em modo debug usando make",
            "generator": "Unix Makefiles",
            "binaryDir": "${sourceDir}/build/debug",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Debug"
            }
        },
        {
            "name": "release",
            "displayName": "Compilacao Release",
            "description": "Compila em modo release utilizando o Ninja",
            "generator": "Ninja",
            "binaryDir": "${sourceDir}/build/release",
            "cacheVariables": {
                "CMAKE_BUILD_TYPE": "Release"
            }
        }
    ]
}

Com essa configuração criamos dois geradores:

  • Debug (Utilizando Unix Makefiles): A escolha do Unix Makefiles para o gerador de Debug é por ele ser sequencial, onde cada arquivo espera sua vez para ser compilado, facilitando a identificação de erros.
  • Release (Utilizando Ninja): A escolha do Ninja para o gerador de Release possibilita uma compilação paralela, onde múltiplos arquivos são compilados ao mesmo tempo, tornando a compilação mais rápida.

Por fim, para configurar o projeto utilizando o preset escolhido:

$ pwd
/home/user/Projetos/OlaMundo
$ ls
CMakeLists.txt  CMakePresets.json  README.md  src
$ cmake . --list-presets
Available presets:

  "debug"   - Compilacao Debug
  "release" - Ninja

$ cmake . --preset=debug
Preset CMake variables:

  CMAKE_BUILD_TYPE="Debug"

-- The C compiler identification is GNU 10.2.0
-- The CXX compiler identification is GNU 10.2.0
-- Detecting C compiler ABI info
-- Detecting C compiler ABI info - done
-- Check for working C compiler: /usr/bin/cc - skipped
-- Detecting C compile features
-- Detecting C compile features - done
-- Detecting CXX compiler ABI info
-- Detecting CXX compiler ABI info - done
-- Check for working CXX compiler: /usr/bin/c++ - skipped
-- Detecting CXX compile features
-- Detecting CXX compile features - done
-- Configuring done
-- Generating done
-- Build files have been written to: /home/user/Projetos/helloworld/build/debug

A última linha indica onde os arquivos de build do projeto foram gerados. Esta se refere à linha definida em binaryDir no arquivo de preset. Para compilar o programa, deve-se executar cmake --build. Por exemplo, para compilar o projeto em debug:

$ cmake --build ./build/debug
Scanning dependencies of target HelloWorld
[ 50%] Building CXX object src/CMakeFiles/HelloWorld.dir/main.cpp.o
[100%] Linking CXX executable HelloWorld
[100%] Built target HelloWorld

O objetivo desse capítulo foi dar uma longa introdução ao C++ considerando várias ferramentas comuns em ambiente de trabalho profissional. Para maiores detalhes em relação às ferramentas, sugere-se buscar as documentação das mesmas, pois a explicação detalhada de cada uma foge do escopo desse livro.

Conceitos Básicos

Esse capítulo traz uma sequência inicial e básica de conteúdos para começar a fazer programas em C++.

Variáveis

Uma variável é um meio de armazenar valores e atribuir nomes a eles; de modo mais técnico, variáveis são um espaço de memória no seu computador usado para guardar informações, que podem ser de vários tipos: números, caracteres, texto, entre outros. Graças a isso, diz-se que C++ é uma linguagem de programação tipada.

Em C++, todas as variáveis possuem um tipo, e o tipo da variável não muda, até que ela deixe de existir (nos referimos a isso como o "tempo de vida", ou "lifetime", de uma variável). Diz-se então que C++ é uma linguagem de programação estaticamente tipada.

Variáveis são definidas da seguinte forma:

int main()
{
    int some_int = 8;
    // A variávei de nome some_int é do tipo inteiro (int) e possui o valor 8
    float some_float = 9.2;
    // A variávei de nome some_float é do tipo ponto flutuante (float) e possui o valor 9.2
    char some_char = 'a';
    // A variávei de nome some_int é do tipo caractere (char) e possui o valor 'a'
    bool some_bool = true;
    // A variávei de nome some_int é do tipo booleano (bool) e possui o valor true

    return 0;
}

O tempo de vida de uma variável é delimitado por seu escopo. O escopo de uma variável é determinado pelo bloco que a contém, delimitado por chaves ({}). O exemplo anterior possui quatro variáveis, cada uma de um tipo diferente, e todas elas estão no escopo da função "main". Uma variável chega ao fim do seu tempo de vida ao final do escopo que a contém. Se uma variável está fora de qualquer escopo, ela é chamada de variável global.

Alternativamente, C++ moderno (do padrão C++11 em diante) traz uma sintaxe alternativa para definir o tipo das variáveis, por meio da palavra chave auto. Dessa forma, não é necessário explicitar o tipo das variáveis. Note que a variável ainda possui um tipo bem definido. A única diferença é que o tipo é determinado automaticamente em tempo de compilação (compile time), através de um processo chamado inferência:

int main()
{
    auto some_int = 8;
    auto some_float = 9.2;
    auto some_char = 'a';
    auto some_bool = true;
}

Em C++, o tipo string, para representar textos, não é um tipo primitivo. Para isso, existe o tipo std::string na biblioteca padrão do C++ (stdlib). Para utilizá-lo, é necessário incluir o header <string>, da mesma forma que incluímos <iostream> anteriormente para podermos escrever na tela.

#include <string>

int main()
{
    std::string some_string = "First string";
    
    // ... alternativamente, usando `auto`:
    auto some_string_2 = std::string("Second string");

    // ... ou, usando `auto` e `string literals`:
    using namespace std::string_literals;
    auto some_string_3 = "Third string"s; // Note o `s` após a string!
}

Note que um literal de texto entre aspas duplas como "my string" não é do tipo std::string. Isto causa um pouco de confusão, e será melhor explicado adiante. Por ora, mantenha em mente as formas de declarar vistas acima (perceba o sufixo "s" no terceiro caso utilizando auto!)

Instruções

Instruções em C++ são conceitos de alto nível (isto é, conceitos simples de serem entendidos por humanos) e servem para dividir a execução do código em etapas. Elas não devem ser confundidas com as instruções de um processador, que são de baixo nível (mais difícil de entender por humanos, porém fácil para uma máquina executar).

Existem vários tipos de instruções, as mais comuns sendo as instruções de expressão (expression statements). Instruções de expressão, também chamadas expressões, são operações básicas como x + 2, a + 5 * 3 e pi = atan(i) * 4, seguidas de um ;. Ao declarar variáveis, nós utilizamos instruções de declaração (declaration statements), também chamadas declarações.

Instruções compostas

Saber diferenciar todos os tipos de instrução é raramente necessário, mas as instruções compostas (compound statements) merecem atenção. Instruções compostas agrupam zero ou mais instruções em uma só.

O corpo de uma função comum, que veremos mais adiante, é uma instrução composta contendo quatro instruç:

int main()
{ // Início da instrução composta
    int x = 2;
    int y = 3;
    int z = x + y;

    return 0;
} // Fim da instrução composta

Você verá, mais pra frente, muitos outros casos em que há a necessidade de utilizar esse tipo de instrução.

Estruturas de controle

A estrutura "if" / "else if" / "else"

Estruturas de controle (control structures, control flow) são construções da linguagem que nos permitem fazer coisas mais complexas do que apenas executar instruções de maneira sequencial. A primeira que veremos é a estrutura if/else if/else. Ela nos permite tomar decisões e executar instruções diferentes dada uma ou mais condições. No exemplo abaixo, temos um programa simples que informa o preço que um cliente deve pagar em um cinema onde:

  • Clientes abaixo de 10 anos de idade não pagam.
  • Clientes acima de 50 anos pagam meia-entrada.
  • Demais clientes pagam o valor normal da entrada.
#include <iostream>

int main()
{
   int age = 0;
   double const price = 20.0;
   std::cout << "Insira sua idade: ";
   std::cin >> age;

   std::cout << "Sua idade é " << age << '\n';

   if (age <= 10) {
       std::cout << "Você não precisa pagar!\n";
   } else if (age > 50) {
       std::cout << "Você paga apenas " << price/2 << "!\n";
   } else {
       std::cout << "Você paga " << price << ".\n";
   }
}

O exemplo acima introduz também uma forma de ler dados de entrada (input) providenciados pelo usuário. Note que a variável age é inicializada com o valor 0. Porém, a linha std::cin >> age; permite ao usuário que estiver executando o programa informar um novo valor para age. Desta forma, o valor de age que será utilizado pelo programa será conhecido apenas durante a execução.

No exemplo, a linha 10 sempre será executada depois que o usuário informar o valor de age. Porém, a linha 13 será executada apenas se (if) a variável age tiver um valor abaixo de 10. A construção else if permite fazer um novo teste caso o teste anterior falhe, ou seja, a condição não seja verdadeira. Por outro lado, utilizar apenas else faz com que, caso nenhuma das condições anteriores tenha sido satisfeita, o código dentro do bloco seguinte execute garantidamente. Experimente alterar o valor de age para fazer com que o programa siga cada uma das possibilidades.

As estruturas while e for

As estruturas de controle while e for servem para repetir um bloco de código até que uma condição seja satisfeita. Este tipo específico de estrutura de controle se chama laço (loop). No exemplo abaixo, o programa pergunta se o usuário deseja aprender C++. Enquanto (while) a resposta não for s (sim) nem n (não), a mesma pergunta aparecerá para o usuário. Uma vez que o usuário der uma resposta válida, um if imprime a reação correspondente à resposta do usuário.

#include <iostream>

int main()
{
   char answer = '\0';

   while (answer != 's' && answer != 'n') {
       std::cout << "Você deseja aprender C++? [s/n]: ";
       std::cin >> answer;
   }

   if (answer == 's') {
       std::cout << "Esse é o espírito!\n";
   } else {
       std::cout << "Que triste...\n";
   }
}

Em geral, usamos o laço while quando não existe uma definição clara de quantas vezes precisamos rodar o laço, apenas uma condição de continuidade que deve em algum momento posterior tornar-se falsa.

O laço for adiciona legibilidade ao código quando temos uma inicialização e um passo que deve ser executado em toda iteração do laço. Tudo que é feito com um laço for pode também ser feito com um laço while, e vice versa. A diferença entre eles é que, em alguns casos, a leitura do código fica mais natural com um ou com outro. Ao contrário do while, o for se presta mais quando sabemos exatamente quantas vezes precisamos repetir as operações. No código abaixo, o laço for itera com valores de i = a até valor de i = b, somando 1 ao valor de i a cada iteração. Uma forma alternativa e mais compacta de escrever i = i + 1 seria escrever ++i.

#include <iostream>

int main()
{
   int a = 0;
   int b = 0;

   std::cout << "Digite o valor de A: ";
   std::cin >> a;

   std::cout << "Digite o valor de B: ";
   std::cin >> b;

   int sum = 0;

   for (int i = a; i <= b; i = i + 1) {
       sum += i;
   }

   std::cout << "Sum [" << a << ", " << b << "] = " << sum << std::endl;

   return 0;
}

As instruções continue e break

Quando utilizamos um laço, é possível que queiramos interrompê-lo prematuramente. Para isso existe a instrução break:

#include <iostream>

int main()
{
    for (int i = 1; i <= 100; i = i + 1) {
        std::cout << "Iteração #" << i << '\n'
                  << "Você deseja continuar? [s/n]: ";

        char op;
        std::cin >> op;
        if (op == 'n')
            break;
        
        std::cout << "Incrementando i..." << std::endl;
    }

    return 0;
}

Assim que a execução do programa atinge a instrução break, o laço é imediatamente interrompido; o i deixa de ser incrementado e vamos direto para nosso return.

Caso queiramos interromper a execução do corpo (a instrução composta, delimitada por chaves) de nosso laço mas continuar iterando, devemos utilizar a instrução continue:

#include <iostream>

int main()
{
    for (int i = 1; i <= 100; i = i + 1) {
        std::cout << "Iteração #" << i << '\n'
                  << "Você deseja pular essa iteração? [s/n]: ";

        char op;
        std::cin >> op;
        if (op == 's')
            continue;
        
        std::cout << "Executando o resto do corpo do laço" << std::endl;
    }

    return 0;
}

Perceba que, como i = i + 1 não faz parte do corpo do laço, ele é executado independentemente do continue. Lembre-se que isso não se aplica ao break.

É possível que você não veja vantagens imediatas no uso dessas duas instruções, mas elas podem simplificar o código e às vezes são até necessárias.

Devo utilizar { e }?

Você pode ter percebido que o corpo das instruções if (e suas partes else), for e while podem ser tanto uma instrução simples quanto uma instrução composta. Quando estas estruturas de controle consistem apenas de uma instrução, não é obrigatório utilizar chaves. Por exemplo, os dois if seguintes são equivalentes:

if (a) {
    b = 5;
}

if (a)
    b = 5;

Agora, quando precisamos de mais de uma instrução, precisamos agrupá-las como uma só:

if (a) {
    b = 5;
    c = 6;
} // Ok! Ambas as atribuições fazem parte do if

if (a)
    b = 5;
    c = 6; // Ops! Essa instrução não faz parte do if 

Alguns programadores optam por utilizar sempre instruções compostas para evitar surpresas. Há bons argumentos tanto a favor quanto contra isso, portanto ao trabalhar em um projeto preexistente siga o padrão de estilo que já esteja sendo utilizado.

Funções

Para deixar partes do programa reutilizáveis, e para adicionar legibilidade ao código, utilizamos funções. Funções são pedaços de código que são definidas uma vez em um lugar, mas que podem ser reutilizadas quantas vezes quiser em outros lugares do código.

Elas servem para que não precisemos reescrever o mesmo código múltiplas vezes, e permitem que sigamos o princípio do Não Se Repita (Don't repeat yourself - DRY).

Funções podem ou não receber parâmetros, que são usados para levar informações de um escopo para outro. Os parâmetros são, por padrão, cópias das variáveis que são levadas ao escopo da função invocada, e eles aparecem entre parênteses após o nome da função, separados por vírgula.

Funções possuem também um valor de retorno, que possibilita capturar uma variável concebida dentro do escopo da função para o local onde ela foi chamada. Abaixo um exemplo de função:

#include <iostream>

int calculate_sum(int a, int b)
{
   int sum = 0;
   for (int i = a; i <= b; ++i) {
       sum += i;
   }
   return sum;
}

int main()
{
   int a = 0;
   int b = 0;
   std::cout << "Digite o valor de A: ";
   std::cin >> a;
   std::cout << "Digite o valor de B: ";
   std::cin >> b;

   std::cout << "Sum [" << a << ", " << b << "] = " << calculate_sum(a, b) << std::endl;

   return 0;
}

No código acima, foi definida uma função calculate_sum, que necessita de dois parâmetros do tipo int a e b, e que efetua a soma dos valores de a a b, e retorna essa soma (por exemplo, se a for 2 e b for 5, a soma será 2 + 3 + 4 + 5, e o valor retornado será 14). O tipo de retorno de uma função é determinado em sua assinatura. A assinatura de uma função é o conjunto contendo seu nome, seu tipo e seus parâmetros, por exemplo, int calculate_sum(int a, int b).

As funções devem ser pelo menos declaradas antes de serem utilizadas. Por exemplo, se movermos a definição da função calculate_sum para baixo da função main, teriamos um erro de compilação. Existe uma distinção entre declarar uma função e definir uma função. Declarar uma função é apenas mostrar que ela existe, sem determinar seu funcionamento (ou seja, sem adicionar o corpo de código dela). Definir uma função é adicionar um corpo de código a uma declaração.

No exemplo acima, estamos definindo a função calculate_sum. Um exemplo de separação entre a declaração e a definição de função é mostrado abaixo.

#include <iostream>

// Declaração da função `calculate_sum`
int calculate_sum(int a, int b);

int main()
{
   int a = 0;
   int b = 0;
   std::cout << "Digite o valor de A: ";
   std::cin >> a;
   std::cout << "Digite o valor de B: ";
   std::cin >> b;

   std::cout << "Sum [" << a << ", " << b << "] = " << calculate_sum(a, b) << std::endl;

   return 0;
}

// Definição da função `calculate_sum`
int calculate_sum(int a, int b)
{
   int sum = 0;
   for (int i = a; i <= b; ++i) {
       sum += i;
   }
   return sum;
}

Caso a função não retorne nenhum valor, é possível utilizar a palavra reservada void, conforme exemplo abaixo. Funções com tipo de retorno void podem (opcionalmente) omitir a chamada return, indicando o término da função. Enquanto funções com retornos específicos são particularmente úteis para obter o resultado de uma operação para que façamos algo com ele, funções do tipo void são particularmente úteis para executar uma ação. No exemplo anterior, queríamos obter um resultado, um número que representa uma soma, e fizemos algo mais com ele em main(). No exemplo seguinte, a única ação sendo executada é printar na tela o texto "Olá Mundo!", e não queremos fazer nenhuma operação com este texto depois que a função é executada.

void say_hello()
{
   std::cout << "Olá Mundo!" << std::endl;

   // Opcional:
   // return
}

Tipos definidos pelo usuário

Structs (estruturas) são uma forma de adicionar tipos novos ao programa (user-defined types). Eles fornecem mais semântica a grupos de variáveis que se relacionam de alguma forma, agrupando múltiplas propriedades para que sejam usadas de uma vez.

No exemplo abaixo, uma estrutura de nome Point é definida entre as linhas 4 e 7. Ela possui dois membros: x e y, ambos do tipo primitivo double. Essas linhas definem um novo tipo de dado de nome Point, que pode ser utilizado em qualquer lugar do programa. As linhas 16 e 17 criam duas instâncias de duas variáveis com o novo tipo Point.

#include <iostream>
#include <cmath>

struct Point {
   double x;
   double y;
};

double calculate_distance(Point p1, Point p2)
{
   return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
}

int main()
{
   Point p1{1.1, 2.7};
   Point p2{3.7, 4.4};

   std::cout << "A distância entre os pontos p1 e p2 é " << calculate_distance(p1, p2) << std::endl;

   return 0;
}

Assim como qualquer outro tipo de variável, é possível utilizar o tipo Point como parâmetro para funções, como é o caso do exemplo acima, na linha 9. A função calculate_distance necessita de dois parâmetros, p1 e p2, ambos do tipo Point para ser invocada. Note que, ao invocar a função na linha 19, as duas variáveis p1 e p2 são copiadas para o escopo da função calculate_distance. Ou seja, se alterarmos os valores de p1 e p2 dentro do corpo da função calculate_distance, os valores de p1 e p2 presentes no escopo da função main não serão alteradas.

Note que, por Point ser um tipo como qualquer outro, é possível compor estruturas aninhadas. Por exemplo, considere o seguinte problema: Você está desenvolvendo um jogo de plataforma, e precisa representar o personagem em um espaço bidimensional. O personagem possui uma posição e um número de vidas. Utilizando a estrutura anterior para representar a posição, poderia-se escrever:

using Life = unsigned int;

struct Player {
    Point position;
    Life life;
};

Como exemplo ilustrativo, utiliza-se aqui a palavra reservada using para criar um type alias (um "apelido" para um tipo qualquer). Basicamente, dá-se um novo nome para um tipo que já existe. No exemplo acima, Life será equivalente ao tipo unsigned int, ou seja, um inteiro não negativo. Deste modo, toda vez que usarmos o tipo Life, estaremos criando, por trás dos panos, um unsigned int.

Outro modo de criarmos tipos definidos pelo usuário é por meio de classes, que veremos mais à frente.

Referências

Referências permitem criar "apelidos" para variáveis, isto é, uma referência para uma variável acessa o valor daquela variável, nomeada de outra forma. Considere o código abaixo, onde é criada uma referência de nome b para uma variável de nome a. Utiliza-se o simbolo & junto ao tipo da variável para declarar uma referência. Diferente de uma variável "comum", alterar o valor da referência altera o valor da variável referenciada. Portanto, no exemplo abaixo, o resultado vai ser a mensagem a = 7, b = 7.

#include <iostream>

int main()
{
    int a = 1;
    int& b = a;
    // Já que a variável 'b' se refere a 'a',
    b = 7;
    // então atribuir '7' para 'b' significa atribuir '7' para 'a'.
    std::cout << "a = " << a << ", b = " << b << std::endl;

    return 0;
}

Como uma referência é um "apelido" para outra variável, é impossível criar uma referência para um valor. Por exemplo, int& b = 9; não faz sentido, e ocasiona em um erro de compilação.

É possível usar referências também na passagem de parâmetros de funções. Quando não são utilizadas referências nos parâmetros de uma função, as variáveis são copiadas para o escopo da função. Referências passadas por parâmetro e modificadas dentro do escopo de uma função alteram o valor da variável original de onde foi chamada, como mostra o exemplo abaixo.

#include <iostream>

void wont_change_data(int a)
{
    a = 1;
}

void will_change_data(int& a)
{
    a = 1;
}

int main()
{
    int my_a = 8;
    std::cout << my_a << std::endl;
    // Saída: 8
    wont_change_data(my_a);
    std::cout << my_a << std::endl;
    // Saída: 8
    will_change_data(my_a);
    std::cout << my_a << std::endl;
    // Saída: 1
}

Outro exemplo pode observado no código abaixo. Imagine que Matrix é uma estrutura de dados que representa uma matriz 100x100. A função sum_elements soma todos os elementos dessa matriz.

O loop na função main foi criado sinteticamente para representar muitas chamadas da função sum_elements. Para cada invocação da função sum_elements, uma instância da estrutura de dados Matrix é copiada para o contexto da função, apenas para somar o valor de cada um de seus elementos.

/**
 * Soma dos elementos de uma matriz 100x100
 * :param Matrix matrix:
 * :returns double:
 */
double sum_elements(Matrix matrix)
{
    double sum = 0.;
    for (int i = 0; i < 100; ++i) {
        for (int j = 0; j < 100; ++j) {
            sum += matrix[i][j];
        }
    }
    return sum;
}

int main()
{
    // ...
    double total_sum = 0.;
    for (int i = 0; i < 1000000; ++i) {
        total_sum += sum_elements(matrix);
    }
    std::cout << "Total Sum = " << total_sum << std::endl;

    return 0;
}

Uma forma de evitar a cópia dessa estrutura de dados é utilizando uma referência para uma Matrix, ou seja, trocando a assinatura da função para double sum_elements(Matrix& matrix). Apesar de resolver o problema das cópias desnecessárias, a solução mencionada não é ideal, pois passando uma referência à estrutura Matrix, os dados da estrutura podem ser modificados dentro do escopo da função.

Normalmente, é interessante evitar funções com esse tipo de "efeito colateral" ("side effects"), pois à medida que o projeto cresce, isso pode gerar um problema de manutenibilidade. Para ter noção do quanto essas cópias fazem diferença nesse caso, considere a comparação abaixo, extraída a partir de benchmark feito no site Quick Bench.

Benchmark

Existem sim alguns programas que usam referências como parâmetros de funções e modificam seus dados. Não há problemas em fazer isso, desde que seja bem documentado ou seja óbvio ao leitor. Por exemplo, no caso acima, não é esperado que uma função de nome sum_elements altere os elemento da matriz, porém uma função chamada void sum_one_to_all_elements_in(Matrix& m) deixa mais claro que todos os elementos da matriz serão alterados.

Uma alternativa para evitar o problema comentado é o uso do modificador const, que faz com que a "variável" em questão seja constante, ou seja, não possa ser alterada durante a execução do programa. Antes de partir para seu uso no caso da função sum_elements, considere o uso de const em um exemplo mais simples:

#include <iostream>

int main()
{
   double const a = 8;
   a = 9; // Erro de compilação
}

O código acima não irá compilar. Isso porque a variável "a" é uma constante, ou seja, não se pode alterar seu valor, como se está tentando fazer na linha 6. Casos comuns de uso de const são, por exemplo, as variáveis matemáticas PI e e. O modificador const pode também ser utilizado em conjunto com referências, criando-se referências constantes, ou seja, não é possível alterar o valor da variável referenciada.

#include <iostream>

int main()
{
   double a = 8;
   double const& b = a;
   a = 9; // OK. `a` não é `const`. (b também assume o valor `9`)
   b = 7; // Erro de compilação: `b` é uma referência const
}

O código acima não irá compilar. O motivo é que não se pode alterar o valor de "a" por meio da referência "b", pois ela é uma referência const. Um detalhe importante a se comentar é que o modificador const se aplica sempre ao tipo à sua esquerda. Se não houver nada à esquerda, ele se aplica ao tipo à direita. Dessa forma temos:

  • double const& my_data: Cria uma referência para um const double
  • const double& my_data: Cria uma referência para um const double (Igual à anterior)
  • double &const my_data: Erro de compilação, pois não faz sentido ter uma "referência const", visto que referências não são modificáveis

Por fim, voltando ao problema da matriz, quando escrevemos double sum_elements(Matrix& matrix), evitamos que a matriz seja copiada, e isso é um ponto positivo, em termos de performance. Porém, introduzimos um possível problema: se a matriz for modificada dentro do escopo da função sum_elements, então a matriz do escopo da função main também será modificada.

Por convenção, não esperamos que ninguém vá de fato modificar os elementos da matriz naquela função. É possível garantir isso utilizando const&, isto é, se alguém por engano alterar a variável matrix de dentro da função sum_elements, um erro de compilação ocorrerá. A assinatura da função fica da seguinte forma: double sum_elements(Matrix const& matrix), e nenhuma outra modificação é necessária.

Ponteiros e smart pointers

Quando definimos uma variável em um programa C++, ela é alocada em uma posição de memória do computador. Um "endereço" é a localização em memória virtual de uma variável.

Apenas para fins ilustrativos, imagine que toda a memória do seu computador é uma grande tabela. Cada item da tabela possui um endereço e um valor, como no exemplo abaixo. O código de exemplo captura o endereço da variável a e o coloca como valor da variável b: A linha 6 cria a variável b, do tipo int*, e inicializa essa variável com o endereço de memória da variável a.

#include <iostream>
 
int main()
{
   int a = 1;
   int* b = &a;
 
   std::cout << b << std::endl;
 
   return 0;
}
| Endereço  | Valor     | Variável associada |
|---        |---        |---                 |
| 0x7ff0    | 1         | a                  |
| 0x7ff4    | 0x7ff0 	| b                  |

Os endereços na tabela estão em hexadecimal, pois é a forma comum de representar endereços de memória. São valores meramente ilustrativos, de forma que, caso você execute o programa no seu computador, irá ter resultados diferentes.

Apesar do uso do mesmo simbolo (&), nesse caso &a não é uma referência. Essa distinção é importante: Caso & esteja ao lado de um tipo (int, double, Point, etc...) tem-se uma referência. Por outro lado, caso & esteja ao lado do nome de uma variável (como no caso acima), lê-se "endereço de..." (no exemplo, "endereço de a").

O operador * aplicado a uma variável cujo tipo é um ponteiro, retorna o valor apontado por aquela variável, da seguinte forma:

#include <iostream>
 
int main()
{
   int a = 1;
   int* b = &a;
   *b = 9;
   std::cout << a << std::endl;
   // Saída: 9
   std::cout << b << std::endl;
   // Saída: um endereço de memória (como 0x7ffcf973efb8 por exemplo)
   std::cout << *b << std::endl;
   // Saída: 9
   return 0;
}

As variáveis criadas até agora nos exemplos desse livro são todas criadas numa região de memória denominada stack. O gerenciamento dessa memória é feito automaticamente, ou seja, as variáveis são criadas e alocadas na região da stack ao entrar em determinado escopo e, ao sair do mesmo, as variáveis são automaticamente removidas.

Outra forma de alocar memória é por meio das funções malloc e new. No geral, malloc é o jeito antigo de gerenciar memória, herdado do C. Em C++, sugere-se sempre utilizar new para alocação dinâmica de memória, ao invés de malloc, pois para instâncias de classes o new chama o construtor, fazendo a inicialização da variável, como será visto em capitulos posteriores.

Variáveis alocadas com new e malloc são colocadas em uma região de memória denominada heap. Diferente da stack, essas variáveis não são removidas automaticamente ao saírem do escopo corrente. O gerenciamento do tempo de vida desse tipo de variável deve ser feito pelo programador. O programador deve devolver a memória ao sistema operacional após término de uso da variável por meio da função delete, como no exemplo abaixo:

#include <iostream>
 
int main()
{
   int* a = new int(3);
   std::cout << "a = " << *a << std::endl;
   delete a;
 
   return 0;
}

A linha 5 faz a alocação dinâmica de uma variável do tipo int, inicializada com valor 3, e atribui o endereço dessa variável alocada para a variável a de tipo int*. A linha 6 mostra o valor apontado por a. A linha 7 deleta (devolve) a memória alocada na linha 5. Não deve-se utilizar uma variável depois que foi deletada. No caso de sistemas operacionais de propósito geral, como Windows e Linux, toda a memória alocada que não foi deletada é automaticamente devolvida no momento que o programa termina.

Gerenciar memória da heap não é um problema trivial. O mau gerenciamento da memória pode ocasionar em uma excessiva utilização da memória RAM, como é o caso dos vazamentos de memória (memory leaks), que é quando o programador "esquece" de devolver a memória.

Smart pointers são estruturas de dados que auxiliam no gerenciamento de dados alocados na heap. Basicamente, a estrutura aloca a variável na heap e, ao não ser mais utilizada, a remove automaticamente. De um modo geral, é boa prática de C++ utilizar smart pointers em abundância.

O exemplo abaixo é trivial, e apenas mostra o funcionamento da infraestrutura, mas não consegue captar por completo o valor de usar smart pointers. O uso de ponteiros e alocação dinâmica é especialmente relevante nos casos de interfaceamento com bibliotecas de baixo nível (por exemplo, funções C), ou no caso de uso de polimorfismo em programação orientada a objetos em C++, tema que será discutido posteriormente nesse livro.

#include <iostream>
#include <memory>

int main()
{
   auto a = std::make_unique<int>(7);
 
   std::cout << "a = " << *a << std::endl;

   // A variável `a` será deletada automaticamente quando o escopo terminar
}

const, constexpr e consteval

Conforme apresentado no capítulo 2.6, o modificador const pode ser utilizado para criar constantes (variáveis com acesso apenas de leitura). Alterar uma variável que contém o modificador const causa um erro de compilação:

int main()
{
    const int i = 10;
    i = 11; // Erro de compilação: Atribuição à uma variável somente-leitura
    return 0;
}

Este capítulo apresenta a diferença entre os modificadores const, constexpr e consteval, considerando seu uso em relação à variáveis e funções. O uso do modificador constinit e do modificador const em relação a métodos de classes será abordado apenas em capítulos posteriores.

Para compreender a diferença entre os modificadores, considere o seguinte código:

#include <iostream>

int sum(int a, int b) // Case (a)
// constexpr int sum(int a, int b) // Caso (b)
{
    int c = a + b;
    return c;
}

int main()
{
    const int a = 1;
    // int a = 1; // Caso (c)
    int x = sum(a, 2);
    std::cout << x;
}

Ao invocar a função sum, a soma dos valores a e b será computada. Esse calculo será feito em tempo de execução (runtime). Isso significa que a soma será efetuada apenas quando o usuário executar o programa. Por outro lado, ao utilizar constexpr (introduzido no C++11) na função sum, a função potencialmente será executada em tempo de compilação (compile time), ou seja, o valor de x talvez seja computado no momento de compilação do programa, caso as informações necessárias para tal estejam disponíveis.

Para compreender esse processo, é importante lembrar que todo código C++ é compilado, gerando um arquivo binário que contém o código de máquina. Esse arquivo binário contém instruções em linguagem de baixo nível específicas para cada processador. Mostra-se abaixo uma comparação de dois trechos de código de baixo nível gerados pelo exemplo anterior para o caso (a) sem o uso de constexpr e para o caso (b) com o uso de constexpr:

a) Sem o uso de constexpr:

mov     esi, 2
mov     edi, 1
call    sum(int, int)
mov     DWORD PTR [rbp-8], eax

b) Com o uso de constexpr:

mov     DWORD PTR [rbp-8], 3

Não se preocupe caso não compreenda totalmente os códigos de máquina apresentados acima; trata-se de uma linguagem chamada assembly, extremamente próxima da linguagem de máquina. O código assembly está sendo usado apenas para ilustrar a diferença de código gerado: menos linhas aqui significa código mais eficiente. No caso (a), a linha call sum(int, int) mostra que a função sum será de fato invocada em tempo de execução. Já no caso (b), uma única linha é gerada, contendo o resultado (3) que foi, portanto, calculado em tempo de compilação e por isso aparece diretamente no código gerado.

É importante notar que constexpr só será executado em tempo de compilação caso seja possível conhecer todos os dados necessários para sua execução. Ainda no exemplo, considerando o caso (c), nota-se que utilizar uma variável não-const em uma função contendo o modificador constexpr ocasionará no seguinte código de máquina gerado:

c) Com uso de constexpr, porém, a variável a não é const

mov     DWORD PTR [rbp-4], 1
mov     eax, DWORD PTR [rbp-4]
mov     esi, 2
mov     edi, eax
call    sum(int, int)
mov     DWORD PTR [rbp-8], eax

Uma forma de evitar que uma função constexpr gere valores em tempo de execução inesperadamente, é utilizar consteval (a partir do C++20). Diferente de constexpr, consteval ocasiona em um erro de compilação caso não consiga processar a função em tempo de compilação. Por exemplo:

#include <iostream>

consteval int sum(int a, int b)
{
    int c = a + b;
    return c;
}

int main()
{
    // int a = 1; // Caso (a), gera erro de compilação
    constexpr int a = 1; // Caso (b)
    // const int a = 1; // Caso (c)
    int x = sum(a, 2);
    std::cout << x;
}

No exemplo acima, o caso (a) ocasionará em um erro de compilação. Os casos (b) e (c) geram o valor da variável x em tempo de compilação garantidamente. Note que, apesar dos casos (b) e (c) produzirem o mesmo código nesse caso, o uso de const e de constexpr não é equivalente, como será visto a seguir.

Basicamente, o modificador const aplicado a uma variável declara que aquela variável não será modificada em tempo de execução. O modificador constexpr aplicado a uma variável declara que aquela variável será computada em tempo de compilação e não será modificada em tempo de execução. Dessa forma, o caso (a) abaixo irá compilar normalmente, pois a variável a irá conter uma cópia constante da variável var, em tempo de execução. Já o caso (b) não irá compilar, pois a variável a não pode ser processada em tempo de compilação, visto que a variável var poderia ter sido modificada em algum momento no código.

int main()
{
    int var = 1;

    const int a = var; // Caso (a)
    // constexpr int a = var; // Caso (b)
}

Namespaces

Considere o seguinte cenário: você está ajudando no desenvolvimento de um software de um painel digital para uma loja. O painel mostra mensagens de promoções primárias da loja e, agora, deseja-se adicionar um painel secundário para exibir promoções secundárias. Você foi designado a implementar uma função que toma como parâmetro uma mensagem e a mostra no display do novo painel. Para tal, você começa a escrever o código:

void write(std::string const& message)
{
    // ...
}

Para sua surpresa, já existe uma outra função com exatamente a mesma assinatura, e você recebe o seguinte erro de compilação: error: redefinition of 'void write(string)'. De fato, existe um problema de ambiguidade: Ambas as funções foram criadas para o mesmo propósito: escrever uma mensagem em um display. Porém, o display em questão é diferente dependendo do contexto. Existem várias maneiras de resolver tal problema. Uma possibilidade é dividir as implementações em namespaces diferentes.

Um namespace é uma estrutura que pode conter classes, funções e variáveis. Eles não servem apenas para desambiguar uma função ou classe, mas também para organizar o projeto. Eles são estruturas opcionais, e não é raro encontrar bibliotecas e aplicativos escritos em C++ que são implementados sem o uso de namespaces.

Para resolver o problema comentado, poderia-se escrever o código anterior da seguinte forma:

namespace main_display {
    void write(std::string const& message)
    {
        // ... Implementação do display principal
    }
}

namespace secondary_display {
    void write(std::string const& message)
    {
        // ... Implementação do display secundário
    }
}

Para usar uma ou outra implementação, é necessário explicitar o namespace ao chamar a função, da seguinte forma: main_display::write("...exemplo...");. Um código de exemplo é mostrado abaixo:

int main()
{
    main_display::write("Mensagem no display principal");
    secondary_display::write("Mensagem no display secundário");

    return 0;
}

O namespace mais comum nos programas C++ é o std (namespace relacionado à biblioteca padrão). Inclusive, ele apareceu já nos primeiros capítulos desse livro, para utilizar a função cout:

#include <iostream>
 
int main()
{
    std::cout << "Olá mundo!" << std::endl;
    return 0;
}

No caso de você utilizar muitas funções e estruturas de dentro de um mesmo namespace, é possível utilizar using para evitar a repetição do prefixo do namespace em questão, como no exemplo abaixo. Note, porém, que o uso de using no escopo global é desencorajado em alguns casos, pois pode afetar parcelas de código indesejadamente. Esse ponto será discutido novamente em capítulos posteriores desse livro.

#include <iostream>

// Sugere-se evitar `using` global
// using namespace std;

int main()
{
    // `using` dentro do escopo de alguma função é preferível
    using namespace std;
    cout << "Ol mundo!" << endl;
    
    return 0;
}

Visão geral sobre o processo de compilação

Conhecer o processo de compilação e linkagem do C++ é importante não apenas pelo aspecto teórico. A compreensão ajuda na solução de problemas principalmente relacionados à gerenciamento de dependências, e o reconhecimento de problemas de linkagem.

Apesar do processo ser possivelmente um pouco diferente entre diferentes sistemas operacionais, a noção geral é aproximadamente a mesma. O processo tem basicamente 3 etapas: Pré-processamento, compilação e linkagem, como ilustrado na figura abaixo. Após explicado essas etapas, esse capítulo explica brevemente sobre o processo de carregamento e execução de código compilado, e sobre o uso de bibliotecas externas.

Processo de compilação

Pré-processamento

O pré-processador cuida de resolver todas as macros disponíveis no programa. Macros são os comandos que iniciam com #, tais como #include e #define. Para o escopo dessa seção, explicaremos o uso de quatro macros: #include, #define, #ifdef e #ifndef.

Na verdade, já usamos #include algumas vezes desde o início desse livro, mas não sabiamos exatamente seu funcionamento. Basicamente, o que esse comando faz é buscar o conteúdo de um arquivo e inclui-lo (copia-lo) no local do #include. Considere o seguinte exemplo: imagine que você tem um projeto com 2 arquivos, main.cpp e sum.hpp. Esse último contém uma função auxiliar int sum(int a, int b), e você gostaria de utilizar essa função no main.cpp. Isso pode ser feito utilizando o #include:

sum.hpp

int sum(int a, int b)
{
    return a + b;
}

main.cpp

#include <iostream>
#include "sum.hpp"

int main()
{
    std::cout << sum(2, 3) << std::endl;
    return 0;
}

Note a diferença entre os dois includes: O primeiro utiliza <nome do arquivo>, enquanto o segundo utiliza "nome do arquivo". A diferença é que enquanto "" busca o arquivo à ser incluído a partir da pasta corrente (ou seja, a mesma pasta onde reside o arquivo main.cpp, nesse caso), utilizar <> faz com que o pré-processador busque o arquivo em algumas pastas pré-determinadas, como é o caso dos diretórios padrão do sistema.

Existe um problema que, usualmente, um arquivo pode depender de outro que pode já ter sido incluído previamente. Por exemplo, se existirem dois arquivos, a.hpp e b.hpp, onde a.hpp inclui b.hpp e vice versa, teriamos uma recursão de inclusões impossível de resolver. Por isso, utiliza-se include guards para evitar esse problema, como no exemplo abaixo.

Ao incluir-se o arquivo a.hpp em main.cpp, o conteúdo do arquivo a.hpp irá ser copiado. Em a.hpp, o pré-processador irá verificar a existência da macro __A_HPP. Como ela não existe (#ifndef), ela será definida (#define), e o conteúdo de a.hpp será totalmente copiado. Dessa forma, o arquivo b.hpp será incluído. Aqui, o mesmo procedimento irá ocorrer, sendo definida uma variável __B_HPP. Porém, ao incluir a.hpp de dentro de b.hpp, percebe-se que, agora, a macro __A_HPP está definida. Sendo assim, o ifndef do arquivo a.hpp percebe que não precisa processar novamente o mesmo arquivo.

main.cpp

#include "a.hpp"

int main()
{
    return 0;
}

a.hpp

#ifndef __A_HPP
#define __A_HPP

#include "b.hpp"

#endif

b.hpp

#ifndef __B_HPP
#define __B_HPP

#include "a.hpp"

#endif

O processo de inclusão de arquivos hpp é muito importante para a modularização de código. Em C++, é comum separar a declaração das funções, classes e estruturas em arquivos .hpp, e a definição das mesmas em arquivos .cc ou .cpp. Exceto em alguns casos específicos, como no uso de templates (será visto apenas em capítulos mais avançados).

Erros de pré-processamento ocorrem na primeira etapa do processo de compilação. Para exemplificar, considere o exemplo abaixo, e imagine que não existe o arquivo some_file.hpp. O erro no gcc é a.cc:1:10: fatal error: some_file.hpp: No such file or directory. Isso significa que o arquivo não existe ou que ele existe, mas não está na pasta correta para que seja encontrado. É muito importante que se tenha isso bem conhecido, visto que esse conhecimento auxilia na resolução de problemas no processo de compilação.

#include "some_file.hpp"

int main()
{
    return 0;
}

Macros também podem ser utilizadas como "funções" modificadas em tempo de compilação. Esse tema não vai ser discutido em profundidade nesse capítulo, e sugere-se não utilizar macros para executar esse tipo de rotina em tempo de compilação. Apenas a titulo de exemplo, seria possível definir a seguinte macro:

#include <iostream>
#define POW2(x) x*x

int main()
{
    std::cout << POW2(4) << std::endl;

    return 0;
}

A macro toma como parâmetro um valor x não tipado e troca o código por x*x. Para visualizar esse efeito, no gcc, é possível compilar o arquivo acima com gcc -E main.cpp, produzindo algo semelhante ao arquivo pré-processado abaixo. Note que a compilação não gerou um executável nesse caso, pois a opção -E orienta o compilador a parar o processo de compilação após a etapa de pré-processamento.

// ... Outros comentários aqui ...
int main()
{
    std::cout << 4*4 << std::endl;

    return 0;
}

Apesar de parecer útil, a utilização de macros traz vários problemas. Por exemplo, veja o que aconteceria se chamassemos POW2(4 + 1):

int main()
{
    std::cout << 4 + 1*4 + 1 << std::endl;

    return 0;
}

O resultado seria 4 + 1 * 4 + 1, ou seja, 9 (Ao invés de 25, como seria o esperado). Isso acontece por que as macros não são equivalentes à chamadas de função. Lembre-se você está apenas criando macros que copiam valores de um lado para outro. Uma possível solução para esse caso seria de adicionar parênteses à macro: #define POW2(x) (x)*(x), resultando em:

int main()
{
    std::cout << (4 + 1)*(4 + 1) << std::endl;

    return 0;
}

De qualquer forma, orienta-se a criar funções simples do C++ e, possívelmente, utilizar constexpr em casos semelhantes à esses.

Compilação

A próxima etapa é a de compilação, que traduz o código C++ em código assembly. Esse código, em conjunto com vários outros arquivos, são linkados, produzindo assim o executável final (ou biblioteca).

Essa é uma das partes do livro que depende de ambiente (Sistema Operacional, compilador...) para que você possa reproduzir. Apenas para fins didáticos, essa seção utilizará o Sistema Operacional Linux e o compilador GCC, invocado pela linha de comando (terminal). Considere o seguinte programa:

main.cpp

#include <iostream>

double f(double x)
{
    return 3*x*x + 2*x - 1;
}

double g(double x)
{
    return 3*f(x + 1.);
}

int main()
{
    std::cout << f(3.) << std::endl;
    std::cout << g(4.) << std::endl;
}

Pela linha de comando, utilizando GCC, é possível compilar o programa acima invocando o comando g++ main.cpp, o que produz um executável de nome a.out. Imagine, porém, que o código do main.cpp começa a crescer, e está agora com 1000 linhas de código. Outras pessoas começaram a trabalhar no mesmo projeto. Não seria fácil manter e evoluir o programa em um só arquivo grande com várias pessoas mexendo nele todos os dias. Essa é uma das motivações para separar o projeto em vários arquivos. Move-se, portanto, a implementação de f e de g para os arquivos f.hpp e g.hpp:

main.cpp

#include <iostream>
#include "f.hpp"
#include "g.hpp"

int main()
{
    std::cout << f(3.) << std::endl;
    std::cout << g(4.) << std::endl;
}

f.hpp

double f(double x)
{
    return 3*x*x + 2*x - 1;
}

g.hpp

#include "f.hpp"

double g(double x)
{
    return 3*f(x + 1.);
}

Compilar esse pequeno exemplo produz a seguinte mensagem de erro:

$ g++ main.cc 
In file included from g.hpp:1,
                 from main.cc:3:
f.hpp:1:8: error: redefinition of ‘double f(double)’
    1 | double f(double x)
      |        ^
In file included from main.cc:2:
f.hpp:1:8: note: ‘double f(double)’ previously defined here
    1 | double f(double x)
      |        ^

Conforme explicado no capítulo anterior, a ausência das include guards pode ser perigoso, como é o presente caso. Isso porque tanto main.cpp quanto g.hpp incluem f.hpp, gerando uma redefinição da função f. Isso fica bem claro quando verificamos o output da etapa de pré-processamento isoladamente com g++ -E main.cc. O problema pode ser resolvido, incluindo os include guards (#ifndef ... #define ... #endif), conforme código abaixo:

f.hpp

#ifndef __F_HPP
#define __F_HPP

double f(double x)
{
    return 3*x*x + 2*x - 1;
}

#endif

g.hpp

#ifndef __G_HPP
#define __G_HPP

#include "f.hpp"

double g(double x)
{
    return 3*f(x + 1.);
}

#endif

O novo código compila e roda normalmente, porém, não é recomendado manter as implementações das funções em headers files (arquivos .hpp), pois a medida que o projeto cresce, o processo de compilação começará a ficar lento. Além disso, uma modificação em qualquer header file ocasionará na recompilação de todo o projeto (Ou seja, ocasionaria em um "rebuild").

Dessa forma, é comum separar a declaração das classes e funções em arquivos hpp e a definição das mesmas em arquivos cpp, exceto aqueles que contém templates. Nosso exemplo segue da seguinte forma:

main.cc

#include <iostream>
#include "f.hpp"
#include "g.hpp"

int main()
{
    std::cout << f(3.) << std::endl;
    std::cout << g(4.) << std::endl;
}

f.hpp

#ifndef __F_HPP
#define __F_HPP

double f(double x);

#endif

f.cc

#include "f.hpp"

double f(double x)
{
    return 3*x*x + 2*x - 1;
}

g.hpp

#ifndef __G_HPP
#define __G_HPP

#include "f.hpp"

double g(double x);

#endif

g.cc

#include "g.hpp"

double g(double x)
{
    return 3*f(x + 1.);
}

Para que os novos arquivos estejam acessiveis no momento da compilação, o processo de compilação precisa ser alterado. Precisamos, agora, colocar todos os arquivos .cpp para que sejam compilados e linkados juntos:

g++ f.cc g.cc main.cc

Conforme notado anteriormnete, nos #includes locais, utiliza-se aspas duplas para incluir os arquivos de projeto, e utilizamos < e > para incluir arquivos de sistema (iostream, por exempo). Arquivos incluídos com aspas duplas são arquivos presentes da mesma pasta em relação ao arquivo que está sendo incluído. Arquivos incluidos com < e > são buscados em uma lista pré-definida de pastas, no processo de compilação. É possível adicionar uma pasta nessa lista, utilizando a opção de compilação -I [caminho]. De fato, não é raro encontrar projetos que acabam adicionando a pasta corrente (local) nessa lista e utilizando apenas < e > em todos os includes. Isso pode ser uma vantagem, por exemplo, quando estamos reestruturando o projeto e movendo arquivos. Trocando todos os includes do nosso exemplo para atuarem com < e >, o processo de compilação se tornaria-se g++ f.cc g.cc main.cc -I...

O processo de compilação da forma como está invocado acima, na verdade inclui o processo de pré-processamento (que já vimos) e o processo de linkagem. É possível invocar o compilador individualmente para cada arquivo arquivo .cpp e, posteriormente, invocar o processo de linkagem separadamente. Faremos isso para fins didáticos:

g++ -c g.cc -I.
g++ -c f.cc -I.
g++ -c main.cc -I.

Esses comandos geram os arquivos chamados código objeto: g.o, f.o e main.o. Esses arquivos não são executáveis. Para finalizar o processo de compilação, é necessário invocar (Não estou utilizando diretamente o linker nesse caso): g++ f.o g.o main.o. Uma representação do que foi feito é mostrada abaixo:

Diagrama de compilação e linkagem

É importante ter em mente essas duas etapas como processos separados. Erros de compilação (sintaxe e pré-processamento, por exemplo) acontecerão mesmo quando você tentar compilar o arquivo isoladamente. Erros de linkagem (tais como undefined reference) acontecerão apenas quando você tentar compilar todos os arquivos em conjunto ou quando você estiver executando a etapa de linkagem manualmente.

Linkagem

O processo final de geração do executável é a linkagem (não é um verbo real). Neste passo, os códigos objeto de todas as unidades de tradução envolvidas no programa são unidos em um só. Um arquivo executável pode ser obtido com o seguinte comando:

g++ f.o g.o main.o

É importante ter em mente essas etapas do processo de compilação. Erros de compilação (sintaxe e pré-processamento) acontecerão mesmo quando você tentar compilar cada arquivo isoladamente. Erros de linkagem (undefined reference, por exemplo) acontecerão apenas quando você tentar linkar os arquivos para gerar um executável.

A linkagem comentada nessa seção é conhecida como linkagem estática. O código executável gerado contém todas as funções necessárias para executa-lo, como ilustrado na imagem abaixo. Nota importante: também é usual utilizar arquivos .a ("Archive") no processo de linkagem estática no Linux. Arquivos .a são apenas uma coleção de vários arquivos .o. Para gerar arquivos .a, é possível utilizar o executável ar.

Linkagem estática

De forma alternativa, é possível organizar um projeto de software com funções separadas em diversos arquivos executáveis. Esses arquivos, ao invés de serem linkados estáticamente, são linkados dinamicamente, o que significa que as funções ficam espalhadas em diferentes arquivos, e buscadas pelo software em tempo de execução, ao invés de serem buscadas em tempo de compilação, como no caso da linkagem estática.

No Linux, esses arquivos possuem extensão .so ("Shared Objects"), enquanto no Windows possuem extensão .dll (Dynamic Linked Library). O processo de geração, linkagem e carregamento de arquivos com linkagem dinâmica é diferente no linux e no Windows.

A ilustração abaixo traz uma visão geral do processo de consumo da função function_c a partir de uma linkagem dinâmica. Nesse caso, não está sendo mostrado como o arquivo arquivo2.so foi gerado. No caso do Windows, o arquivo arquivo2.so se chamaria arquivo2.dll.

Linkagem estática

As vantagens de utilizar uma biblioteca de linkagem estática incluem:

  • Como todo o código está num só arquivo, atualizações de outras dependências não afetam (portanto, não quebram) o executável;
  • Facilidade de instalação e compartilhamento do programa.

Enquanto algumas desvantagens são:

  • O arquivo executável tende a ficar cada vez maior, pois precisa conter todas as funções em um só arquivo;
  • Para atualizar um subconjunto de funções é necessário atualizar o arquivo executável inteiro.

Por outro lado, algumas vantagens da linkagem dinâmica incluem:

  • Como o código está distribuído em diversos arquivos, atualizar um subconjunto de funções significa atualizar apenas alguns arquivos, ao invés de necessitar atualizar o código inteiro;
  • As funções mais comuns podem ser aproveitadas e utilizadas em mais de um arquivo executável;
  • Funções não utilizadas não precisam ser carregadas para a memória.

Finalmente, algumas desvantagens incluem:

  • No caso de uso excessivo de bibliotecas dinâmicas, pode ser difícil de gerenciar as dependências;
  • No caso de uma ou mais bibliotecas dinâmicas não estarem disponíveis, isso pode impossibilitar o uso de um executável.

Existem duas formas de consumir uma biblioteca dinâmica: Por meio de linkagem dinâmica em tempo de compilação (Dynamic Linking) ou por meio de carregamento dinâmico em tempo de execução (Dynamic Loading). Os dois conceitos podem ser inicialmente confusos, principalmente porque o primeiro deles possa ser confundido com o caso de linkagem estática, mas essas formas são diferentes.

Dynamic Linking faz uso de um procedimento provido pelo sistema operacional. Ao executar um programa que foi compilado dessa forma, as bibliotecas dependentes são automaticamente carregadas no início da execução do programa. Ou seja, a linkagem é feita em tempo de execução, mas é feita automaticamente.

Por outro lado Dynamic Loading significa que o programador do executável utiliza funções auxiliares para carregar dinâmicamente cada biblioteca que queira. Por exemplo, no Linux, é possível utilizar a função dlopen para carregar um arquivo .so. Ou seja, a linkagem é feita em tempo de execução, mas é feita manualmente.

Gerenciamento de pacotes

Como visto, o processo de compilação passa por diferentes estágios até a criação de um arquivo executável. A separação de funções, variáveis, constantes e outras entidades em artefatos (arquivos .hpp, .a, .so etc...) possibilita o compartilhamento de código.

Por exemplo, se um programador A cria um módulo de um software escrito em C++ que é capaz de criar uma janela (interface gráfica), esse mesmo módulo pode ser utilizado por um programador B em outro projeto, desde que o programador A tenha utilizado alguma das ferramentas apresentadas nessa seção.

Uma possibilidade, seria de o programador A ter criado uma biblioteca estática e prover esses arquivos binários e os arquivos .hpp associados para o programador B. Dessa forma, o programador A pode criar um pacote, contendo a biblioteca que pode ser enviado ao programador B, e que contém todos os arquivos necessários para que o programador B possa criar seus programas.

Alguns exemplos de bibliotecas incluem: Qt (criação de programas com interface gráfica), Boost (contém várias funções auxiliares), GoogleTest (automatização de testes), entre muitas outras.

O problema é que, em geral, para gerenciar e utilizar essas bibliotecas, normalmente o programador precisa ter algum conhecimento específico da biblioteca (normalmente obtém-se o conhecimento por meio da leitura dos manuais de cada uma delas). Uma alternativa interessante para utilizar as bibliotecas é o uso do software CMake, abordado em outra seção desse livro.

Um segundo problema que ocorre é que é possível que as bibliotecas tenham sido criadas utilizando outras bibliotecas. Essa sequência de dependências pode ocasionar em problemas de compilação e execução que são dificeis de se lidar. Daí vem a necessidade de um gerenciador de pacotes. Gerenciadores de pacote são ferramentas que auxiliam no processo de download, automatizam o processo de instalação, e cuidam das dependências entre os pacotes e o seu programa.

O intuito dessa seção não é fazer propaganda nem ensinar uma ferramenta específica. Ao invés disso, apenas apontam-se os problemas e algumas possíveis ferramentas que podem ser utilizadas para resolve-los, que incluem:

  • Conan
  • Conda
  • apt - Advanced Package Tool
  • vcpkg

Introdução à biblioteca padrão do C++

A biblioteca padrão do C++ provê algumas funcionalidades básicas para desenvolvimento de software, como strings, vetores, listas, tuplas, além de funções utilitárias para lidar com gerenciamento de memória, I/O, sistema de arquivos, threads, entre outras. Esse capítulo apresenta uma breve introdução, mostrando algumas das funcionalidades disponíveis na biblioteca. Não é necessário baixar nem configurar nada, para utilizar a biblioteca padrão.

String

Strings são estruturas de dados que representam sequências de elementos, sendo o mais comum a sequência de caracteres (char). A biblioteca padrão provê uma série de funcionalidades para lidar com strings, a partir do header <string>.

É importante notar que sequências de caracteres dentro de aspas duplas (") não são do tipo string em C++. Considere o exemplo abaixo:

#include <string>

int main()
{
    auto word = "Exemplo"; // *Não* é do tipo std::string
    auto other_word = std::string("Outro Exemplo"); // É do tipo std::string

    // (A partir de C++14)
    using namespace std::string_literals;
    auto yet_another_word = "Mais um exemplo"s; // É do tipo std::string

    return 0;
}

word não é do tipo std::string. Isso por que C++ não possui string como tipo primitivo da linguagem. Ao invés disso, word possui tipo const char[8], que representa um "array de 8 elementos do tipo char" (7 caracteres na palavra "Exemplo" + 1 caracter de fim de texto, representado por \0). Por outro lado, other_word e yet_another_word são do tipo std::string.

A vantagem de utilizar std::string ao invés de const char[], é que ela provê várias funções e métodos de manipulação e extração de dados para strings. Por exemplo, é possível concatenar duas std::strings utilizando o operador +, mas não é possível fazer o mesmo com const char[]:

#include <string>
#include <iostream>

int main()
{
    using namespace std::string_literals;
    auto string1 = "Hello "s;
    auto string2 = "World"s;
    auto string3 = string1 + string2;
    std::cout << string3 << "\n"; // Mostra "Hello World"
    
    // O código abaixo produz um erro de compilação, pois não é possível
    // somar duas variáveis do tipo `const char*`:
    // auto var1 = "Hello ";
    // auto var2 = "World";
    // auto var3 = var1 + var2; // ERRO de compilação
    
    return 0;
}

A partir desse momento, utiliza-se a palavra "string" para denotar std::string.

É possível converter strings de/para os tipos primitivos int e float, por meio de funções auxiliares. Esse tipo de conversão é especialmente útil para traduzir inputs de usuário em dados numéricos. Exemplo:

#include <string>
#include <iostream>

int main()
{
    using namespace std::string_literals;
    auto value1 = "12"s;
    auto value2 = "7"s;
    auto string3 = value1 + value2; // Produz "127" (com tipo string)
    auto value3 = std::stoi(value1) + std::stoi(value2); // Produz 19 (com tipo int)
    std::cout << string3 << ", " << value3 << "\n"; // Mostra 127, 19

    return 0;
}

Para converter strings para floats, pode-se utilizar std::stof. Para converter int e float para strings, pode-se utilizar std::to_string. Uma lista extensa de funcionalidades para std::sring está disponível aqui.

Input/Output (Entrada/Saída)

O termo I/O se refere aos dispositivos de Entrada (Input) e Saída (Output). Essa seção traz uma introdução para trabalhar com a leitura de dados do usuário pelo terminal (CMD) e a escrita de dados em terminal e em arquivos de texto.

A forma mais comum de escrever dados no terminal, em C++, é a partir de std::cout ("Character Output"), e é a forma como vêm sendo escritos os exemplos desse livro desde os primeiros capítulos. Para utilizar o std::cout, é necessário incluir o header <iostream>, como mostra o exemplo abaixo.

#include <iostream>

int main()
{
    std::cout << "Hello World!\n"; // Escreve "Hello World!" e pula uma linha (\n)
    return 0;
}

A sintaxe é diferente em relação às chamadas de função normalmente utilizada. Note que ao invés de std::cout("texto"), escreve-se std::cout << "texto";. Isso se dá pela forma como std::cout foi implementado. Na verdade, ele é um objeto que possui um operator<<, e não uma função. Essa informação é apenas a título de curiosidade, e não afeta o uso do std::cout.

Não é necessário converter os dados para string para utilizar o std::cout. Ele consegue lidar com dados de outros tipos, como int e float. Basta escrever std::cout << my_int;, sendo my_int uma variável do tipo int, por exemplo.

É possível ler dados do usuário a partir de std::cin ("Character input"), como no exemplo abaixo. Os dados lidos podem ser de qualquer tipo primitivo ou string. Os dados podem ser separados por quebra de linha ou por espaço.

#include <iostream>
#include <string>

int main()
{
    using namespace std::string_literals;
    std::cout << "Digite os 3 dados:\n";
    auto my_int = 0;
    auto my_float = 0.;
    auto my_string = ""s;

    std::cin >> my_int >> my_float >> my_string;

    std::cout << "Você digitou: " << my_int << ", " << my_float << ", " << my_string << "\n";

    return 0;
}

Note que até agora todos os programas apresentados guardavam as suas variáveis em memória. Isso significa que, ao reiniciar o programa, todos os cálculos com as variáveis são perdidos. Uma das formas de persistir (guardar) dados em disco é utilizando o fstream, como no exemplo abaixo. Claro que isso é uma forma bastante primitiva de persistir dados. É importante lembrar que existem softwares de banco de dados e bibliotecas de gerenciamento de arquivos mais robustas. Mesmo assim, é interessante conhecer a biblioteca padrão, por ser uma alternativa válida para a persistência de dados em algumas situações. É importante apenas manter em mente que não é a única e nem necessariamente a melhor ferramenta para resolver qualquer problema.

#include <iostream>
#include <fstream>
#include <string>

int main() {
    using namespace std::string_literals;

    auto filename = "arquivo.txt"s;
    auto file_stream = std::fstream(filename, std::ios::out);
    if (!file_stream.is_open()) {
        std::cout << "Failed to open " << filename << '\n';
        return -1;
    }

    auto variable = 1.234;
    file_stream << "Conteúdo do arquivo!\n"s;
    file_stream << variable;
    
    return 0;
}

O exemplo acima abre um arquivo chamado arquivo.txt em modo de saída (std::ios::out). O programa verifica se foi possível abrir o arquivo e, caso negativo, gera uma mensagem e sai do programa com um código de erro (return -1). Uma vez aberto, escreve-se uma string de exemplo, e uma variável do tipo float. Note que o arquivo não é explicitamente fechado (close). Isso por que o fstream garante que o arquivo será fechado ao fim do escopo de vida do mesmo. Dessa forma, não é necessário fechar o arquivo explicitamente.

O código capaz de gerar um programa que faz a leitura do mesmo arquivo é mostrado abaixo. Na verdade, essa é apenas uma das formas de fazer a leitura dos dados de um arquivo. Também é possível ler o conteúdo de um arquivo caracter por caracter. Para isso, poderia-se utilizar o método file_stream.get(l);, com l sendo do tipo char. Cada forma de leitura tem suas vantagens e desvantagens, e seu uso vai depender do problema que se está tentando resolver.

#include <iostream>
#include <fstream>
#include <string>

int main() {
    using namespace std::string_literals;

    auto filename = "arquivo.txt"s;
    auto file_stream = std::fstream(filename, std::ios::in);
    if (!file_stream.is_open()) {
        std::cout << "Failed to open " << filename << '\n';
        return -1;
    }

    auto line = ""s;
    auto i = 1;
    while(!file_stream.eof()) {
        getline(file_stream, line);
        std::cout << "Linha " << i << ": " << line << '\n';
        i += 1;
    }

    return 0;
}

Estudo de Caso: Input de dados para juizes online

Existem vários sites na internet para exercitar a prática de programação e algoritmos. Esses sites são conhecidos como "juizes online" como, por exemplo, UVa e Spoj. Nesses sites, é comum encontrar problemas com um formato bastante específico para os inputs dos dados dos desafios. Essa subseção traz um breve tutorial para iniciantes em C++, em relação a como poderia-se fazer a leitura dos dados para esses tipos de problema.

Para o exercício, considere o problema PRIME1 do Spoj, copiado (e traduzido) abaixo, para referência:

Peter deseja gerar todos os números primos para seu sistema. Você deve ajuda-lo. Gere todos os números primos entre dois números dados.

Entrada

A entrada inicia com um número t, que representa o número de testes (t <= 10). Cada uma das t próximas linhas contém dois números, m e n, separados por espaços.

Saída

Para cada caso de teste, escreva na tela todos os primos p onde m <= p <= n. Cada número primo deve ser escrito em uma linha diferente. Casos de teste devem ser separados por uma linha em branco.

Exemplo

Entrada:
2
1 10
3 5

Saída:
2
3
5
7

3
5

O objetivo aqui não é resolver o problema completamente, apenas implementar a leitura dos dados. A entrada de dados segue um padrão bem definido, conforme o exemplo acima. A primeira linha contém apenas um número, que deve ser lido na variável t. As t linhas seguintes devem conter pares de números (m, n).

Conforme visto, é possível fazer a leitura dos dados a partir do std::cin. Utiliza-se, então, essa forma para fazer a leitura da variável t:

#include <iostream>

int main() {
    auto t = 0;
    std::cin >> t;
    std::cout << "CASOS DE TESTE: " << t << '\n';

    return 0;
}

A seguir, para fazer a leitura dos pares m e n, é possível utilizar um for com um contador de i = 0 até t. Enquanto esse contador não chegar no valor de t, lê-se um novo par de valores. Assume-se que os valores serão dois números válidos (Não é feita nenhuma verificação sobre os valores).

#include <iostream>

int main() {
    auto t = 0;
    std::cin >> t;
    std::cout << "CASOS DE TESTE: " << t << '\n';

    for (auto i = 0; i < t; ++i) {
        auto n = 0;
        auto m = 0;
        std::cin >> n >> m;

        std::cout << "Processando dados: (" << n << ", " << m << ")\n";
        // ... resto do código ...
    }

    return 0;
}

Uma curiosidade interessante é que, no linux, é possível redirecionar o conteúdo de um arquivo texto, de forma que ele sirva de entrada para um programa. Por exemplo, digamos que você compile o arquivo acima e gere um executável chamado programa. Ao invés de executar esse programa e digitar as entradas dos dados manualmente, é possível criar um arquivo (por exemplo, input.txt) cujo conteúdo é conforme dado abaixo, e redirecionar o conteúdo desse arquivo para a entrada do programa programa, da seguinte forma: ./programa < input.txt.

2
1 10
3 5

O resultado da execução é dado abaixo. Essa é uma forma rápida de manter arquivos de teste para problemas de maratona de programação e juizes online.

$ ./programa < input.txt 
CASOS DE TESTE: 2
Processando dados: (1, 10)
Processando dados: (3, 5)

Array

Um std::array é um container com um número pré-definido de dados de um certo tipo. O tipo e o tamanho do array devem ser conhecidos em tempo de compilação. Isso significa que você não pode aumentar o tamanho do array a partir de um input de usuário, por exemplo.

Uma das vantagens em utilizar std::array ao invés de arrays "crus" (por exemplo, int[3]), é que você pode passar std::array como argumentos de funções, e retorna-los sem complicações. Além disso, std::array possui métodos de acesso à dados do array e algumas operações auxiliares sobre ele.

Para declarar e definir um std::array, basta escolher um tipo (e.g. int, float, etc...) e um tamanho, conforme exemplo abaixo:

#include <array>
#include <iostream>

int main()
{
    // Inicializa um array com 3 elementos. Os valores são '1', '2' e '3'.
    auto my_arr = std::array<int, 3>{1, 2, 3};
    for (int i = 0; i < my_arr.size(); ++i) {
        std::cout << "my_arr[" << i << "] = " << my_arr[i] << "\n";
    }

    // Preenche todo o array com o valor '42'    
    my_arr.fill(42);
    for (int i = 0; i < my_arr.size(); ++i) {
        std::cout << "my_arr[" << i << "] = " << my_arr[i] << "\n";
    }

    return 0;
}

Vector

std::vector permite o programador a criar um vetor com um tamanho dinâmico de elementos. Em outras palavras, é uma estrutura que permite inserir e remover elementos de uma lista em tempo de execução. No exemplo abaixo, A linha 3 inclui o header <vector>, para que seja possível utilizar a estrutura de dados std::vector. A linha 17 cria uma instancia de um vector. Os elementos desse vector serão do tipo Point. Ou seja, não será possível colocar inteiros, doubles, ou variáveis de nenhum outro tipo nesse vector. O vector foi inicializado sem elementos e, nas linhas 19 e 20, dois elementos são adicionados utilizando o método push_back.

#include <iostream>
#include <cmath>
#include <vector>
 
struct Point {
   double x;
   double y;
};
 
double calculate_distance(Point p1, Point p2)
{
   return sqrt(pow(p1.x - p2.x, 2) + pow(p1.y - p2.y, 2));
}
 
int main()
{
   std::vector<Point> points;
 
   points.push_back(Point{1.2, 3.4});
   points.push_back(Point{4.5, 5.6});
 
   std::cout << "A distância entre os pontos é " << calculate_distance(points[0], points[1]) << std::endl;
 
   return 0;
}

A linha 22 usa a função calculate_distance, mas passa como parâmetros os elementos cujos índices são 0 e 1 no vector. Tenha cuidado para nunca acessar o vector com indices que não foram populados. Para saber a quantidade de elementos de um vector, é possível utilizar o método size(). Para remover o último elemento adicionado, é possível utilizar o método pop_back().

Map

std::map é a primeira estrutura de dados que será abordada nesse livro. Pense em uma estrutura de dados como uma struct que guarda informações para consultas, e que algumas estruturas organizam os dados de uma maneira específica para que as consultas sejam mais rápidas.

Toda estrutura de dados é diferente uma da outra, e mais apta para resolver um problema do que outro, por exemplo, todas as coisas que são feitas com std::map podem ser feitas com std::vector (estrutura exibida posteriormente), mas dependendo do que seja, fazer com std::map pode ser mais fácil e simples, ou mais difícil e complicado.

std::map é uma estrutura de dados que mapeia um dado à outro. Ele é formado por vários pares de chave (key) e valor (value), depois de montado, dada uma chave, a estrutura deverá retornar qual é o valor único que foi atribuído àquela chave.

Considere a seguinte tabela de dados como exemplo:

NomeIdade
"Alice"20
"Bruno"30
"Carol"40
"Júlia"50
"Pedro"50

Vamos criar um std::map vazio, adicionar cada linha dessa tabela, e então, dado um nome, quermos ter certeza que a estrutura vai retornar a idade correta.

#include <iostream>
#include <map>
#include <string>

int main()
{
    // An empty map
    std::map<std::string, int> ages;
    // Adding the data
    ages["Alice"] = 20;
    ages["Bruno"] = 30;
    ages["Carol"] = 40;
    ages["Júlia"] = 50;
    ages["Pedro"] = 50;

    // Outputs: "Alice tem 20 anos."
    std::cout << "Alice tem " << ages["Alice"] << " anos.\n";

    // Outputs: "Pedro tem 50 anos."
    std::cout << "Pedro tem " << ages["Pedro"] << " anos.\n";

    ages["Pedro"] = 51;
    // "Pedro agora tem 51 anos."
    std::cout << "Pedro agora tem " << ages["Pedro"] << " anos.\n";


    return 0;
}

Para declarar essa estrutura, é necessário explicitar os tipos de dados das chaves e dos valores, com a seguinte sintaxe:

std::map<K, V>

Isso representa um map que possui chaves do tipo K e valores do tipo V, no exemplo acima o mapa foi declarado da seguinte forma:

std::map<std::string, int> ages;

Nesse caso, K=std::string e V=int, então std::map<std::string, int> mapeia pares de string e inteiros.

Também é possível iterar em todos os elementos do map:

#include <iostream>
#include <map>

int main()
{
    std::map<int, bool> my_map;
    my_map[3] = true;
    my_map[7] = false;

    for (auto const& [key, value] : my_map) {
        std::cout << key << ": " << value << std::endl;
    }

    std::cout << my_map[3] << std::endl;
    std::cout << my_map.at(3) << std::endl;

    // With [], values will be added if they are not present
    std::cout << my_map[9] << std::endl;
    // With .at(), will throw exception if value not present
    std::cout << my_map.at(8) << std::endl;

    return 0;
}

Set

std::function & lambda

Introdução à biblioteca padrão do C++

A biblioteca padrão do C++ provê algumas funcionalidades básicas para desenvolvimento de software, como strings, vetores, listas, tuplas, além de funções utilitárias para lidar com gerenciamento de memória, I/O, sistema de arquivos, threads, entre outras. Esse capítulo apresenta uma breve introdução, mostrando algumas das funcionalidades disponíveis na biblioteca. Não é necessário baixar nem configurar nada, para utilizar a biblioteca padrão.

Classes e Objetos

A base da programação orientada a objetos são os objetos. No contexto de C++ objetos são instâncias de classes, classes são abstrações que contém a descrição de quais atributos e métodos um objeto possui e atributos são variáveis internas de uma classe. Métodos são funções membro (member functions) de uma classe. Exemplo:

#include <iostream>

class Cachorro {
public:
    void latir()
    {
        std::cout << "Au! Au!!\n";
    }
};

int main()
{
    Cachorro bidu;
    bidu.latir();
    return 0;
}

No código acima, Cachorro é uma classe. O objeto bidu é uma instância da classe Cachorro. Como Cachorro possui o método latir, é possível invoca-lo por meio de bidu.latir().

Métodos podem ser públicos (public), protegidos (protected) ou privados (private). Métodos e atributos privados não podem ser invocados fora do escopo da classe. Métodos e atributos protegidos podem ser invocados apenas no escopo da classe ou de classes filhas (Hierarquia de classes é tema do próximo capítulo). Métodos e atributos públicos podem ser acessados de dentro ou de fora do escopo da classe.

Sugere-se manter os atributos de uma classe como membros privados ou protegidos, e acessa-los apenas através de métodos, a fim de esconder os detalhes do objeto, e expondo apenas alguns métodos públicos. À isso da-se o nome de encapsulamento.

O construtor de uma classe é um método especial que possui o nome igual ao nome da classe e não possui retorno. O construtor da classe serve para inicializar os atributos internos e deixa-la em um estado utilizavel. Exemplo:

#include <iostream>
#include <string>

class Cachorro {
public:
    Cachorro(std::string const& nome)
        : nome(nome)
    {}

    void latir()
    {
        std::cout << "Au! Au!! Eu sou o " << this->nome << '\n';
    }

private:
    std::string nome;
};

int main()
{
    Cachorro bidu{"Bidu"};
    bidu.latir();
    return 0;
}

No exemplo acima Cachorro(std::string const& nome) é o construtor da classe Cachorro. Note que o construtor possui um parâmetro do tipo std::string. Dessa forma, para construir um Cachorro agora é necessário passar alguma string como parâmetro. Isso está sendo feito na linha Cachorro bidu{"Bidu"};. Essa linha está construindo uma instância da classe Cachorro de nome bidu e cuja variável interna nome terá o valor Bidu.

Note a sintaxe de construção do Cachorro:

    Cachorro(std::string const& nome)
        : nome(nome)
    {}

Note que o : nome(nome) está ANTES da abertura do escopo do corpo do construtor, ou seja, antes das chaves {}. Essa sintaxe possibilita que a construção do objeto seja feita antes de entrar no corpo do construtor. Isso se chama lista de inicialização (initializer list). Alternativamente poderia-se escrever

    Cachorro(std::string const& nome)
    {
        this->nome = nome;
    }

Onde this é uma variável especial que é um ponteiro para a própria instância da classe em questão. O uso de this é opcional dentro do escopo da classe, de forma que é possível se referir a atributos da mesma diretamente. O código acima altera o valor do membro nome para o valor contido na variável local nome. Diferente do código anterior, this->nome primeiro é inicializado com valor vazio, para somente ser alterado dentro do corpo do construtor de Cachorro.

Assim como qualquer outra variável em C++, instâncias de classes podem ser alocadas na pilha ou na heap. Para alocar uma instância na pilha, basta seguir o procedimento dos exemplos anteriores, copiado abaixo apenas para facilitar a leitura:

int main()
{
    Cachorro bidu{"Bidu"}; // <--- Alocado na pilha
    bidu.latir();
    return 0;
}

Por outro lado, para alocar na heap deve-se fazer uso da palavra reservada new, conforme exemplo abaixo. Tal qual explicado em capítulos anteriores toda memória alocada na heap deve ser desalocada pelo programador. No caso de variáveis inicializadas com new é necessário utilizar o delete.

int main()
{
    auto* bidu = new Cachorro{"Bidu"}; // <--- Alocado na heap
    bidu->latir();
    delete bidu;
    return 0;
}

Note que a sintaxe de acesso ao método latir mudou. Isso ocorre por que no exemplo acima bidu é um ponteiro para uma instância de Cachorro. Dessa forma o acesso ao método latir é feito com -> ao invés de .. Alternativamente poderia-se de-referenciar o ponteiro. Porém, a sintaxe ficaria bastante esquisita e não é sugerida:

int main()
{
    auto* bidu = new Cachorro{"Bidu"}; // <--- Alocado na heap
    (*bidu).latir();
    delete bidu;
    return 0;
}

O objeto bidu será destruído no momento que delete é invocado. No caso da variável em pilha o objeto é destruido no momento que sair do escopo. Em ambos os casos é possível invocar um código especial de destrução do objeto. Esse código fica no método destrutor da classe que possui nome equivalente ao do construtor, porém, com o simbolo ~ como prefixo. O destrutor é útil quando é necessário liberar algum recurso obtido em algum momento da vida do objeto. Exemplo:

#include <iostream>
#include <string>

class Cachorro {
public:
    Cachorro(std::string const& nome)
        : nome(nome)
    {}

    ~Cachorro()
    {
        // Código executado no momento que o cachorro está sendo destruido
        std::cout << "Estou sendo destruido!!\n";
    }

    void latir()
    {
        std::cout << "Au! Au!! Eu sou o " << this->nome << '\n';
    }

private:
    std::string nome;
};

int main()
{
    Cachorro bidu{"Bidu"};
    bidu.latir();
    return 0;
}

Esses são os conceitos básicos de orientação a objetos necessários para iniciar um contato com o tema em linguagem C++. Os capítulos posteriores vão entrar em outros conceitos fundamentais e de extrema importância para a programação orientada a objetos. Por fim, é importante mencionar que programação orientada a objetos é um paradigma muito interessante e pode ajudar na solução de vários problemas. Porém, tenha sempre em mente que não é o único paradigma de programação e que nem sempre é o melhor para qualquer situação.

Herança e Polimorfismo

Chama-se herança o mecanismo que permite a uma classe herdar membros (métodos e atributos) de outra classe. Tal mecanismo permite reaproveitar código comum entre classes filhas. Porém, uma vantagem ainda mais importante é a possibilidade de criar interfaces.

A sintaxe para a criação de uma classe base é mostrada no exemplo abaixo. A classe ClasseFilha estende a classe ClasseMae. Em outras palavras, a classe ClasseFilha é formada por todos os seus atributos e métodos mais todos os da classe ClasseMae.

#include <iostream>
#include <string>

class ClasseMae {
public:
    ClasseMae(int atributo_1)
        : atributo_1(atributo_1)
        , atributo_privado('z')
    {}

    void metodo_1()
    {
        std::cout << "O metodo 1 foi invocado!\n";
    }

protected:
    int atributo_1;
private:
    char atributo_privado;
};

class ClasseFilha : public ClasseMae {
public:
    ClasseFilha(int atributo_1, double atributo_2)
        : ClasseMae(atributo_1)
        , atributo_2(atributo_2)
    {}

    void metodo_2()
    {
        std::cout << "O metodo 2 foi invocado!";
    }

private:
    double atributo_2;
};

int main()
{
    ClasseMae objeto_1{1};
    objeto_1.metodo_1();

    ClasseFilha objeto_2{1, 2.2};
    objeto_2.metodo_1();
    objeto_2.metodo_2();

    return 0;
}

O exemplo é extremamente simples, e serve apenas para ilustrar a sintaxe de herança em C++ e iniciar um contato do leitor sobre esses aspectos da linguagem. Note que ambas as instâncias objeto_1 e objeto_2 podem invocar o método metodo_1, e apenas a instância da classe ClasseFilha pode invocar o método metodo_2. Isso ocorre porque objeto_2 é uma instância da classe ClasseFilha, que possui tanto os métodos e atributos de ClasseMae quanto aqueles referentes à própria classe ClasseFilha.

O construtor da classe ClasseFilha precisa invocar o construtor da classe mãe (ClasseMae) de forma a construir corretamente a mesma. A sintaxe para a construção da classe mãe é mostrada no exemplo acima. A classe ClasseFilha possui um construtor que contém uma lista de inicialização onde o construtor da classe mãe é invocado. Caso a classe mãe não tivesse um construtor definido (se estivesse sendo utilizado o construtor padrão), não seria necessário invoca-lo explicitamente como no exemplo acima.

Além disso, é possível reescrever um método da classe mãe, de forma a alterar seu funcionamento, na classe filha. Considere o exemplo abaixo. A classe ClasseMae possui agora um método publico virtual chamado metodo_virtual, e a classe ClasseFilha possui um método de mesmo nome com o modificador override. Esse modificador é opcional e está disponível apenas em versões mais atuais da linguagem. É uma boa prática de programação adiciona-lo, a fim de manter uma consistência entre classe base e classe filha.

#include <iostream>

class ClasseMae {
public:
    virtual void metodo_virtual()
    {
        std::cout << "Comportamento da classe mae.\n";
    }
};

class ClasseFilha : public ClasseMae {
public:
    void metodo_virtual() override
    {
        std::cout << "Outro comportamento!!\n";
    }
};

void executar_algoritmo(ClasseMae& objeto)
{
    // ... Imagine um algoritmo aqui ...
    objeto.metodo_virtual();
}

int main()
{
    ClasseMae objeto_1;
    executar_algoritmo(objeto_1);

    ClasseFilha objeto_2;
    executar_algoritmo(objeto_2);

    return 0;
}

Note que a função executar_algoritmo recebe como parâmetro uma referência para um objeto da classe ClasseMae. Ainda assim, na função main, uma instância da classe ClasseFilha é enviada como parâmetro. Sempre é possível utilizar instâncias de classes filhas como parâmetros para referências ou ponteiros para sua classe mãe. O método executar_algoritmo vai se comportar de forma diferente, dependendo do tipo de objeto sendo recebido como parâmetro. Sendo mais específico, para o caso do exemplo acima a primeira chamada de executar_algoritmo(objeto_1) resultará no texto escrito Comportamento da classe mae., enquanto a segunda chamada executar_algoritmo(objeto_2) resultará no texto Outro comportamento!!. Essa característica se chama Polimorfismo.

Um aspecto muito importante de se ter em mente é que no caso da ausência do virtual na classe mãe, o polimorfismo do exemplo não acontecerá conforme esperado. Basicamente a ausência de virtual ignora a funcionalidade que possibilita a invocação correta do método da classe filha, quando a mesma é passada por parâmetro como uma classe base, como no exemplo. Em outras palavras, a remoção de virtual (e por consequência a remoção de override) no exemplo resultaria que as duas chamadas retornarem o texto Comportamento da classe mae..

Uma classe pode possuir métodos virtuais sem implementação, como no próximo exemplo. Classes com pelo menos um método virtual sem implementação não podem ser instanciadas. Ou seja, não é possível construir um objeto cujo tipo seja uma classe com métodos sem implementação. Classes cujos métodos são todos virtuais e sem implementação são chamadas interfaces. Classes que contenham pelo menos um método com implementação são chamadas classes abstratas. Portanto, no exemplo abaixo, a classe ClasseInterface é uma interface.

#include <iostream>

class ClasseInterface {
public:
    virtual void metodo_a() = 0;
};

class ClasseConcreta_A : public ClasseInterface {
public:
    void metodo_a() override {
        std::cout << "Metodo A invocado pela classe concreta A!\n";
    }
};

class ClasseConcreta_B : public ClasseInterface {
public:
    void metodo_a() override {
        std::cout << "Metodo A invocado pela classe concreta B!\n";
    }
};

void executar_algoritmo(ClasseInterface& objeto)
{
    objeto.metodo_a();
}

int main()
{
    ClasseConcreta_A objeto_1;
    executar_algoritmo(objeto_1);

    ClasseConcreta_B objeto_2;
    executar_algoritmo(objeto_2);

    return 0;
}

Um exemplo prático do uso de polimorfismo é mostrado a seguir. Imagine que tenhamos um sistema com vários elementos gráficos: retangulos, circulos, triangulos... Cada estrutura é representada por uma classe com as informações pertinentes. Por exemplo:

class Retangulo {
private:
    Vetor posicao;
    double largura;
    double altura;
};

class Circulo {
private:
    Vetor posicao;
    double raio;
};

Para desenhar um conjunto de elementos gráficos na tela poderíamos ter uma interface em comum para eles, e determinar um método para cada classe desenhar-se:

class ElementoGrafico {
public:
    virtual void desenhar() const = 0;
};

class Retangulo : ElementoGrafico {
public:
    void desenhar() const override { /*...*/ }
/*...*/
};

Por fim, no programa principal é possível ter um vetor de ElementoGrafico, e considerando que cada um deles sabe se desenhar, o código final fica:

void desenhar_tela(std::vector<std::unique_ptr<ElementoGrafico>> const& elementos)
{
    for (auto const& elemento : elementos) {
        elemento->desenhar();
    }
}

O código completo é apresentado abaixo. O código apresentado é apenas ilustrativo, visto que ele não desenha nada de fato na tela, ele apenas escreve textos no terminal.

#include <iostream>
#include <vector>
#include <memory>

struct Vetor {
    double x;
    double y;
};

class ElementoGrafico {
public:
    virtual void desenhar() const = 0;
};

class Retangulo : public ElementoGrafico {
public:
    void desenhar() const override
    {
        std::cout << "Desenhando o retangulo!\n";
    }

private:
    Vetor posicao;
    double largura;
    double altura;
};

class Circulo : public ElementoGrafico {
public:
    void desenhar() const override
    {
        std::cout << "Desenhando o circulo!\n";
    }

private:
    Vetor posicao;
    double raio;
};

void desenhar_tela(std::vector<std::unique_ptr<ElementoGrafico>> const& elementos)
{
    for (auto const& elemento : elementos) {
        elemento->desenhar();
    }
}

int main()
{
    auto elementos = std::vector<std::unique_ptr<ElementoGrafico>>{};
    elementos.push_back(std::make_unique<Circulo>());
    elementos.push_back(std::make_unique<Retangulo>());

    desenhar_tela(elementos);    
}

Utiliza-se um std::vector de unique_ptr neste exemplo pois não é possível criar um std::vector<ElementoGrafico> (já que trata-se de uma interface) e nem um std::vector<ElementoGrafico&> (já que não é possível criar um vector de referências). A última alternativa seria std::vector<ElementoGrafico*>, que funcionaria normalmente, exceto pelo fato de que seria necessário fazer o gerenciamento da memória de alguma forma. Para delegar esse gerenciamento, decide-se utilizar os smart pointers (nesse caso unique_ptr) de forma a simplificar o código final.

FAQ - Frequent Asked Questions

Perguntas que surgem recorrentemente no canal C/C++ Brasil são movidas para esse FAQ, de forma a centralizar a informação e minimizar o número de discussões repetidas.

Qual o melhor editor/IDE pra programar em C++?

De forma geral, aquele que você se sente mais confortável. Se você está começando e ainda não sabe qual é o mais confortável pra você, abaixo deixamos uma lista de sugestões. Sugerimos que iniciantes não usem o Code::Blocks por ser um projeto abandonado.

  1. KDevelop
  2. (Windows) Visual Studio Community Edition
  3. CLion Student Edition

Se você não gosta de IDEs (Integrated Development Environment), existem bons editores de texto para programar em C++:

  1. Vim
  2. NeoVIM
  3. Kate

Essa lista é baseada no que as pessoas do canal usam (Se você usa algo que não está na lista, fique à vontade para adicionar). Ao atualizar a lista por favor mantenha os projetos open-source no topo, em qualquer ordem.