Técnicas Avançadas em C++: Manipulação de Arquivos, Depuração, Asserções e Busca Binária na Resposta

Redirecionamento de Fluxos Padrão

O redirecionamento de entrada e saída permite que programas leiam de ou escrevam para arquivos em vez do console padrão. Essa técnica é fundamental para testes automatizados, maratonas de programação e processamento de grandes volumes de dados.

Redirecionamento via Terimnal

A forma mais simples de redirecionar fluxos é através do sistema operacional, sem alterar o código-fonte:

# Ler de entrada.txt e exibir no terminal
./meu_app < entrada.txt

# Salvar a saída do programa em log.txt
./meu_app > log.txt

# Adicionar saída ao final do arquivo
./meu_app >> log.txt

# Combinar entrada e saída
./meu_app < entrada.txt > log.txt

# Capturar erros separadamente
./meu_app 2> erros.log

Uso de freopen em Competições

Em ambientes de competição, é comum usar diretivas de pré-processador para alternar entre a leitura de arquivos locais e a entrada padrão.

#include <iostream>
#include <cstdio>

int main() {
    #ifdef JUDGE_LOCAL
        freopen("dados.txt", "r", stdin);
        freopen("resultado.txt", "w", stdout);
    #endif

    int x, y;
    std::cin >> x >> y;
    std::cout << (x + y) << '\n';
    return 0;
}
// Compilação local: g++ -DJUDGE_LOCAL app.cpp -o app
// Compilação oficial: g++ app.cpp -o app

Manipulação com File Streams

Para um controle mais refinado, a biblioteca padrão C++ oferece std::ifstream e std::ofstream. O exemplo a seguir processa um arquivo de funcionários, calculando bônus e ordenando por salário.

#include <iostream>
#include <fstream>
#include <vector>
#include <algorithm>
#include <iomanip>

struct Funcionario {
    std::string nome;
    double salario;
};

int main() {
    std::ifstream arquivoEntrada("funcionarios.txt");
    if (!arquivoEntrada.is_open()) {
        std::cerr << "Falha ao abrir arquivo de entrada.\n";
        return 1;
    }

    int total;
    arquivoEntrada >> total;
    std::vector<Funcionario> equipe(total);

    for (int i = 0; i < total; ++i) {
        arquivoEntrada >> equipe[i].nome >> equipe[i].salario;
    }
    arquivoEntrada.close();

    std::sort(equipe.begin(), equipe.end(), [](const Funcionario& a, const Funcionario& b) {
        return a.salario > b.salario;
    });

    std::ofstream relatorio("relatorio.txt");
    relatorio << std::fixed << std::setprecision(2);
    for (const auto& func : equipe) {
        relatorio << func.nome << " - " << (func.salario * 1.1) << '\n';
    }
    relatorio.close();

    return 0;
}

Estratégias de Depuração

Depuração eficiente separa desenvolvedores intermediários de engenheiros seniores. O domínio de técnicas de rastreamento e análise de execução economiza horas de desenvolvimento.

Macros de Rastreamento

Inserir mensagens de log em pontos críticos do código ajuda a entender o fluxo de execução e o estado das variáveis. Usar macros permite desativar esses logs na versão final.

#include <iostream>
#include <vector>

#ifdef DBG_MODE
    #define TRACE(x) std::cerr << "[LOG] " << #x << " = " << (x) << '\n'
    #define TRACE_VEC(v) do { \
        std::cerr << "[LOG] " << #v << " = [ "; \
        for (const auto& item : v) std::cerr << item << ' '; \
        std::cerr << "]\n"; \
    } while(0)
#else
    #define TRACE(x)
    #define TRACE_VEC(v)
#endif

int main() {
    std::vector<int> dados = {42, 17, 89, 5};
    TRACE_VEC(dados);

    int alvo = 89;
    TRACE(alvo);

    return 0;
}

Análise de Desempenho com RAII

Para identificar gargalos, um cronômetro baseado no padrão RAII mede o tempo de vida de um bloco de código automaticamente.

#include <chrono>
#include <iostream>
#include <vector>
#include <algorithm>

class Cronometro {
    std::chrono::high_resolution_clock::time_point inicio;
    std::string etiqueta;
public:
    Cronometro(std::string tag) : etiqueta(tag), inicio(std::chrono::high_resolution_clock::now()) {}
    ~Cronometro() {
        auto fim = std::chrono::high_resolution_clock::now();
        auto duracao = std::chrono::duration_cast<std::chrono::milliseconds>(fim - inicio).count();
        std::cerr << "[PERF] " << etiqueta << " levou " << duracao << " ms\n";
    }
};

int main() {
    std::vector<int> dados(10000);
    for (int i = 0; i < 10000; ++i) dados[i] = 10000 - i;

    {
        Cronometro t("Ordenacao Standard");
        std::sort(dados.begin(), dados.end());
    }
    return 0;
}

Depurador GDB

O GNU Debugger (GDB) permite inspecionar o estado da memória e o fluxo de execução. Para utilizá-lo, compile com a flag -g.

Comando Abreviação Objetivo
run r Inicia a execução
break b Define um ponto de parada
next n Avança sem entrar em funções
step s Avança entrando em funções
print p Exibe valor de variável
backtrace bt Mostra pilha de chamadas

Testes de Estresse

Quando uma solução complexa falha em casos específicos, gerar dados aleatórios e comparar com uma solução trivial (força bruta) revela falhas lógicas.

