Projeto Contas Bancárias (POO)

Um banco é o exemplo perfeito de por que a orientação a objetos existe: o saldo de uma conta não pode ser mudado por qualquer um, de qualquer jeito. Neste projeto você vai construir, em oito etapas, um pequeno banco em C++: contas que depositam e sacam com validação, uma exceção própria para "saldo insuficiente", uma conta poupança que rende no fim do mês, uma lista de contas de tipos diferentes, um extrato bem alinhado e, no fim, um programa dividido em três arquivos com um menu que você usa digitando comandos.

Você vai praticar

Todos os programas foram compilados e executados na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp/banco, com o g++ 15.2 e o comando padrão do guia, g++ -std=c++23 -Wall -Wextra. Cada etapa tem uma caixa Experimente em que você faz a etapa no navegador.


O que vamos construir

No fim, a pasta do projeto terá três arquivos:

$ ls
conta.cpp
conta.h
main.cpp

E o programa funciona assim (o que vem depois de banco> é o que a Ana digitou):

$ g++ -std=c++23 -Wall -Wextra main.cpp conta.cpp -o banco
$ ./banco
Banco da Ana. Comandos: l, d N VALOR, s N VALOR, e N, m, q
banco> l
1. Ana (Corrente)
2. Bruno (Poupança)
banco> d 1 1000
Depósito feito.
banco> d 2 1000
Depósito feito.
banco> s 1 250
Saque feito.
banco> s 2 5000
Recusado: saldo insuficiente (saldo 1000.00, pedido 5000.00)
banco> d 3 10
Erro: não existe a conta 3
banco> d 1 -5
Erro: o valor precisa ser positivo
banco> m
Mês fechado.
banco> e 1
Extrato de Ana (Corrente)
  Depósito      +1000.00
  Saque          -250.00
  Saldo           750.00
banco> e 2
Extrato de Bruno (Poupança)
  Depósito      +1000.00
  Rendimento       +5.00
  Saldo          1005.00
banco> q

Até logo!

O caminho até lá:

  1. A classe Conta: titular, saldo privado, depósito.
  2. Validar depósito e saque devolvendo bool.
  3. A exceção SaldoInsuficiente: throw, try e catch.
  4. A conta poupança: herança, virtual e override.
  5. Uma lista de contas: std::vector<std::unique_ptr<Conta>>.
  6. O extrato com std::format.
  7. Separar em conta.h, conta.cpp e main.cpp.
  8. O menu: comandos digitados pelo usuário.

Antes de começar

Este projeto junta assuntos do Nível 3. Se algum termo for novidade, a aula indicada em cada etapa explica com calma; aqui o foco é ver as peças funcionando juntas. Crie a pasta do projeto:

$ mkdir -p cpp/banco
$ cd cpp/banco

Nas etapas 1 a 6, todo o programa fica num arquivo só, banco.cpp, que vai crescendo. Na etapa 7 ele é dividido em três.


Etapa 1 · A classe Conta

Uma conta tem duas informações: o titular (o nome do dono) e o saldo. E sabe fazer uma coisa, por enquanto: receber um depósito. Em C++, isso vira uma classe:

#include <iostream>
#include <string>

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    void depositar(double valor) {
        saldo_ += valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(150.0);
    ana.depositar(49.90);
    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';
}
TrechoO que faz
class Conta { … };O molde de toda conta. Repare no ; depois da chave final: esquecê-lo é um erro clássico.
public:Daqui para baixo, tudo pode ser usado por quem tem uma conta (o main, por exemplo).
explicit Conta(std::string titular) : titular_{titular} {}O construtor: roda quando a conta nasce e guarda o nome em titular_. O explicit impede que um texto solto vire conta "sem querer" (por exemplo, numa função que espera uma Conta e recebe "Ana").
void depositar(double valor)Soma o valor ao saldo. É o único jeito de fazer o saldo crescer.
double saldo() constDevolve o saldo para quem quiser ler. O const promete que o método não muda a conta (veja métodos const).
private:Daqui para baixo, só os métodos da própria classe enxergam. É o cofre.
titular_, saldo_Os dados. O _ no fim é um costume para diferenciar o dado (saldo_) do método que o lê (saldo()).
double saldo_ = 0.0;Toda conta nasce zerada. Nunca deixe um número sem valor inicial.
Conta ana{"Ana"};Cria o objeto ana chamando o construtor.
$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Ana tem R$ 199.9

O std::cout mostra 199.9, sem o zero do fim. Na etapa 6 o std::format vai mostrar 199.90, como se espera de dinheiro.

O cofre funciona?

O motivo de o saldo ser private é que ninguém de fora possa escrever ana.saldo_ = 1000000;. Coloque essa linha no main, logo depois dos depósitos, e compile:

$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
banco.cpp: In function ‘int main()’:
banco.cpp:24:9: error: ‘double Conta::saldo_’ is private within this context
   24 |     ana.saldo_ = 1000000;
      |         ^~~~~~
banco.cpp:17:12: note: declared private here
   17 |     double saldo_ = 0.0;
      |            ^~~~~~
banco.cpp:24:9: note: field ‘double Conta::saldo_’ can be accessed via ‘double Conta::saldo() const’
   24 |     ana.saldo_ = 1000000;
      |         ^~~~~~
      |         saldo()

Lendo de cima para baixo: error: ‘double Conta::saldo_’ is private within this context ("é privado neste contexto": o main não pode mexer). A primeira note mostra onde ele foi declarado private, e a segunda até sugere o caminho certo: ler pelo método saldo(). É exatamente o que queremos: o compilador protege o saldo para você. Apague a linha e siga em frente.

double e dinheiro

O double guarda números em binário, e a maioria dos centavos não tem representação exata em binário (como 1/3 não tem em decimal). Veja:

#include <format>
#include <iostream>

int main() {
    double total = 0.1 + 0.2;
    std::cout << total << '\n';
    std::cout << std::format("{}", total) << '\n';
    std::cout << (total == 0.3) << '\n';
}
$ g++ -std=c++23 -Wall -Wextra frac.cpp -o frac
$ ./frac
0.3
0.30000000000000004
0

O cout arredonda e mostra 0.3, mas o valor guardado é 0.30000000000000004, e por isso total == 0.3 dá falso (0). Para aprender POO, o double serve. Um banco de verdade guarda centavos inteiros (long long, com R$ 10,50 guardado como 1050): fica como desafio no fim da página. Mais em float e double.

Missão (etapa 1): crie uma segunda conta, bruno, do titular "Bruno". Deposite 300 e depois 25.50, e mostre a frase dele igual à da Ana. A saída deve terminar com:

Bruno tem R$ 325.5
#include <iostream>
#include <string>

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    void depositar(double valor) {
        saldo_ += valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(150.0);
    ana.depositar(49.90);
    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';
}
#include <iostream>
#include <string>

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    void depositar(double valor) {
        saldo_ += valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(150.0);
    ana.depositar(49.90);
    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';

    Conta bruno{"Bruno"};
    bruno.depositar(300.0);
    bruno.depositar(25.50);
    std::cout << bruno.titular() << " tem R$ " << bruno.saldo() << '\n';
}

Etapa 2 · Validar depósito e saque

A conta da etapa 1 aceita ana.depositar(-500), e isso é, na prática, um saque escondido. Também falta o saque. As regras do banco:

  • depósito e saque só com valor positivo;
  • saque só se houver saldo.

Primeira forma de fazer: cada método devolve bool (true = deu certo, false = recusado) e não mexe no saldo quando recusa.

#include <iostream>
#include <string>

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    bool depositar(double valor) {
        if (valor <= 0) {
            return false;          // recusado: nada muda
        }
        saldo_ += valor;
        return true;
    }

    bool sacar(double valor) {
        if (valor <= 0 || valor > saldo_) {
            return false;
        }
        saldo_ -= valor;
        return true;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(1000.0);

    if (ana.sacar(250.0)) {
        std::cout << "Saque de 250 feito.\n";
    }
    if (!ana.sacar(5000.0)) {
        std::cout << "Saque de 5000 recusado.\n";
    }
    if (!ana.depositar(-50.0)) {
        std::cout << "Depósito de -50 recusado.\n";
    }
    ana.sacar(800.0);      // ninguém conferiu se deu certo...
    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';
}
TrechoO que faz
bool depositar(double valor)Agora o método responde se deu certo.
if (valor <= 0) { return false; }Recusa antes de mexer em qualquer coisa. O return sai do método na hora.
valor <= 0 || valor > saldo_No saque, duas condições de recusa ligadas por "ou" (operadores lógicos).
if (!ana.sacar(5000.0))Quem chama confere a resposta. O ! lê-se "se não conseguiu sacar".
$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Saque de 250 feito.
Saque de 5000 recusado.
Depósito de -50 recusado.
Ana tem R$ 750

Funciona, mas olhe a penúltima linha do main: ana.sacar(800.0);. O saque foi recusado (só havia 750) e ninguém percebeu: o programa seguiu como se o dinheiro tivesse saído. Com bool, conferir é opcional, e o que é opcional acaba esquecido.

