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;
}