Organizar e depurar programas
Cabeçalhos, definições, ligação, CMake, CLion e um procedimento de depuração.
Uma declaração de dobro permite compilar a chamada sem mostrar o seu corpo. A definição pode ficar noutro ficheiro, mas precisa de entrar na ligação para formar o executável.
Esta página segue esses dois percursos separados até se juntarem.
Cabeçalhos e implementação
Para separar uma função que duplica um valor real, cria estes três ficheiros na mesma pasta.
dobro.hpp declara a interface. A guarda impede repetir o conteúdo do cabeçalho na mesma unidade de tradução.
#ifndef DOBRO_HPP
#define DOBRO_HPP
double dobro(double valor);
#endif
dobro.cpp define a operação e inclui o próprio cabeçalho, por isso o compilador deteta muitas divergências entre declaração e definição.
Não garante tudo: se o cabeçalho declara double dobro(double); e o ficheiro define double dobro(int v), estás a declarar outra sobrecarga e a definição esperada continua em falta, por isso a ligação falha. Mantém declaração e definição com a mesma assinatura.
#include "dobro.hpp"
double dobro(double valor) {
return 2.0 * valor;
}
main.cpp usa a interface sem reproduzir a implementação:
#include "dobro.hpp"
#include <iostream>
int main() {
std::cout << dobro(4.0) << "\n";
}
Este programa escreve 8. A saída continua igual se reorganizares a implementação sem alterar o contrato.
Compilação e ligação
Cada .cpp, depois do pré-processamento dos seus #include, forma uma unidade de tradução. A compilação produz ficheiros objeto. A ligação reúne-os e resolve as referências entre eles.
Neste exemplo há duas unidades, dobro.cpp com a definição e main.cpp com a chamada. Cada uma compila sozinha. Só a ligação as junta.
g++ -std=c++17 -Wall -Wextra -Wpedantic -c dobro.cpp -o dobro.o
g++ -std=c++17 -Wall -Wextra -Wpedantic -c main.cpp -o main.o
g++ main.o dobro.o -o exemplo
./exemplo
O cabeçalho entra nos dois percursos, mas não produz sozinho a definição compilada de dobro, por isso se faltar o ramo de dobro.o a ligação fica com uma chamada por resolver.
Também podes compilar e ligar de uma vez com g++ -std=c++17 main.cpp dobro.cpp -o exemplo. Compilar só main.cpp encontra a declaração, mas a ligação falha por faltar a definição de dobro. O erro típico é undefined reference to 'dobro(double)': a compilação passou, a ligação não encontrou o código.
Uma definição não inline de função colocada num cabeçalho incluído por vários .cpp pode criar múltiplas definições na ligação. Coloca a implementação num .cpp, ou usa a regra adequada para funções inline e templates. Não incluas ficheiros .cpp para resolver um erro de ligação; acrescenta-os ao alvo de compilação.
CMake para vários ficheiros
Guarda este CMakeLists.txt junto das fontes:
cmake_minimum_required(VERSION 3.16)
project(exemplo LANGUAGES CXX)
add_executable(exemplo main.cpp dobro.cpp)
target_compile_features(exemplo PRIVATE cxx_std_17)
set_target_properties(exemplo PROPERTIES CXX_EXTENSIONS OFF)
Na pasta do projeto, corre:
cmake -S . -B build
cmake --build build
./build/exemplo
-S escolhe a pasta das fontes e -B a pasta dos ficheiros de build. Não edites os ficheiros gerados dentro de build. A localização do executável pode variar num gerador com várias configurações, como Debug e Release. No CLion, abre a pasta com CMakeLists.txt, confirma a toolchain e escolhe o alvo exemplo.
Localizar a fase do erro
| Sintoma | Fase provável | O que conferir |
|---|---|---|
| Tipo ou nome desconhecido | Compilação | Declaração, include, namespace e assinatura |
| Referência não definida | Ligação | .cpp no alvo, definição e tipos iguais |
| Símbolo definido várias vezes | Ligação | Definições repetidas e conteúdo de cabeçalhos |
| Resultado errado | Execução | Entrada, contrato, estado e caminho seguido |
| Falha de memória | Execução | Limites, duração de vida e propriedade |
Lê a primeira mensagem relevante, porque um erro de sintaxe pode causar muitas mensagens seguintes. Um erro de ligação não se resolve na entrada do programa, pois a execução ainda não começou.
Usar o depurador
Compila com -g -O0 ou seleciona Debug no CLion. Para o erro de remoção com dois negativos consecutivos, coloca um breakpoint na linha do erase e corre com {-1, -2}, observando o iterador devolvido, que aponta para -2, ainda por analisar; se o código avançar outra vez sem ler esse elemento, -2 sobrevive por engano. Compara argumentos e variáveis com o que o contrato exige.
Step into entra na função chamada; step over executa-a sem abrir o seu corpo; step out termina a chamada atual. A pilha de chamadas mostra como chegaste à linha. Ao seguir apontadores, distingue o valor do endereço do conteúdo apontado e confirma que o objeto ainda vive.
Um procedimento para corrigir erros
Primeiro, reduz o problema a uma entrada pequena com saída conhecida; depois identifica a primeira operação em que o estado diverge do esperado. Corrige essa operação, volta a correr a mesma entrada e acrescenta um caso que detete a causa, e só então testa fronteiras próximas.
Se uma função remove negativos mas perde também o elemento seguinte, segue dois negativos consecutivos, pois esse caso revela se o iterador é avançado indevidamente depois de erase; dez testes com positivos não esclarecem esse erro. Usa avisos, sanitizers e testes em conjunto, pois cada um procura falhas diferentes.
Exercícios
A declaração não é a implementação
main.cpp inclui dobro.hpp e chama dobro.
dobro.cpp contém a definição, mas o alvo só compila
main.cpp. A compilação de main.cpp resulta
e a ligação falha. Qual é a correção?
Pista
O compilador conhece a assinatura, mas o executável continua sem o código.
Solução
Acrescenta dobro.cpp ao alvo, por exemplo
add_executable(exemplo main.cpp dobro.cpp). O
include traz a declaração para a unidade de tradução; não
liga automaticamente o outro .cpp.
Confere a tua resposta:
- Localizei a falha na ligação.
- Acrescentei dobro.cpp ao alvo.
- Distingui include da ligação dos ficheiros objeto.
Erros frequentes
Não incluas dobro.cpp nem copies a definição para todos
os cabeçalhos para contornar a ligação.
Reduzir um erro de remoção
Uma função que remove negativos falha quando há dois consecutivos. Indica uma entrada mínima, o resultado esperado e o estado a observar no depurador.
Pista
Conserva só o padrão que desencadeia o erro.
Solução
Uma entrada mínima é {-1, -2} e o resultado
esperado é o vetor vazio. Observa o iterador devolvido por erase e
verifica se o código o incrementa novamente antes de analisar o segundo
negativo. Um caso como {1, -1, -2, 3} ajuda depois
a confirmar que os restantes valores se conservam.
Confere a tua resposta:
- Escolhi dois negativos consecutivos.
- Derivei o resultado vazio do contrato.
- Indiquei o iterador devolvido por erase como estado relevante.
Erros frequentes
Testar apenas negativos separados por positivos pode esconder o avanço indevido.
Incluir o cabeçalho não define a função
O cabeçalho declara double dobro(double), mas
dobro.cpp define double dobro(int).
main.cpp inclui o cabeçalho e chama dobro(4.0). A
compilação passa e a ligação falha. Porquê? Como corrigir?
Pista
Os tipos dos parâmetros fazem parte da identidade da função.
Solução
A definição com int é outra sobrecarga e a versão com
double continua sem definição. A ligação responde
undefined reference to dobro(double). Corrige a assinatura em
dobro.cpp e confirma que ambos os ficheiros participam no
alvo. Não incluas o .cpp para contornar a ligação.
Confere a tua resposta:
- Expliquei por que o include não liga o .cpp.
- Identifiquei a sobrecarga como outra função.
- Corrigi o tipo do parâmetro na definição e confirmei o alvo.
Erros frequentes
Não copies a definição para o cabeçalho sem inline. Cria múltiplas definições na ligação se vários ficheiros o incluírem.