O C++ tem um remédio parcial: o atributo [[nodiscard]] ("não descarte") C++17. Escreva-o antes de bool nos dois métodos ([[nodiscard]] bool depositar(…)) e compile de novo:

$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
banco.cpp: In function ‘int main()’:
banco.cpp:34:18: warning: ignoring return value of ‘bool Conta::depositar(double)’, declared with attribute ‘nodiscard’ [-Wunused-result]
   34 |     ana.depositar(1000.0);
      |     ~~~~~~~~~~~~~^~~~~~~~
banco.cpp:8:24: note: declared here
    8 |     [[nodiscard]] bool depositar(double valor) {
      |                        ^~~~~~~~~
banco.cpp:45:14: warning: ignoring return value of ‘bool Conta::sacar(double)’, declared with attribute ‘nodiscard’ [-Wunused-result]
   45 |     ana.sacar(800.0);      // ninguém conferiu se deu certo...
      |     ~~~~~~~~~^~~~~~~
banco.cpp:16:24: note: declared here
   16 |     [[nodiscard]] bool sacar(double valor) {
      |                        ^~~~~

Dois avisos: ignoring return value … declared with attribute ‘nodiscard’ ("ignorando o valor de retorno"). O segundo pega justamente o saque esquecido. O primeiro pega o depósito inicial, que também não foi conferido. É uma ajuda, mas ainda é só um aviso: o programa compila e roda do mesmo jeito. Na próxima etapa, um saque recusado vai interromper o caminho normal do programa, e não dá para ignorar.

Missão (etapa 2): o método sacar está vazio: sempre recusa. Escreva o corpo dele: devolva false se o valor for zero, negativo ou maior que o saldo; senão, tire o valor do saldo e devolva true. (O aviso unused parameter ‘valor’ some quando você usar o parâmetro.) A saída deve ser:

Saque de 250 feito.
Saque de 5000 recusado.
Depósito de -50 recusado.
Ana tem R$ 750
#include <iostream>
#include <string>

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    bool depositar(double valor) {
        if (valor <= 0) {
            return false;          // recusado: nada muda
        }
        saldo_ += valor;
        return true;
    }

    bool sacar(double valor) {
        // Missão: recuse (return false) se o valor for zero, negativo
        // ou maior que o saldo; senão, tire do saldo e devolva true.
        return false;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(1000.0);

    if (ana.sacar(250.0)) {
        std::cout << "Saque de 250 feito.\n";
    }
    if (!ana.sacar(5000.0)) {
        std::cout << "Saque de 5000 recusado.\n";
    }
    if (!ana.depositar(-50.0)) {
        std::cout << "Depósito de -50 recusado.\n";
    }
    ana.sacar(800.0);      // ninguém conferiu se deu certo...
    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';
}
#include <iostream>
#include <string>

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    bool depositar(double valor) {
        if (valor <= 0) {
            return false;          // recusado: nada muda
        }
        saldo_ += valor;
        return true;
    }

    bool sacar(double valor) {
        if (valor <= 0 || valor > saldo_) {
            return false;
        }
        saldo_ -= valor;
        return true;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(1000.0);

    if (ana.sacar(250.0)) {
        std::cout << "Saque de 250 feito.\n";
    }
    if (!ana.sacar(5000.0)) {
        std::cout << "Saque de 5000 recusado.\n";
    }
    if (!ana.depositar(-50.0)) {
        std::cout << "Depósito de -50 recusado.\n";
    }
    ana.sacar(800.0);      // ninguém conferiu se deu certo...
    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';
}

Etapa 3 · A exceção SaldoInsuficiente

Uma exceção é um aviso de "deu errado" que não pode ser ignorado: quando um método faz throw (lança), o programa para o que estava fazendo e pula para o catch (captura) mais próximo. Se não houver nenhum, o programa termina.

Para o saque sem saldo, vamos criar a nossa própria exceção, SaldoInsuficiente. Para valor zero ou negativo, usamos uma que já vem pronta na biblioteca: std::invalid_argument ("argumento inválido").

#include <format>
#include <iostream>
#include <stdexcept>
#include <string>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    void depositar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(1000.0);

    try {
        ana.sacar(250.0);
        std::cout << "Saque de 250 feito.\n";
        ana.sacar(5000.0);
        std::cout << "Esta linha não aparece.\n";
    } catch (const SaldoInsuficiente& e) {
        std::cout << "Recusado: " << e.what() << '\n';
    }

    try {
        ana.depositar(-50.0);
    } catch (const std::invalid_argument& e) {
        std::cout << "Erro: " << e.what() << '\n';
    }

    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';
}
TrechoO que faz
#include <stdexcept>Traz as exceções prontas: std::runtime_error, std::invalid_argument, std::out_of_range…
class SaldoInsuficiente : public std::runtime_errorA nossa exceção é um runtime_error (herança, que a etapa 4 explica). Com isso ela ganha de graça o método what(), que devolve a mensagem.
: std::runtime_error{std::format(…)}O construtor monta a mensagem com o saldo e o pedido e entrega ao runtime_error. O {:.2f} mostra o número com 2 casas (detalhes na etapa 6).
throw std::invalid_argument{"…"};Lança a exceção. Nada depois dela, no método, é executado: o saldo não muda.
throw SaldoInsuficiente{saldo_, valor};Lança a nossa, com os dois números.
void sacar(…)Voltou a ser void: não precisa mais responder true/false. Se voltou do método, deu certo.
try { … }"Tente": o trecho que pode lançar.
catch (const SaldoInsuficiente& e)Captura só esse tipo de exceção. O const & evita copiar o objeto da exceção; use sempre assim.
e.what()A mensagem montada no construtor.
$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Saque de 250 feito.
Recusado: saldo insuficiente (saldo 750.00, pedido 5000.00)
Erro: o valor precisa ser positivo
Ana tem R$ 750

Repare que Esta linha não aparece. de fato não aparece: o throw do segundo saque pulou direto para o catch. E o saldo continua 750: nenhuma operação recusada mexeu nele.

Quando dá errado: uma exceção que ninguém captura

Coloque ana.sacar(800.0); fora de qualquer try, logo antes da última linha do main:

$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Saque de 250 feito.
Recusado: saldo insuficiente (saldo 750.00, pedido 5000.00)
Erro: o valor precisa ser positivo
terminate called after throwing an instance of 'SaldoInsuficiente'
  what():  saldo insuficiente (saldo 750.00, pedido 800.00)
Abortado                   (imagem do núcleo gravada) ./banco
$ echo $?
134
  • terminate called after throwing an instance of 'SaldoInsuficiente': "terminar, chamado depois de lançar um SaldoInsuficiente". Como ninguém capturou, o C++ chama std::terminate, que encerra o programa.
  • what(): mostra a nossa mensagem. Ainda bem que ela diz o saldo e o pedido.
  • Abortado (imagem do núcleo gravada) é o Bash avisando que o programa foi abortado; o código de saída 134 é 128 + 6, o número do sinal de "abortar".
  • A frase final Ana tem R$ 750 nunca foi impressa. Diferente do bool esquecido, o erro não passou despercebido.

Como consertar: toda operação que pode lançar fica dentro de um try com o catch certo, como as outras do main.

Sem -std=c++23 o std::format não existe

O g++ 15 usa C++17 se você não pedir outro padrão. Compilando com g++ -Wall -Wextra banco.cpp -o banco, a primeira mensagem é error: ‘format’ is not a member of ‘std’, seguida da dica note: ‘std::format’ is only available from C++20 onwards. Use sempre o comando padrão do guia.

Missão (etapa 3): o sacar já lança as exceções, mas o depositar aceita qualquer valor, e o depósito de −50 tirou dinheiro da Ana (ela ficou com 700). Faça o depositar lançar std::invalid_argument com a mensagem "o valor precisa ser positivo" quando o valor não for positivo. O main já tem o catch. A saída deve ser:

Saque de 250 feito.
Recusado: saldo insuficiente (saldo 750.00, pedido 5000.00)
Erro: o valor precisa ser positivo
Ana tem R$ 750
#include <format>
#include <iostream>
#include <stdexcept>
#include <string>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    void depositar(double valor) {
        // Missão: lance std::invalid_argument se o valor não for positivo.
        saldo_ += valor;
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(1000.0);

    try {
        ana.sacar(250.0);
        std::cout << "Saque de 250 feito.\n";
        ana.sacar(5000.0);
        std::cout << "Esta linha não aparece.\n";
    } catch (const SaldoInsuficiente& e) {
        std::cout << "Recusado: " << e.what() << '\n';
    }

    try {
        ana.depositar(-50.0);
    } catch (const std::invalid_argument& e) {
        std::cout << "Erro: " << e.what() << '\n';
    }

    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';
}
#include <format>
#include <iostream>
#include <stdexcept>
#include <string>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}

    void depositar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