#include <iostream>
#include <vector>
#include <algorithm>
#include <random>

std::vector<int> forcaBruta(std::vector<int> v) {
    std::sort(v.begin(), v.end());
    return v;
}

bool validar(std::vector<int> original, const std::vector<int>& ordenado) {
    auto brute = forcaBruta(original);
    return brute == ordenado;
}

int main() {
    std::mt19937 rng(42);
    for (int i = 0; i < 100; ++i) {
        std::vector<int> dados(10);
        for (int& x : dados) x = rng() % 100;

        auto esperado = forcaBruta(dados);
        // Substitua 'esperado' pela chamada de sua função customizada
        if (!validar(dados, esperado)) {
            std::cerr << "Falha detectada no caso " << i << '\n';
            return 1;
        }
    }
    return 0;
}

Validação com Asserções

Asserções (assert) validam premissas lógicas durante o desenvolvimento. Elas abortam o programa se uma condição for falsa, evitando que erros silenciosos se propaguem.

Tipos de Asserções

  • Pré-condição: Valida parâmetros de entrada (ex: divisor não zero).
  • Pós-condição: Garante que o retorno da função está correto.
  • Invariante: Assegura que um estado permanece consistente em loops.
#include <iostream>
#include <cassert>
#include <vector>

class PilhaLimitada {
    int* dados;
    int topo;
    int capacidade;
public:
    PilhaLimitada(int cap) : capacidade(cap), topo(-1) {
        assert(cap > 0 && "Capacidade deve ser positiva");
        dados = new int[cap];
    }
    ~PilhaLimitada() { delete[] dados; }

    void empilhar(int valor) {
        assert(topo + 1 < capacidade && "Pilha cheia");
        dados[++topo] = valor;
    }

    int desempilhar() {
        assert(topo >= 0 && "Pilha vazia");
        return dados[topo--];
    }
};

Asserções vs Exceções

Asserções tratam erros de lógica do programador e geralmente são desativadas em produção (#define NDEBUG). Exceções lidam com falhas externas inevitáveis (ex: arquivo não encontrado, falta de memória) e permitem recuperação em tempo de execução.

#include <iostream>
#include <cassert>
#include <stdexcept>

class CarteiraDigital {
    double saldo;
public:
    CarteiraDigital(double inicial) : saldo(inicial) {
        assert(inicial >= 0 && "Saldo inicial não pode ser negativo");
    }

    void debitar(double valor) {
        assert(valor > 0 && "Valor de débito deve ser positivo");
        if (valor > saldo) {
            throw std::runtime_error("Saldo insuficiente");
        }
        saldo -= valor;
        assert(saldo >= 0);
    }
};

Busca Binária na Resposta

Quando o espaço de soluções é monotônico, podemos aplicar a busca binária não apenas em um array, mas no próprio resultado do problema. O objetivo é encontrar o valor ótimo que satisfaz uma condição de viabilidade.

Estrutura Base

int buscarSolucaoOtima(int limiteInferior, int limiteSuperior) {
    int resposta = -1;
    while (limiteInferior <= limiteSuperior) {
        int meio = limiteInferior + (limiteSuperior - limiteInferior) / 2;
        if (verificarViabilidade(meio)) {
            resposta = meio;
            limiteInferior = meio + 1; // Ajuste conforme maximização ou minimização
        } else {
            limiteSuperior = meio - 1;
        }
    }
    return resposta;
}

Aplicação: Divisão Ótima de Tarefas

Problema: Dado um conjunto de tarefas com tempos diferentes, atribua-as a K trabalhadores de forma contígua, minimizando o tempo máximo que um trabalhador levará.

#include <iostream>
#include <vector>
#include <algorithm>

bool podeAlocar(const std::vector<int>& tempoTarefas, int K, int limiteCarga) {
    int trabalhadoresNecessarios = 1;
    int cargaAtual = 0;

    for (int tempo : tempoTarefas) {
        if (tempo > limiteCarga) return false;
        if (cargaAtual + tempo > limiteCarga) {
            trabalhadoresNecessarios++;
            cargaAtual = tempo;
        } else {
            cargaAtual += tempo;
        }
    }
    return trabalhadoresNecessarios <= K;
}

int main() {
    std::vector<int> tempos = {10, 20, 30, 40, 50};
    int K = 3;

    int limiteInf = *std::max_element(tempos.begin(), tempos.end());
    int limiteSup = 0;
    for (int t : tempos) limiteSup += t;

    int melhorCarga = limiteSup;
    while (limiteInf <= limiteSup) {
        int meio = limiteInf + (limiteSup - limiteInf) / 2;
        if (podeAlocar(tempos, K, meio)) {
            melhorCarga = meio;
            limiteSup = meio - 1; // Busca uma carga menor
        } else {
            limiteInf = meio + 1;
        }
    }

    std::cout << "Carga máxima mínima: " << melhorCarga << '\n';
    return 0;
}

Busca em Ponto Flutuante

Para problemas que exigem precisão decimal, a condição de parada se baseia na diferença entre os limites (EPS).

double buscarPrecisao(double inf, double sup) {
    const double EPS = 1e-7;
    while (sup - inf > EPS) {
        double meio = (inf + sup) / 2.0;
        if (verificarCondicao(meio)) {
            inf = meio;
        } else {
            sup = meio;
        }
    }
    return inf;
}

Tags: C++ Iostream gdb Cassert BuscaBinaria

Publicado em 6-25 03:44