int main() {
    Conta ana{"Ana"};
    ana.depositar(1000.0);

    try {
        ana.sacar(250.0);
        std::cout << "Saque de 250 feito.\n";
        ana.sacar(5000.0);
        std::cout << "Esta linha não aparece.\n";
    } catch (const SaldoInsuficiente& e) {
        std::cout << "Recusado: " << e.what() << '\n';
    }

    try {
        ana.depositar(-50.0);
    } catch (const std::invalid_argument& e) {
        std::cout << "Erro: " << e.what() << '\n';
    }

    std::cout << ana.titular() << " tem R$ " << ana.saldo() << '\n';
}

Etapa 4 · A conta poupança

A poupança é uma conta com tudo o que a conta comum tem (titular, saldo, depósito, saque) e uma diferença: no fim do mês, ela rende juros. Em vez de copiar a classe inteira, dizemos que ContaPoupanca herda de Conta (herança) e escrevemos só o que muda.

Para o "fim do mês" funcionar com qualquer conta, a classe base ganha dois métodos virtual: tipo() (o nome do tipo de conta) e fecharMes(). virtual quer dizer "uma classe filha pode trocar este método pelo dela, e o C++ vai chamar o da filha mesmo quando só souber que tem uma Conta nas mãos". Isso é o polimorfismo.

Os #include, a SaldoInsuficiente, o depositar e o sacar continuam iguais à etapa 3. O resto do banco.cpp fica assim (o arquivo inteiro está na missão desta etapa):

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}
    virtual ~Conta() = default;

    virtual std::string tipo() const { return "Corrente"; }
    virtual void fecharMes() {}      // a conta corrente não rende

    // depositar, sacar, titular() e saldo(): iguais à etapa 3

private:
    std::string titular_;
    double saldo_ = 0.0;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa)
        : Conta{titular}, taxa_{taxa} {}

    std::string tipo() const override { return "Poupança"; }

    void fecharMes() override {
        double rendimento = saldo() * taxa_;
        if (rendimento > 0) {
            depositar(rendimento);
        }
    }

private:
    double taxa_;
};

void mostrar(const Conta& conta) {
    std::cout << conta.titular() << " (" << conta.tipo() << "): R$ "
              << conta.saldo() << '\n';
}

int main() {
    Conta ana{"Ana"};
    ContaPoupanca bruno{"Bruno", 0.005};    // rende 0,5% ao mês

    ana.depositar(1000.0);
    bruno.depositar(1000.0);                // depositar veio de Conta
    mostrar(ana);
    mostrar(bruno);

    ana.fecharMes();
    bruno.fecharMes();
    std::cout << "--- fim do mês ---\n";
    mostrar(ana);
    mostrar(bruno);
}
TrechoO que faz
virtual ~Conta() = default;Destrutor virtual. Regra: classe com métodos virtual tem destrutor virtual. Sem ele, apagar uma poupança por um ponteiro de Conta (etapa 5) não chamaria a parte da poupança. = default = "o padrão serve".
virtual std::string tipo() const { return "Corrente"; }A versão da conta comum. As filhas podem trocar.
virtual void fecharMes() {}A conta comum não faz nada no fim do mês: corpo vazio.
class ContaPoupanca : public Conta"Poupança é uma Conta". Herda tudo; o private da Conta continua fechado até para a filha, que mexe no saldo só pelos métodos públicos.
: Conta{titular}, taxa_{taxa}O construtor da filha chama primeiro o da mãe (que guarda o titular) e depois guarda a taxa.
std::string tipo() const overrideoverride ("sobrescreve") diz ao compilador: "esta função substitui uma virtual da mãe". Se não substituir nada (nome errado, const a mais…), é erro de compilação.
double rendimento = saldo() * taxa_;Com taxa 0.005 (0,5%), mil reais rendem 5.
depositar(rendimento);O rendimento entra pelo depositar herdado: a poupança não precisa (nem consegue) mexer em saldo_ direto.
void mostrar(const Conta& conta)Uma função que recebe qualquer conta por referência. Graças ao virtual, conta.tipo() chama a versão certa.
$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Ana (Corrente): R$ 1000
Bruno (Poupança): R$ 1000
--- fim do mês ---
Ana (Corrente): R$ 1000
Bruno (Poupança): R$ 1005

As duas contas receberam o mesmo depósito e o mesmo fecharMes(), mas só a do Bruno rendeu. E a função mostrar, que só conhece Conta, escreveu "Poupança" para o Bruno.

Quando dá errado: esquecer o virtual

Tire as palavras virtual e override de tipo() e fecharMes(). Compila sem nenhum aviso, mas veja:

$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Ana (Corrente): R$ 1000
Bruno (Corrente): R$ 1000
--- fim do mês ---
Ana (Corrente): R$ 1000
Bruno (Corrente): R$ 1005

O Bruno virou "Corrente" na tela. Sem virtual, a função mostrar recebe uma Conta& e chama o tipo() da Conta, sem olhar que o objeto é uma poupança. O rendimento ainda entrou só porque o main chamou bruno.fecharMes() direto numa variável do tipo ContaPoupanca; numa lista de contas (etapa 5) ele também se perderia. Como consertar: virtual na mãe e override na filha, sempre os dois.

Quando dá errado: um override que não sobrescreve nada

Um erro de digitação, fecharmes com m minúsculo:

$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
banco.cpp:53:10: error: ‘void ContaPoupanca::fecharmes()’ marked ‘override’, but does not override
   53 |     void fecharmes() override {
      |          ^~~~~~~~~

marked ‘override’, but does not override: "marcado como override, mas não sobrescreve". Não existe fecharmes virtual na Conta. Sem o override, esse erro passaria em silêncio: a poupança ganharia um método novo que ninguém chama, e nunca renderia. É por isso que o override vale a pena.

Missão (etapa 4): crie a conta que cobra tarifa. A classe ContaTarifada já herda de Conta; escreva nela tipo(), devolvendo "Tarifada", e fecharMes(), que saca a tarifa de 12.90 (use o sacar herdado). Não esqueça o override. A saída deve ser:

Carla (Tarifada): R$ 87.1
#include <format>
#include <iostream>
#include <stdexcept>
#include <string>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}
    virtual ~Conta() = default;

    virtual std::string tipo() const { return "Corrente"; }
    virtual void fecharMes() {}      // a conta corrente não rende

    void depositar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa)
        : Conta{titular}, taxa_{taxa} {}

    std::string tipo() const override { return "Poupança"; }

    void fecharMes() override {
        double rendimento = saldo() * taxa_;
        if (rendimento > 0) {
            depositar(rendimento);
        }
    }

private:
    double taxa_;
};

class ContaTarifada : public Conta {
public:
    explicit ContaTarifada(std::string titular) : Conta{titular} {}

    // Missão: tipo() devolve "Tarifada" e fecharMes() saca a tarifa de 12.90.
};

void mostrar(const Conta& conta) {
    std::cout << conta.titular() << " (" << conta.tipo() << "): R$ "
              << conta.saldo() << '\n';
}

int main() {
    ContaTarifada carla{"Carla"};
    carla.depositar(100.0);
    carla.fecharMes();
    mostrar(carla);
}
#include <format>
#include <iostream>
#include <stdexcept>
#include <string>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}
    virtual ~Conta() = default;

    virtual std::string tipo() const { return "Corrente"; }
    virtual void fecharMes() {}      // a conta corrente não rende

    void depositar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa)
        : Conta{titular}, taxa_{taxa} {}

    std::string tipo() const override { return "Poupança"; }

    void fecharMes() override {
        double rendimento = saldo() * taxa_;
        if (rendimento > 0) {
            depositar(rendimento);
        }
    }

private:
    double taxa_;
};

class ContaTarifada : public Conta {
public:
    explicit ContaTarifada(std::string titular) : Conta{titular} {}

    std::string tipo() const override { return "Tarifada"; }

    void fecharMes() override {
        sacar(12.90);
    }
};

void mostrar(const Conta& conta) {
    std::cout << conta.titular() << " (" << conta.tipo() << "): R$ "
              << conta.saldo() << '\n';
}

int main() {
    ContaTarifada carla{"Carla"};
    carla.depositar(100.0);
    carla.fecharMes();
    mostrar(carla);
}

Etapa 5 · Uma lista de contas

Um banco tem muitas contas, de tipos misturados. O natural seria um std::vector<Conta>. Veja o que acontece:

int main() {
    std::vector<Conta> contas;
    contas.push_back(Conta{"Ana"});
    contas.push_back(ContaPoupanca{"Bruno", 0.005});

    for (auto& conta : contas) {
        conta.depositar(1000.0);
        conta.fecharMes();
        std::cout << conta.titular() << " (" << conta.tipo() << "): R$ "
                  << conta.saldo() << '\n';
    }
}
$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Ana (Corrente): R$ 1000
Bruno (Corrente): R$ 1000

Compilou sem aviso e o Bruno virou "Corrente" e não rendeu. Cada lugar de um vector<Conta> tem o tamanho exato de uma Conta. Ao guardar a poupança ali, o C++ copia só a parte Conta e joga fora o resto (a taxa e o "jeito poupança" de ser). Isso tem nome: fatiamento (slicing).

A solução é guardar no vetor não as contas, mas ponteiros para elas: todo ponteiro tem o mesmo tamanho, aponte ele para uma conta comum ou para uma poupança. E, para não ter de lembrar de apagar cada conta com delete, usamos o ponteiro inteligente std::unique_ptr (ponteiros inteligentes): quando o vetor some, ele apaga as contas sozinho. As classes ficam iguais à etapa 4; acrescente #include <memory> e #include <vector> no topo e troque o main:

int main() {
    std::vector<std::unique_ptr<Conta>> contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));
    contas.push_back(std::make_unique<ContaPoupanca>("Carla", 0.01));

    for (const auto& conta : contas) {
        conta->depositar(1000.0);
    }
    for (const auto& conta : contas) {
        conta->fecharMes();               // cada uma do seu jeito
    }
    for (const auto& conta : contas) {
        std::cout << conta->titular() << " (" << conta->tipo() << "): R$ "
                  << conta->saldo() << '\n';
    }
}
TrechoO que faz
std::vector<std::unique_ptr<Conta>>"Um vetor de ponteiros únicos para Conta". Cada ponteiro é o único dono da sua conta.
std::make_unique<Conta>("Ana")Cria uma Conta na memória dinâmica e devolve o unique_ptr para ela. Os parênteses levam os argumentos do construtor.
std::make_unique<ContaPoupanca>("Bruno", 0.005)Cria uma poupança. O ponteiro para ContaPoupanca vira, sozinho, ponteiro para Conta, porque toda poupança é uma conta.
conta->depositar(1000.0)Com ponteiro, usa-se -> em vez de ..
const auto& conta : contasPercorre o vetor sem copiar os ponteiros (um unique_ptr não pode ser copiado; veja abaixo).
conta->fecharMes();Polimorfismo de verdade: cada conta faz o seu próprio fechamento.
$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Ana (Corrente): R$ 1000
Bruno (Poupança): R$ 1005
Carla (Poupança): R$ 1010

Agora cada conta se comporta como o que ela é: Bruno rende 0,5%, Carla rende 1%, Ana não rende. E nenhum delete no programa: quando o main termina, o vetor é destruído, cada unique_ptr apaga a sua conta, e o destrutor virtual da etapa 4 garante que a poupança seja apagada inteira.

Quando dá errado: copiar um unique_ptr

No segundo laço, troque const auto& conta por auto conta (sem o &):

$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
banco.cpp: In function ‘int main()’:
banco.cpp:75:23: error: use of deleted function ‘std::unique_ptr<_Tp, _Dp>::unique_ptr(const std::unique_ptr<_Tp, _Dp>&) [with _Tp = Conta; _Dp = std::default_delete<Conta>]’
   75 |     for (auto conta : contas) {
      |                       ^~~~~~
In file included from /usr/include/c++/15/memory:80,
                 from banco.cpp:3:
/usr/include/c++/15/bits/unique_ptr.h:524:7: note: declared here
  524 |       unique_ptr(const unique_ptr&) = delete;
      |       ^~~~~~~~~~
banco.cpp:75:23: note: use ‘-fdiagnostics-all-candidates’ to display considered candidates
   75 |     for (auto conta : contas) {
      |                       ^~~~~~

A mensagem é comprida, mas o começo diz tudo: use of deleted function ‘std::unique_ptr<…>::unique_ptr(const std::unique_ptr<…>&)’, "uso de função apagada: o construtor de cópia do unique_ptr". O auto conta tenta fazer uma cópia de cada ponteiro, e o unique_ptr proíbe cópias (= delete, como mostra a note do arquivo unique_ptr.h): se houvesse duas cópias, as duas tentariam apagar a mesma conta. Como consertar: percorra com auto& ou const auto&.

Missão (etapa 5): abra uma quarta conta, uma poupança do "Diego" com taxa 0.02. Depois do último laço, some o saldo de todas as contas numa variável total (com mais um for) e mostre o total. A saída deve terminar com:

Diego (Poupança): R$ 1020
Total no banco: R$ 4035
#include <format>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}
    virtual ~Conta() = default;

    virtual std::string tipo() const { return "Corrente"; }
    virtual void fecharMes() {}      // a conta corrente não rende

    void depositar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa)
        : Conta{titular}, taxa_{taxa} {}

    std::string tipo() const override { return "Poupança"; }

    void fecharMes() override {
        double rendimento = saldo() * taxa_;
        if (rendimento > 0) {
            depositar(rendimento);
        }
    }

private:
    double taxa_;
};

int main() {
    std::vector<std::unique_ptr<Conta>> contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));
    contas.push_back(std::make_unique<ContaPoupanca>("Carla", 0.01));

    for (const auto& conta : contas) {
        conta->depositar(1000.0);
    }
    for (const auto& conta : contas) {
        conta->fecharMes();               // cada uma do seu jeito
    }
    for (const auto& conta : contas) {
        std::cout << conta->titular() << " (" << conta->tipo() << "): R$ "
                  << conta->saldo() << '\n';
    }
}
#include <format>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}
    virtual ~Conta() = default;

    virtual std::string tipo() const { return "Corrente"; }
    virtual void fecharMes() {}      // a conta corrente não rende

    void depositar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa)
        : Conta{titular}, taxa_{taxa} {}

    std::string tipo() const override { return "Poupança"; }

    void fecharMes() override {
        double rendimento = saldo() * taxa_;
        if (rendimento > 0) {
            depositar(rendimento);
        }
    }

private:
    double taxa_;
};

int main() {
    std::vector<std::unique_ptr<Conta>> contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));
    contas.push_back(std::make_unique<ContaPoupanca>("Carla", 0.01));
    contas.push_back(std::make_unique<ContaPoupanca>("Diego", 0.02));

    for (const auto& conta : contas) {
        conta->depositar(1000.0);
    }
    for (const auto& conta : contas) {
        conta->fecharMes();               // cada uma do seu jeito
    }
    for (const auto& conta : contas) {
        std::cout << conta->titular() << " (" << conta->tipo() << "): R$ "
                  << conta->saldo() << '\n';
    }

    double total = 0.0;
    for (const auto& conta : contas) {
        total += conta->saldo();
    }
    std::cout << "Total no banco: R$ " << total << '\n';
}

Etapa 6 · O extrato com std::format

Um banco precisa lembrar cada movimento, não só o saldo. Cada movimento tem uma descrição ("Depósito", "Saque", "Rendimento") e um valor: positivo quando entra, negativo quando sai. Isso é uma struct, e a conta guarda um std::vector delas. Para mostrar o extrato em colunas alinhadas, usamos std::format.

#include <format>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

struct Movimento {
    std::string descricao;
    double valor;              // positivo entra, negativo sai
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}
    virtual ~Conta() = default;

    virtual std::string tipo() const { return "Corrente"; }
    virtual void fecharMes() {}      // a conta corrente não rende

    void depositar(double valor, std::string descricao = "Depósito") {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
        movimentos_.push_back({descricao, valor});
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
        movimentos_.push_back({"Saque", -valor});
    }

    std::string extrato() const {
        std::string texto = std::format("Extrato de {} ({})\n", titular_, tipo());
        for (const auto& m : movimentos_) {
            texto += std::format("  {:<12}{:>+10.2f}\n", m.descricao, m.valor);
        }
        texto += std::format("  {:<12}{:>10.2f}\n", "Saldo", saldo_);
        return texto;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
    std::vector<Movimento> movimentos_;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa)
        : Conta{titular}, taxa_{taxa} {}

    std::string tipo() const override { return "Poupança"; }

    void fecharMes() override {
        double rendimento = saldo() * taxa_;
        if (rendimento > 0) {
            depositar(rendimento, "Rendimento");
        }
    }

private:
    double taxa_;
};

int main() {
    std::vector<std::unique_ptr<Conta>> contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    contas[0]->depositar(1000.0);
    contas[0]->sacar(250.0);
    contas[0]->depositar(49.9, "Pix da Carla");
    contas[1]->depositar(1000.0);

    for (const auto& conta : contas) {
        conta->fecharMes();
        std::cout << conta->extrato() << '\n';
    }
}

O que mudou em relação à etapa 5:

TrechoO que faz
struct Movimento { … };Um movimento: descrição + valor.
std::vector<Movimento> movimentos_;O histórico, private como o saldo.
std::string descricao = "Depósito"Um valor padrão: depositar(1000.0) registra "Depósito"; depositar(49.9, "Pix da Carla") registra outra descrição. A poupança usa isso para registrar "Rendimento".
movimentos_.push_back({descricao, valor});Acrescenta um Movimento no fim. As chaves montam a struct na hora.
movimentos_.push_back({"Saque", -valor});No saque, o valor vai negativo.
std::string extrato() constMonta o extrato inteiro num texto e devolve. Quem chama decide onde mostrar (tela, arquivo…).
texto += std::format(…)Cada linha é formatada e grudada no fim do texto.
contas[0]->A primeira conta do vetor (a da Ana).

As "máscaras" dentro das chaves dizem como cada valor aparece:

MáscaraSignificadoExemplo de resultado
{}O valor do jeito padrão.Ana
{:<12}Ocupa 12 colunas, alinhado à esquerda (<), completando com espaços.Saque       
{:>10.2f}10 colunas, alinhado à direita (>), número com 2 casas decimais (.2f).   799.90
{:>+10.2f}O mesmo, mas sempre com sinal (+), também nos positivos.  +1000.00
$ g++ -std=c++23 -Wall -Wextra banco.cpp -o banco
$ ./banco
Extrato de Ana (Corrente)
  Depósito      +1000.00
  Saque          -250.00
  Pix da Carla    +49.90
  Saldo           799.90

Extrato de Bruno (Poupança)
  Depósito      +1000.00
  Rendimento       +5.00
  Saldo          1005.00

Os números ficaram alinhados à direita, com duas casas e sinal; e o rendimento da poupança apareceu como um movimento. Repare em "Depósito": tem acento, ocupa 9 bytes em UTF-8 mas 8 colunas na tela, e o std::format do C++23 alinha pelas colunas, por isso a coluna saiu certa (veja Acentos e UTF-8). Com std::setw, do iomanip, que conta bytes, essa linha ficaria um espaço mais curta.

Missão (etapa 6): no fim do main, em vez dos extratos, mostre um resumo com uma linha por conta, usando std::format("{:<6}{:<10}{:>8.2f}\n", …) com o titular, o tipo e o saldo. A saída deve ser:

Ana   Corrente    799.90
Bruno Poupança   1005.00
#include <format>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

struct Movimento {
    std::string descricao;
    double valor;              // positivo entra, negativo sai
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}
    virtual ~Conta() = default;

    virtual std::string tipo() const { return "Corrente"; }
    virtual void fecharMes() {}      // a conta corrente não rende

    void depositar(double valor, std::string descricao = "Depósito") {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
        movimentos_.push_back({descricao, valor});
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
        movimentos_.push_back({"Saque", -valor});
    }

    std::string extrato() const {
        std::string texto = std::format("Extrato de {} ({})\n", titular_, tipo());
        for (const auto& m : movimentos_) {
            texto += std::format("  {:<12}{:>+10.2f}\n", m.descricao, m.valor);
        }
        texto += std::format("  {:<12}{:>10.2f}\n", "Saldo", saldo_);
        return texto;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
    std::vector<Movimento> movimentos_;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa)
        : Conta{titular}, taxa_{taxa} {}

    std::string tipo() const override { return "Poupança"; }

    void fecharMes() override {
        double rendimento = saldo() * taxa_;
        if (rendimento > 0) {
            depositar(rendimento, "Rendimento");
        }
    }

private:
    double taxa_;
};

int main() {
    std::vector<std::unique_ptr<Conta>> contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    contas[0]->depositar(1000.0);
    contas[0]->sacar(250.0);
    contas[0]->depositar(49.9, "Pix da Carla");
    contas[1]->depositar(1000.0);

    for (const auto& conta : contas) {
        conta->fecharMes();
    }
    // Missão: uma linha por conta com
    // std::format("{:<6}{:<10}{:>8.2f}\n", titular, tipo, saldo)
}
#include <format>
#include <iostream>
#include <memory>
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido)
        : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})",
                                         saldo, pedido)} {}
};

struct Movimento {
    std::string descricao;
    double valor;              // positivo entra, negativo sai
};

class Conta {
public:
    explicit Conta(std::string titular) : titular_{titular} {}
    virtual ~Conta() = default;

    virtual std::string tipo() const { return "Corrente"; }
    virtual void fecharMes() {}      // a conta corrente não rende

    void depositar(double valor, std::string descricao = "Depósito") {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        saldo_ += valor;
        movimentos_.push_back({descricao, valor});
    }

    void sacar(double valor) {
        if (valor <= 0) {
            throw std::invalid_argument{"o valor precisa ser positivo"};
        }
        if (valor > saldo_) {
            throw SaldoInsuficiente{saldo_, valor};
        }
        saldo_ -= valor;
        movimentos_.push_back({"Saque", -valor});
    }

    std::string extrato() const {
        std::string texto = std::format("Extrato de {} ({})\n", titular_, tipo());
        for (const auto& m : movimentos_) {
            texto += std::format("  {:<12}{:>+10.2f}\n", m.descricao, m.valor);
        }
        texto += std::format("  {:<12}{:>10.2f}\n", "Saldo", saldo_);
        return texto;
    }

    std::string titular() const { return titular_; }
    double saldo() const { return saldo_; }

private:
    std::string titular_;
    double saldo_ = 0.0;
    std::vector<Movimento> movimentos_;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa)
        : Conta{titular}, taxa_{taxa} {}

    std::string tipo() const override { return "Poupança"; }

    void fecharMes() override {
        double rendimento = saldo() * taxa_;
        if (rendimento > 0) {
            depositar(rendimento, "Rendimento");
        }
    }

private:
    double taxa_;
};

int main() {
    std::vector<std::unique_ptr<Conta>> contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    contas[0]->depositar(1000.0);
    contas[0]->sacar(250.0);
    contas[0]->depositar(49.9, "Pix da Carla");
    contas[1]->depositar(1000.0);

    for (const auto& conta : contas) {
        conta->fecharMes();
    }
    for (const auto& conta : contas) {
        std::cout << std::format("{:<6}{:<10}{:>8.2f}\n",
                                 conta->titular(), conta->tipo(), conta->saldo());
    }
}

Etapa 7 · Separar em conta.h, conta.cpp e main.cpp

O banco.cpp já passou de 90 linhas, e tudo nele está misturado: as classes do banco e o programa que as usa. Programas de verdade separam (veja Arquivos .h e .cpp):

ArquivoO que temComparação
conta.hAs declarações: os nomes das classes, dos métodos e dos dados. O "o quê".O cardápio.
conta.cppAs definições: o corpo de cada método. O "como".A cozinha.
main.cppO programa, que inclui conta.h e usa as contas.O cliente, que só lê o cardápio.
#pragma once
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido);
};

struct Movimento {
    std::string descricao;
    double valor;
};

class Conta {
public:
    explicit Conta(std::string titular);
    virtual ~Conta() = default;

    void depositar(double valor, std::string descricao = "Depósito");
    void sacar(double valor);

    virtual std::string tipo() const;
    virtual void fecharMes();

    std::string titular() const;
    double saldo() const;
    std::string extrato() const;

private:
    std::string titular_;
    double saldo_ = 0.0;
    std::vector<Movimento> movimentos_;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa);

    std::string tipo() const override;
    void fecharMes() override;

private:
    double taxa_;
};
#include "conta.h"
#include <format>

SaldoInsuficiente::SaldoInsuficiente(double saldo, double pedido)
    : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})", saldo, pedido)} {}

Conta::Conta(std::string titular) : titular_{titular} {}

void Conta::depositar(double valor, std::string descricao) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    saldo_ += valor;
    movimentos_.push_back({descricao, valor});
}

void Conta::sacar(double valor) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    if (valor > saldo_) {
        throw SaldoInsuficiente{saldo_, valor};
    }
    saldo_ -= valor;
    movimentos_.push_back({"Saque", -valor});
}

std::string Conta::tipo() const { return "Corrente"; }
void Conta::fecharMes() {}   // a conta corrente não rende
std::string Conta::titular() const { return titular_; }
double Conta::saldo() const { return saldo_; }

std::string Conta::extrato() const {
    std::string texto = std::format("Extrato de {} ({})\n", titular_, tipo());
    for (const auto& m : movimentos_) {
        texto += std::format("  {:<12}{:>+10.2f}\n", m.descricao, m.valor);
    }
    texto += std::format("  {:<12}{:>10.2f}\n", "Saldo", saldo_);
    return texto;
}

ContaPoupanca::ContaPoupanca(std::string titular, double taxa)
    : Conta{titular}, taxa_{taxa} {}

std::string ContaPoupanca::tipo() const { return "Poupança"; }

void ContaPoupanca::fecharMes() {
    double rendimento = saldo() * taxa_;
    if (rendimento > 0) {
        depositar(rendimento, "Rendimento");
    }
}
#include <iostream>
#include <memory>
#include <vector>
#include "conta.h"

int main() {
    std::vector<std::unique_ptr<Conta>> contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    contas[0]->depositar(1000.0);
    contas[0]->sacar(250.0);
    contas[0]->depositar(49.9, "Pix da Carla");
    contas[1]->depositar(1000.0);

    for (const auto& conta : contas) {
        conta->fecharMes();
        std::cout << conta->extrato() << '\n';
    }
}
TrechoO que faz
#pragma once"Inclua este arquivo uma vez só", mesmo que dois arquivos façam #include "conta.h". Sem isso, as classes seriam declaradas duas vezes, e isso é erro (veja o pré-processador).
void depositar(double valor, std::string descricao = "Depósito");No .h, só a "assinatura", terminando em ;. O valor padrão fica só aqui, não se repete no .cpp.
#include "conta.h"Com aspas: um arquivo do projeto, procurado primeiro na pasta atual. Com < >: um da biblioteca padrão.
void Conta::depositar(…) { … }No .cpp, o nome completo: Conta:: diz "o depositar da classe Conta".
override, virtual, explicitAparecem só no .h. No .cpp não se repetem.
#include <format> no conta.cppSó quem usa o std::format precisa dele. O main.cpp nem sabe que ele existe.

Para compilar, passe os dois arquivos .cpp para o g++ (o .h não entra no comando: ele é copiado para dentro dos .cpp pelo #include):

$ ls
conta.cpp
conta.h
main.cpp
$ g++ -std=c++23 -Wall -Wextra main.cpp conta.cpp -o banco
$ ./banco
Extrato de Ana (Corrente)
  Depósito      +1000.00
  Saque          -250.00
  Pix da Carla    +49.90
  Saldo           799.90

Extrato de Bruno (Poupança)
  Depósito      +1000.00
  Rendimento       +5.00
  Saldo          1005.00

A mesma saída da etapa 6: o programa é o mesmo, só mudou de organização. Veja os detalhes do comando em Compilar Vários Arquivos.

Quando dá errado: esquecer o conta.cpp no comando
$ g++ -std=c++23 -Wall -Wextra main.cpp -o banco
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccqdte6y.o: na função "main":
main.cpp:(.text+0x147): referência não definida para "Conta::depositar(double, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)"
/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:(.text+0x184): referência não definida para "Conta::sacar(double)"
/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:(.text+0x1e5): referência não definida para "Conta::depositar(double, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)"
/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:(.text+0x253): referência não definida para "Conta::depositar(double, std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)"
/usr/bin/x86_64-linux-gnu-ld.bfd: main.cpp:(.text+0x2e9): referência não definida para "Conta::extrato[abi:cxx11]() const"
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccqdte6y.o: na função "std::__detail::_MakeUniq<Conta>::__single_object std::make_unique<Conta, char const (&) [4]>(char const (&) [4])":
main.cpp:(.text._ZSt11make_uniqueI5ContaJRA4_KcEENSt8__detail9_MakeUniqIT_E15__single_objectEDpOT0_[_ZSt11make_uniqueI5ContaJRA4_KcEENSt8__detail9_MakeUniqIT_E15__single_objectEDpOT0_]+0x6f): referência não definida para "Conta::Conta(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >)"
/usr/bin/x86_64-linux-gnu-ld.bfd: /tmp/ccqdte6y.o: na função "std::__detail::_MakeUniq<ContaPoupanca>::__single_object std::make_unique<ContaPoupanca, char const (&) [6], double>(char const (&) [6], double&&)":
main.cpp:(.text._ZSt11make_uniqueI13ContaPoupancaJRA6_KcdEENSt8__detail9_MakeUniqIT_E15__single_objectEDpOT0_[_ZSt11make_uniqueI13ContaPoupancaJRA6_KcdEENSt8__detail9_MakeUniqIT_E15__single_objectEDpOT0_]+0x8d): referência não definida para "ContaPoupanca::ContaPoupanca(std::__cxx11::basic_string<char, std::char_traits<char>, std::allocator<char> >, double)"
collect2: error: ld returned 1 exit status

Este erro não é do compilador, é do ligador (linker, o ld), a etapa que junta os pedaços no fim (por isso essas linhas saem em português: o ld do Ubuntu tem tradução, o g++ não). O main.cpp compilou sem problemas, porque o conta.h prometeu que Conta::depositar existe. Na hora de juntar, o ligador procurou o corpo do método e não achou: referência não definida para "Conta::depositar(…)" ("undefined reference", na versão em inglês). O std::__cxx11::basic_string<…> é o nome completo de std::string. O nome do arquivo em /tmp muda a cada vez. Como consertar: ponha conta.cpp no comando. Quando o projeto tiver muitos arquivos, quem lembra disso por você é o Make ou o CMake.

Missão (etapa 7): o main.cpp já chama contas[0]->transferir(*contas[1], 100.0), mas o método não existe, e a compilação falha. Crie-o: declare void transferir(Conta& destino, double valor); no conta.h e escreva o corpo no conta.cpp (saque da própria conta e depósito no destino com a descrição "Transferência"). O *contas[1] é a conta do Bruno: o * pega a conta para a qual o ponteiro aponta. Na saída devem aparecer:

  Saldo           699.90
  Transferência   +100.00
#include <iostream>
#include <memory>
#include <vector>
#include "conta.h"

int main() {
    std::vector<std::unique_ptr<Conta>> contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    contas[0]->depositar(1000.0);
    contas[0]->sacar(250.0);
    contas[0]->depositar(49.9, "Pix da Carla");
    contas[1]->depositar(1000.0);
    contas[0]->transferir(*contas[1], 100.0);   // Ana manda 100 para o Bruno

    for (const auto& conta : contas) {
        std::cout << conta->extrato() << '\n';
    }
}
#pragma once
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido);
};

struct Movimento {
    std::string descricao;
    double valor;
};

class Conta {
public:
    explicit Conta(std::string titular);
    virtual ~Conta() = default;

    void depositar(double valor, std::string descricao = "Depósito");
    void sacar(double valor);

    virtual std::string tipo() const;
    virtual void fecharMes();

    std::string titular() const;
    double saldo() const;
    std::string extrato() const;

private:
    std::string titular_;
    double saldo_ = 0.0;
    std::vector<Movimento> movimentos_;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa);

    std::string tipo() const override;
    void fecharMes() override;

private:
    double taxa_;
};
#include "conta.h"
#include <format>

SaldoInsuficiente::SaldoInsuficiente(double saldo, double pedido)
    : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})", saldo, pedido)} {}

Conta::Conta(std::string titular) : titular_{titular} {}

void Conta::depositar(double valor, std::string descricao) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    saldo_ += valor;
    movimentos_.push_back({descricao, valor});
}

void Conta::sacar(double valor) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    if (valor > saldo_) {
        throw SaldoInsuficiente{saldo_, valor};
    }
    saldo_ -= valor;
    movimentos_.push_back({"Saque", -valor});
}

std::string Conta::tipo() const { return "Corrente"; }
void Conta::fecharMes() {}   // a conta corrente não rende
std::string Conta::titular() const { return titular_; }
double Conta::saldo() const { return saldo_; }

std::string Conta::extrato() const {
    std::string texto = std::format("Extrato de {} ({})\n", titular_, tipo());
    for (const auto& m : movimentos_) {
        texto += std::format("  {:<12}{:>+10.2f}\n", m.descricao, m.valor);
    }
    texto += std::format("  {:<12}{:>10.2f}\n", "Saldo", saldo_);
    return texto;
}

ContaPoupanca::ContaPoupanca(std::string titular, double taxa)
    : Conta{titular}, taxa_{taxa} {}

std::string ContaPoupanca::tipo() const { return "Poupança"; }

void ContaPoupanca::fecharMes() {
    double rendimento = saldo() * taxa_;
    if (rendimento > 0) {
        depositar(rendimento, "Rendimento");
    }
}
#pragma once
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido);
};

struct Movimento {
    std::string descricao;
    double valor;
};

class Conta {
public:
    explicit Conta(std::string titular);
    virtual ~Conta() = default;

    void depositar(double valor, std::string descricao = "Depósito");
    void sacar(double valor);
    void transferir(Conta& destino, double valor);

    virtual std::string tipo() const;
    virtual void fecharMes();

    std::string titular() const;
    double saldo() const;
    std::string extrato() const;

private:
    std::string titular_;
    double saldo_ = 0.0;
    std::vector<Movimento> movimentos_;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa);

    std::string tipo() const override;
    void fecharMes() override;

private:
    double taxa_;
};
#include "conta.h"
#include <format>

SaldoInsuficiente::SaldoInsuficiente(double saldo, double pedido)
    : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})", saldo, pedido)} {}

Conta::Conta(std::string titular) : titular_{titular} {}

void Conta::depositar(double valor, std::string descricao) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    saldo_ += valor;
    movimentos_.push_back({descricao, valor});
}

void Conta::sacar(double valor) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    if (valor > saldo_) {
        throw SaldoInsuficiente{saldo_, valor};
    }
    saldo_ -= valor;
    movimentos_.push_back({"Saque", -valor});
}

void Conta::transferir(Conta& destino, double valor) {
    sacar(valor);                            // se faltar saldo, para aqui
    destino.depositar(valor, "Transferência");
}

std::string Conta::tipo() const { return "Corrente"; }
void Conta::fecharMes() {}   // a conta corrente não rende
std::string Conta::titular() const { return titular_; }
double Conta::saldo() const { return saldo_; }

std::string Conta::extrato() const {
    std::string texto = std::format("Extrato de {} ({})\n", titular_, tipo());
    for (const auto& m : movimentos_) {
        texto += std::format("  {:<12}{:>+10.2f}\n", m.descricao, m.valor);
    }
    texto += std::format("  {:<12}{:>10.2f}\n", "Saldo", saldo_);
    return texto;
}

ContaPoupanca::ContaPoupanca(std::string titular, double taxa)
    : Conta{titular}, taxa_{taxa} {}

std::string ContaPoupanca::tipo() const { return "Poupança"; }

void ContaPoupanca::fecharMes() {
    double rendimento = saldo() * taxa_;
    if (rendimento > 0) {
        depositar(rendimento, "Rendimento");
    }
}

Etapa 8 · O menu

Até aqui, as operações estavam escritas no main. Agora quem decide é o usuário, digitando comandos. O conta.h e o conta.cpp não mudam nada: essa é a vantagem de ter separado. Só o main.cpp é novo:

#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "conta.h"

using Contas = std::vector<std::unique_ptr<Conta>>;

Conta& escolher(Contas& contas, int numero) {
    if (numero < 1 || numero > static_cast<int>(contas.size())) {
        throw std::out_of_range{"não existe a conta " + std::to_string(numero)};
    }
    return *contas[numero - 1];
}

int main() {
    Contas contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    std::cout << "Banco da Ana. Comandos: l, d N VALOR, s N VALOR, e N, m, q\n";
    std::string linha;
    while (std::cout << "banco> " && std::getline(std::cin, linha)) {
        std::istringstream pedaco{linha};
        char comando = ' ';
        int numero = 0;
        double valor = 0.0;
        pedaco >> comando >> numero >> valor;

        try {
            if (comando == 'q') {
                break;
            } else if (comando == 'l') {
                for (std::size_t i = 0; i < contas.size(); ++i) {
                    std::cout << i + 1 << ". " << contas[i]->titular() << " ("
                              << contas[i]->tipo() << ")\n";
                }
            } else if (comando == 'd') {
                escolher(contas, numero).depositar(valor);
                std::cout << "Depósito feito.\n";
            } else if (comando == 's') {
                escolher(contas, numero).sacar(valor);
                std::cout << "Saque feito.\n";
            } else if (comando == 'e') {
                std::cout << escolher(contas, numero).extrato();
            } else if (comando == 'm') {
                for (auto& conta : contas) {
                    conta->fecharMes();
                }
                std::cout << "Mês fechado.\n";
            } else {
                std::cout << "Comando desconhecido: " << linha << '\n';
            }
        } catch (const SaldoInsuficiente& e) {
            std::cout << "Recusado: " << e.what() << '\n';
        } catch (const std::exception& e) {
            std::cout << "Erro: " << e.what() << '\n';
        }
    }
    std::cout << "\nAté logo!\n";
}
TrechoO que faz
using Contas = std::vector<std::unique_ptr<Conta>>;Um apelido para o tipo comprido (using).
Conta& escolher(Contas& contas, int numero)Recebe o número que o usuário digitou (que começa em 1) e devolve a conta (que no vetor começa em 0). Número fora da lista: lança std::out_of_range.
static_cast<int>(contas.size())size() devolve um número sem sinal; converter para int evita comparar um número com sinal e outro sem sinal, que o -Wall avisa: warning: comparison of integer expressions of different signedness. Veja Conversão de Tipos.
while (std::cout << "banco> " && std::getline(std::cin, linha))Mostra o convite e lê uma linha inteira. O laço para quando a entrada acaba (getline devolve "falso").
std::istringstream pedaco{linha};Transforma a linha num "fluxo de entrada", como um cin particular. Daí, pedaco >> comando >> numero >> valor separa d 1 1000 em 'd', 1 e 1000.0. O que faltar fica com o valor inicial (0).
char comando = ' ';Inicializado: numa linha vazia, cai no "Comando desconhecido".
catch (const SaldoInsuficiente& e) antes de catch (const std::exception& e)O mais específico vem primeiro. std::exception é a mãe de todas as exceções da biblioteca (e da nossa), e captura o resto: invalid_argument, out_of_range.
$ g++ -std=c++23 -Wall -Wextra main.cpp conta.cpp -o banco
$ ./banco
Banco da Ana. Comandos: l, d N VALOR, s N VALOR, e N, m, q
banco> l
1. Ana (Corrente)
2. Bruno (Poupança)
banco> d 1 1000
Depósito feito.
banco> d 2 1000
Depósito feito.
banco> s 1 250
Saque feito.
banco> s 2 5000
Recusado: saldo insuficiente (saldo 1000.00, pedido 5000.00)
banco> d 3 10
Erro: não existe a conta 3
banco> d 1 -5
Erro: o valor precisa ser positivo
banco> m
Mês fechado.
banco> e 1
Extrato de Ana (Corrente)
  Depósito      +1000.00
  Saque          -250.00
  Saldo           750.00
banco> e 2
Extrato de Bruno (Poupança)
  Depósito      +1000.00
  Rendimento       +5.00
  Saldo          1005.00
banco> q

Até logo!

Repare que nenhum erro derruba o programa: conta inexistente, valor negativo e saldo insuficiente viram mensagens, e o menu continua. Esse é o papel do try/catch dentro do laço. Para mais formas de tratar o que o usuário digita, veja Validar a Entrada.

Missão (etapa 8): acrescente ao menu o comando v N, que mostra só o saldo da conta N, com duas casas: Saldo de Ana: R$ 750.00. Use escolher e std::format (lembre do #include <format>). A caixa já digita para você: d 1 1000, s 1 250, v 1 e q.

#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "conta.h"

using Contas = std::vector<std::unique_ptr<Conta>>;

Conta& escolher(Contas& contas, int numero) {
    if (numero < 1 || numero > static_cast<int>(contas.size())) {
        throw std::out_of_range{"não existe a conta " + std::to_string(numero)};
    }
    return *contas[numero - 1];
}

int main() {
    Contas contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    std::cout << "Banco da Ana. Comandos: l, d N VALOR, s N VALOR, e N, m, q\n";
    std::string linha;
    while (std::cout << "banco> " && std::getline(std::cin, linha)) {
        std::istringstream pedaco{linha};
        char comando = ' ';
        int numero = 0;
        double valor = 0.0;
        pedaco >> comando >> numero >> valor;

        try {
            if (comando == 'q') {
                break;
            } else if (comando == 'l') {
                for (std::size_t i = 0; i < contas.size(); ++i) {
                    std::cout << i + 1 << ". " << contas[i]->titular() << " ("
                              << contas[i]->tipo() << ")\n";
                }
            } else if (comando == 'd') {
                escolher(contas, numero).depositar(valor);
                std::cout << "Depósito feito.\n";
            } else if (comando == 's') {
                escolher(contas, numero).sacar(valor);
                std::cout << "Saque feito.\n";
            } else if (comando == 'e') {
                std::cout << escolher(contas, numero).extrato();
            } else if (comando == 'm') {
                for (auto& conta : contas) {
                    conta->fecharMes();
                }
                std::cout << "Mês fechado.\n";
            } else {
                std::cout << "Comando desconhecido: " << linha << '\n';
            }
        } catch (const SaldoInsuficiente& e) {
            std::cout << "Recusado: " << e.what() << '\n';
        } catch (const std::exception& e) {
            std::cout << "Erro: " << e.what() << '\n';
        }
    }
    std::cout << "\nAté logo!\n";
}
#pragma once
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido);
};

struct Movimento {
    std::string descricao;
    double valor;
};

class Conta {
public:
    explicit Conta(std::string titular);
    virtual ~Conta() = default;

    void depositar(double valor, std::string descricao = "Depósito");
    void sacar(double valor);

    virtual std::string tipo() const;
    virtual void fecharMes();

    std::string titular() const;
    double saldo() const;
    std::string extrato() const;

private:
    std::string titular_;
    double saldo_ = 0.0;
    std::vector<Movimento> movimentos_;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa);

    std::string tipo() const override;
    void fecharMes() override;

private:
    double taxa_;
};
#include "conta.h"
#include <format>

SaldoInsuficiente::SaldoInsuficiente(double saldo, double pedido)
    : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})", saldo, pedido)} {}

Conta::Conta(std::string titular) : titular_{titular} {}

void Conta::depositar(double valor, std::string descricao) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    saldo_ += valor;
    movimentos_.push_back({descricao, valor});
}

void Conta::sacar(double valor) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    if (valor > saldo_) {
        throw SaldoInsuficiente{saldo_, valor};
    }
    saldo_ -= valor;
    movimentos_.push_back({"Saque", -valor});
}

std::string Conta::tipo() const { return "Corrente"; }
void Conta::fecharMes() {}   // a conta corrente não rende
std::string Conta::titular() const { return titular_; }
double Conta::saldo() const { return saldo_; }

std::string Conta::extrato() const {
    std::string texto = std::format("Extrato de {} ({})\n", titular_, tipo());
    for (const auto& m : movimentos_) {
        texto += std::format("  {:<12}{:>+10.2f}\n", m.descricao, m.valor);
    }
    texto += std::format("  {:<12}{:>10.2f}\n", "Saldo", saldo_);
    return texto;
}

ContaPoupanca::ContaPoupanca(std::string titular, double taxa)
    : Conta{titular}, taxa_{taxa} {}

std::string ContaPoupanca::tipo() const { return "Poupança"; }

void ContaPoupanca::fecharMes() {
    double rendimento = saldo() * taxa_;
    if (rendimento > 0) {
        depositar(rendimento, "Rendimento");
    }
}
#include <format>
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "conta.h"

using Contas = std::vector<std::unique_ptr<Conta>>;

Conta& escolher(Contas& contas, int numero) {
    if (numero < 1 || numero > static_cast<int>(contas.size())) {
        throw std::out_of_range{"não existe a conta " + std::to_string(numero)};
    }
    return *contas[numero - 1];
}

int main() {
    Contas contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    std::cout << "Banco da Ana. Comandos: l, d N VALOR, s N VALOR, e N, m, q\n";
    std::string linha;
    while (std::cout << "banco> " && std::getline(std::cin, linha)) {
        std::istringstream pedaco{linha};
        char comando = ' ';
        int numero = 0;
        double valor = 0.0;
        pedaco >> comando >> numero >> valor;

        try {
            if (comando == 'q') {
                break;
            } else if (comando == 'l') {
                for (std::size_t i = 0; i < contas.size(); ++i) {
                    std::cout << i + 1 << ". " << contas[i]->titular() << " ("
                              << contas[i]->tipo() << ")\n";
                }
            } else if (comando == 'd') {
                escolher(contas, numero).depositar(valor);
                std::cout << "Depósito feito.\n";
            } else if (comando == 's') {
                escolher(contas, numero).sacar(valor);
                std::cout << "Saque feito.\n";
            } else if (comando == 'v') {
                Conta& conta = escolher(contas, numero);
                std::cout << std::format("Saldo de {}: R$ {:.2f}\n", conta.titular(), conta.saldo());
            } else if (comando == 'e') {
                std::cout << escolher(contas, numero).extrato();
            } else if (comando == 'm') {
                for (auto& conta : contas) {
                    conta->fecharMes();
                }
                std::cout << "Mês fechado.\n";
            } else {
                std::cout << "Comando desconhecido: " << linha << '\n';
            }
        } catch (const SaldoInsuficiente& e) {
            std::cout << "Recusado: " << e.what() << '\n';
        } catch (const std::exception& e) {
            std::cout << "Erro: " << e.what() << '\n';
        }
    }
    std::cout << "\nAté logo!\n";
}

O código completo

Os três arquivos finais, prontos para copiar. Compile com g++ -std=c++23 -Wall -Wextra main.cpp conta.cpp -o banco.

#pragma once
#include <stdexcept>
#include <string>
#include <vector>

class SaldoInsuficiente : public std::runtime_error {
public:
    SaldoInsuficiente(double saldo, double pedido);
};

struct Movimento {
    std::string descricao;
    double valor;
};

class Conta {
public:
    explicit Conta(std::string titular);
    virtual ~Conta() = default;

    void depositar(double valor, std::string descricao = "Depósito");
    void sacar(double valor);

    virtual std::string tipo() const;
    virtual void fecharMes();

    std::string titular() const;
    double saldo() const;
    std::string extrato() const;

private:
    std::string titular_;
    double saldo_ = 0.0;
    std::vector<Movimento> movimentos_;
};

class ContaPoupanca : public Conta {
public:
    ContaPoupanca(std::string titular, double taxa);

    std::string tipo() const override;
    void fecharMes() override;

private:
    double taxa_;
};
#include "conta.h"
#include <format>

SaldoInsuficiente::SaldoInsuficiente(double saldo, double pedido)
    : std::runtime_error{std::format("saldo insuficiente (saldo {:.2f}, pedido {:.2f})", saldo, pedido)} {}

Conta::Conta(std::string titular) : titular_{titular} {}

void Conta::depositar(double valor, std::string descricao) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    saldo_ += valor;
    movimentos_.push_back({descricao, valor});
}

void Conta::sacar(double valor) {
    if (valor <= 0) {
        throw std::invalid_argument{"o valor precisa ser positivo"};
    }
    if (valor > saldo_) {
        throw SaldoInsuficiente{saldo_, valor};
    }
    saldo_ -= valor;
    movimentos_.push_back({"Saque", -valor});
}

std::string Conta::tipo() const { return "Corrente"; }
void Conta::fecharMes() {}   // a conta corrente não rende
std::string Conta::titular() const { return titular_; }
double Conta::saldo() const { return saldo_; }

std::string Conta::extrato() const {
    std::string texto = std::format("Extrato de {} ({})\n", titular_, tipo());
    for (const auto& m : movimentos_) {
        texto += std::format("  {:<12}{:>+10.2f}\n", m.descricao, m.valor);
    }
    texto += std::format("  {:<12}{:>10.2f}\n", "Saldo", saldo_);
    return texto;
}

ContaPoupanca::ContaPoupanca(std::string titular, double taxa)
    : Conta{titular}, taxa_{taxa} {}

std::string ContaPoupanca::tipo() const { return "Poupança"; }

void ContaPoupanca::fecharMes() {
    double rendimento = saldo() * taxa_;
    if (rendimento > 0) {
        depositar(rendimento, "Rendimento");
    }
}
#include <iostream>
#include <memory>
#include <sstream>
#include <string>
#include <vector>
#include "conta.h"

using Contas = std::vector<std::unique_ptr<Conta>>;

Conta& escolher(Contas& contas, int numero) {
    if (numero < 1 || numero > static_cast<int>(contas.size())) {
        throw std::out_of_range{"não existe a conta " + std::to_string(numero)};
    }
    return *contas[numero - 1];
}

int main() {
    Contas contas;
    contas.push_back(std::make_unique<Conta>("Ana"));
    contas.push_back(std::make_unique<ContaPoupanca>("Bruno", 0.005));

    std::cout << "Banco da Ana. Comandos: l, d N VALOR, s N VALOR, e N, m, q\n";
    std::string linha;
    while (std::cout << "banco> " && std::getline(std::cin, linha)) {
        std::istringstream pedaco{linha};
        char comando = ' ';
        int numero = 0;
        double valor = 0.0;
        pedaco >> comando >> numero >> valor;

        try {
            if (comando == 'q') {
                break;
            } else if (comando == 'l') {
                for (std::size_t i = 0; i < contas.size(); ++i) {
                    std::cout << i + 1 << ". " << contas[i]->titular() << " ("
                              << contas[i]->tipo() << ")\n";
                }
            } else if (comando == 'd') {
                escolher(contas, numero).depositar(valor);
                std::cout << "Depósito feito.\n";
            } else if (comando == 's') {
                escolher(contas, numero).sacar(valor);
                std::cout << "Saque feito.\n";
            } else if (comando == 'e') {
                std::cout << escolher(contas, numero).extrato();
            } else if (comando == 'm') {
                for (auto& conta : contas) {
                    conta->fecharMes();
                }
                std::cout << "Mês fechado.\n";
            } else {
                std::cout << "Comando desconhecido: " << linha << '\n';
            }
        } catch (const SaldoInsuficiente& e) {
            std::cout << "Recusado: " << e.what() << '\n';
        } catch (const std::exception& e) {
            std::cout << "Erro: " << e.what() << '\n';
        }
    }
    std::cout << "\nAté logo!\n";
}

Desafios extras

Sem solução pronta: use o editor da etapa 8, que tem o banco completo, ou o seu computador.

  1. Centavos inteiros. Troque o double saldo_ por long long centavos_ (R$ 10,50 = 1050). No extrato, mostre com std::format("{}.{:02}", c / 100, c % 100). Cuidado com os valores negativos.
  2. Vírgula brasileira. Faça o extrato mostrar 1.000,00 em vez de 1000.00. Dica: formate e depois troque os caracteres no texto (Funções de String).
  3. Transferência no menu. Com o transferir da missão 7, crie o comando t ORIGEM DESTINO VALOR. Se faltar saldo, nenhuma das duas contas pode mudar.
  4. Cheque especial. Crie ContaEspecial, que pode ficar negativa até um limite. Dica: a mãe precisa de um método virtual para perguntar "quanto posso sacar?".
  5. Salvar as contas. No q, grave titular, tipo e saldo num arquivo contas.csv; ao abrir, leia de volta (Ler e Escrever Arquivos). O Controle de Estoque faz isso com um CSV.
  6. Classe abstrata. Faça tipo() virar virtual std::string tipo() const = 0; e crie ContaCorrente. O que acontece com std::make_unique<Conta>("Ana")? (Classes Abstratas)

Resumo

  • Dados private + métodos public: o saldo só muda por depositar e sacar, e o compilador barra ana.saldo_ = ….
  • Validar com bool funciona, mas a resposta pode ser ignorada ([[nodiscard]] só avisa). Uma exceção não passa despercebida: sem catch, o programa termina com terminate called after throwing….
  • Exceção própria: class SaldoInsuficiente : public std::runtime_error, com a mensagem montada no construtor e lida com what(). Capture com catch (const Tipo& e), do mais específico para o mais geral.
  • Herança: class ContaPoupanca : public Conta. Na mãe, virtual (e destrutor virtual); na filha, override. Sem virtual, a função errada é chamada em silêncio.
  • Contas de tipos diferentes num vetor: std::vector<std::unique_ptr<Conta>> com std::make_unique. Um vector<Conta> fatia a poupança; unique_ptr não se copia, então percorra com auto&.
  • std::format("{:<12}{:>+10.2f}", …) alinha colunas, fixa as casas decimais e mostra o sinal.
  • .h com as declarações e #pragma once, .cpp com os corpos (Conta::metodo); compile todos os .cpp juntos, senão vem referência não definida do ligador.