C++ O Pré-processador

Desde o primeiro programa você escreve #include <iostream> no topo do arquivo. Essa linha começa com # e não termina com ;, e isso tem um motivo: ela não é C++. É uma ordem para um programa que trabalha antes do compilador, o pré-processador. Nesta aula você vai ver o que ele faz com o seu arquivo, aprender as ordens que ele entende (#include, #define, #ifdef…) e conhecer as armadilhas que fizeram os programadores de C++ moderno usarem o pré-processador o mínimo possível.

Você vai aprender
  • O que é o pré-processador e em que momento ele roda.
  • A diferença entre #include <iostream> e #include "meu.h".
  • Como criar constantes e macros com #define, e por que DOBRO(1+2) pode dar 5.
  • Como ligar e desligar pedaços do código com #ifdef, #ifndef e #if.
  • Como definir um nome na hora de compilar, com g++ -D.
  • Como ver o arquivo que o compilador recebe de verdade, com g++ -E.
  • Para que servem __FILE__, __LINE__ e #pragma once.

Todos os programas desta aula foram compilados com o g++ 15.2 na máquina fictícia da Ana (usuária ana, computador notebook), na pasta ~/cpp/prepro, com o comando padrão do guia: g++ -std=c++23 -Wall -Wextra arquivo.cpp -o programa (veja Compilar com g++). Nas caixas "Experimente", o editor do guia compila de verdade, com o Clang.


A ideia: o assistente que prepara a receita

Imagine uma cozinheira (o compilador) que só sabe seguir receitas escritas por inteiro. Antes de a receita chegar até ela, um assistente passa pelo papel e faz trocas mecânicas:

  • onde está escrito "cole aqui a receita do molho", ele cola o texto inteiro da receita do molho;
  • onde está escrito "XÍCARA", ele troca por "240 ml", porque alguém avisou lá em cima que XÍCARA quer dizer 240 ml;
  • os trechos marcados "só se for para festa" ele recorta fora, se não for festa.

O assistente não sabe cozinhar. Ele não entende o que está escrito; ele só cola, troca e recorta texto. Esse assistente é o pré-processador. As ordens para ele são as linhas que começam com #, chamadas diretivas (do inglês directive, "instrução").

EtapaQuem fazO que recebeO que entrega
1. Pré-processamentoo pré-processadoro seu .cpp, com as linhas #um texto C++ sem nenhuma linha # (as diretivas já foram cumpridas)
2. Compilaçãoo compiladoresse textocódigo de máquina (um arquivo .o)
3. Ligaçãoo ligador (linker)os .o e as bibliotecaso programa pronto

Quando você digita g++ arquivo.cpp -o programa, as três etapas rodam em sequência, sem você ver. A aula Como o C++ Vira Programa mostra o caminho todo; aqui vamos olhar só a primeira etapa.


1. A regra de ouro: só troca de texto

Comece pelo exemplo mais famoso. A Ana quer uma "função" que dobra um número e escreve, usando o pré-processador:

#include <iostream>

#define DOBRO(x) x*2

int main() {
    std::cout << "DOBRO(4) = " << DOBRO(4) << '\n';
    std::cout << "DOBRO(1+2) = " << DOBRO(1+2) << '\n';
}
#define
"Defina": cria um nome que o pré-processador vai trocar por outro texto. Um nome assim se chama macro.
DOBRO(x)
O nome da macro, com um parâmetro x. Por costume, macros são escritas EM MAIÚSCULAS, para chamar a atenção.
x*2
O texto que vai entrar no lugar de DOBRO(…), com o x trocado pelo que estiver entre os parênteses.
(sem ;)
Diretivas não terminam com ponto e vírgula. A linha acaba e a diretiva acaba junto.
$ g++ -std=c++23 -Wall -Wextra dobro.cpp -o dobro
$ ./dobro
DOBRO(4) = 8
DOBRO(1+2) = 5          « 1+2 é 3, e o dobro de 3 é 6, não 5!

Nenhum erro, nenhum aviso, e uma resposta errada. Para entender, peça ao g++ para fazer só o pré-processamento e mostrar o resultado. A opção é -E. Num arquivo pequeno, sem #include, dá para ler tudo:

#define PRECO 10
#define DOBRO(x) x*2

int main() {
    int a = PRECO;
    int b = DOBRO(1+2);
    return a + b;
}
$ g++ -E mini.cpp
# 0 "mini.cpp"
# 0 "<built-in>"
# 0 "<command-line>"
# 1 "/usr/include/stdc-predef.h" 1 3 4
# 0 "<command-line>" 2
# 1 "mini.cpp"



int main() {
    int a = 10;
    int b = 1+2*2;          « o texto "1+2" entrou no lugar do x
    return a + b;
}

Aí está o problema. O pré-processador não calculou nada: trocou x pelo texto 1+2 e entregou 1+2*2 ao compilador. Como a multiplicação vem antes da soma (veja Precedência), o resultado é 1 + 4 = 5.

-E
"Pare depois do pré-processamento" (E de expand, expandir). O resultado sai na tela, ou num arquivo com -o. Nada é compilado.
linhas # 1 "mini.cpp"
Marcas que o pré-processador deixa para o compilador saber de que arquivo e de que linha veio cada pedaço. É assim que as mensagens de erro conseguem dizer mini.cpp:6.
linhas vazias
Onde estavam os #define. A diretiva foi cumprida e sumiu.

Para ver só o código, sem as marcas, acrescente -P:

$ g++ -E -P mini.cpp
int main() {
    int a = 10;
    int b = 1+2*2;
    return a + b;
}

O conserto: parênteses em tudo

Quem escreve macros aprende a cercar cada parâmetro e a macro inteira de parênteses:

#define DOBRO(x) ((x) * 2)

Agora DOBRO(1+2) vira ((1+2) * 2), que dá 6. Os parênteses de fora protegem contra outro tropeço: com #define DOBRO(x) (x)*2, a conta 100 / DOBRO(5) vira 100 / (5)*2, que o C++ faz da esquerda para a direita: 20 * 2 = 40, e não 100 / 10 = 10.

Em C++ moderno, prefira uma função

Uma função de verdade recebe o valor já calculado, não o texto, e não tem esse problema:

constexpr int dobro(int x) { return x * 2; }   // dobro(1+2) dá 6

Ela é tão rápida quanto a macro, o compilador confere os tipos e ela aparece com o nome certo no depurador. Você vai ver funções em Funções e o constexpr em constexpr e consteval.

Experimente: rode e veja o 5 errado. Depois troque a definição por #define DOBRO(x) ((x) * 2) e rode de novo. Para comparar, a função dobro já está pronta embaixo.

#include <iostream>

#define DOBRO(x) x*2

constexpr int dobro(int x) { return x * 2; }

int main() {
    std::cout << "macro:  DOBRO(1+2) = " << DOBRO(1+2) << '\n';
    std::cout << "função: dobro(1+2) = " << dobro(1+2) << '\n';
}

2. #include: colar outro arquivo aqui

#include quer dizer "inclua". O pré-processador abre o arquivo indicado e cola o conteúdo inteiro no lugar da linha. Veja o tamanho da colagem num "Olá" de cinco linhas:

#include <iostream>

int main() {
    std::cout << "Olá!\n";
}
$ g++ -std=c++23 -E ola.cpp -o ola.ii
$ wc -l ola.ii
68846 ola.ii             « 68 mil linhas!
$ tail -5 ola.ii

# 3 "ola.cpp"
int main() {
    std::cout << "Olá!\n";
}

O seu programa de 5 linhas virou um arquivo de quase 69 mil linhas: o <iostream> inclui outros arquivos, que incluem outros, e tudo é colado. Só no final aparece o seu main. (O .ii é a extensão costumeira para "C++ já pré-processado"; wc -l conta linhas.) É por isso que compilar C++ pode demorar, e é um dos motivos dos Módulos do C++20.

Com < > ou com " "?

As duas formas fazem a mesma colagem. A diferença é onde o pré-processador procura o arquivo:

FormaOnde procuraUse para
#include <iostream>só nas pastas do sistema (as da biblioteca padrão e das bibliotecas instaladas)biblioteca padrão e bibliotecas de terceiros
#include "calculos.h"primeiro na pasta do arquivo que está incluindo; se não achar, nas pastas do sistemaos arquivos do seu próprio projeto

Dá para ver a lista de pastas pedindo ao g++ que conte o que está fazendo (-v, de verbose, "falante"). O comando é comprido porque pré-processa um arquivo vazio só para mostrar a lista:

$ echo | g++ -std=c++23 -x c++ -E -v - 2>&1 | sed -n '/search starts here/,/End of search/p'
#include "..." search starts here:
#include <...> search starts here:
 /usr/include/c++/15                  « aqui mora o arquivo iostream
 /usr/include/x86_64-linux-gnu/c++/15
 /usr/include/c++/15/backward
 /usr/lib/gcc/x86_64-linux-gnu/15/include
 /usr/local/include
 /usr/include
End of search list.

A lista das aspas está vazia porque, para elas, a primeira pasta é sempre a do próprio arquivo, e só depois vem a lista de baixo. O <iostream> é o arquivo /usr/include/c++/15/iostream (sem extensão mesmo).

"fatal error: …: Arquivo ou diretório inexistente"

A Ana criou um arquivo precos.h na mesma pasta e o incluiu com sinais de menor e maior:

#include <iostream>
#include <precos.h>

int main() {
    std::cout << "Pão: R$ " << PRECO_PAO << '\n';
}
$ g++ -std=c++23 -Wall -Wextra loja.cpp -o loja
loja.cpp:2:10: fatal error: precos.h: Arquivo ou diretório inexistente
    2 | #include <precos.h>
      |          ^~~~~~~~~~
compilation terminated.

O que significa: fatal error é um erro que faz o g++ desistir na hora (compilation terminated, "compilação encerrada"). Com < >, ele procurou precos.h só nas pastas do sistema e não achou. A parte "Arquivo ou diretório inexistente" vem em português porque é o sistema que diz que o arquivo não existe; o resto da mensagem é do g++, em inglês.

Como resolver: para arquivos seus, use aspas: #include "precos.h". Outra saída é acrescentar uma pasta à lista do sistema com -I (de include): -I. quer dizer "procure também na pasta atual".

$ g++ -std=c++23 -Wall -Wextra -I. loja.cpp -o loja
$ ./loja
Pão: R$ 0.75

O mesmo erro aparece se o nome estiver errado ("preco.h" no lugar de "precos.h") ou se o arquivo estiver em outra pasta. Confira com ls.

Como separar um programa em arquivos .h e .cpp é o assunto da próxima aula, Arquivos .h e .cpp.


3. #define para constantes

A forma mais simples do #define não tem parâmetros: só troca um nome por um valor. Você vai encontrá-la em muito código antigo e em código C:

#define PRECO 10        // onde aparecer PRECO, escreva 10
#define PI 3.14159
#define LOJA "Lojinha da Ana"

Em C++, existe um jeito melhor de criar constantes, que você já viu em Constantes (const e constexpr):

constexpr int PRECO = 10;
constexpr double PI = 3.14159;
#define PRECO 10constexpr int PRECO = 10;
Tem tipo?não, é só textosim, int
Respeita escopo (chaves, namespaces)?não: vale do #define até o fim do arquivo, em todo lugarsim
Aparece nas mensagens de erro e no depurador?não, o compilador só vê 10sim, pelo nome
Termina com ;?não (veja o cenário abaixo)sim

Qual usar: constexpr (ou const) para constantes. Deixe o #define para o que só o pré-processador consegue fazer, como as compilações condicionais da próxima seção.

Um ; a mais no #define
#include <iostream>

#define PRECO 10;

int main() {
    int total = PRECO * 3;
    std::cout << total << '\n';
}
$ g++ -std=c++23 -Wall -Wextra ponto.cpp -o ponto
ponto.cpp: In function ‘int main()’:
ponto.cpp:6:23: error: invalid type argument of unary ‘*’ (have ‘int’)
    6 |     int total = PRECO * 3;
      |                       ^~~

O que aconteceu: o ; faz parte do texto da macro. A linha 6 virou int total = 10; * 3;. O compilador lê int total = 10; e depois * 3; sozinho, e reclama que o * "de um operando só" (unary) não serve para um int. A mensagem fala de * e não de PRECO, porque o compilador nunca viu a palavra PRECO: esse é o pior defeito das macros, o erro aparece longe da causa.

Como resolver: tire o ; do #define. Melhor ainda: troque por constexpr int PRECO = 10;.

"warning: ‘DESCONTO’ redefined"
#include <iostream>

#define DESCONTO 10
#define DESCONTO 15

int main() {
    std::cout << DESCONTO << '\n';
}
$ g++ -std=c++23 -Wall -Wextra redef.cpp -o redef
redef.cpp:4:9: warning: ‘DESCONTO’ redefined
    4 | #define DESCONTO 15
      |         ^~~~~~~~
redef.cpp:3:9: note: this is the location of the previous definition
    3 | #define DESCONTO 10
      |         ^~~~~~~~
$ ./redef
15

O que significa: "‘DESCONTO’ foi redefinido". A segunda definição venceu, e a nota (note) mostra onde estava a primeira. Em programas grandes, isso costuma acontecer quando dois arquivos incluídos usam o mesmo nome de macro, e é um ótimo motivo para preferir constexpr, que daria um erro claro.

Como resolver: apague uma das definições. Se você quer mesmo trocar o valor, desfaça a primeira antes com #undef DESCONTO ("desdefina").


4. Ligar e desligar código: #ifdef, #ifndef e #if

Aqui o pré-processador faz algo que o C++ comum não faz: recortar pedaços do código antes de compilar. O uso mais comum é deixar mensagens de depuração no programa e ligá-las só quando precisar:

#include <iostream>

int main() {
    int total = 42;
#ifdef DEBUG
    std::cout << "[debug] total vale " << total << '\n';
#endif
    std::cout << "Total: " << total << '\n';
}
#ifdef DEBUG
"Se DEBUG estiver definido" (if defined). Só pergunta se o nome existe; o valor não importa.
a linha do meio
Fica no código só se a resposta for sim. Se for não, o pré-processador a apaga e o compilador nem fica sabendo que ela existiu.
#endif
"Fim do se". Todo #ifdef, #ifndef e #if precisa do seu.

E onde se define DEBUG? Pode ser com #define DEBUG no topo do arquivo, mas o jeito prático é na linha de comando, com -D (de define), sem mexer no código:

$ g++ -std=c++23 -Wall -Wextra debug.cpp -o debug
$ ./debug
Total: 42
$ g++ -std=c++23 -Wall -Wextra -DDEBUG debug.cpp -o debug
$ ./debug
[debug] total vale 42
Total: 42

-DDEBUG (tudo junto) é o mesmo que escrever #define DEBUG na primeira linha do arquivo. Para dar um valor, use =: -DTAXA=8 equivale a #define TAXA 8.

#ifndef: um valor padrão que a linha de comando pode trocar

#ifndef é o contrário: "se não estiver definido" (if not defined). Junto com #if, que testa uma conta, e #elif/#else, que funcionam como o else if e o else:

#include <iostream>

#ifndef TAXA
#define TAXA 5
#endif

#define NIVEL 2

int main() {
    std::cout << "Taxa: " << TAXA << "%\n";
#if NIVEL >= 2
    std::cout << "Mostrando detalhes (nível " << NIVEL << ")\n";
#elif NIVEL == 1
    std::cout << "Só o resumo\n";
#else
    std::cout << "Silêncio\n";
#endif
}
$ g++ -std=c++23 -Wall -Wextra taxa.cpp -o taxa
$ ./taxa
Taxa: 5%
Mostrando detalhes (nível 2)
$ g++ -std=c++23 -Wall -Wextra -DTAXA=8 taxa.cpp -o taxa
$ ./taxa
Taxa: 8%                « o -D chegou primeiro, e o #ifndef não trocou
Mostrando detalhes (nível 2)
DiretivaPerguntaExemplo
#ifdef XX está definido?#ifdef DEBUG
#ifndef XX não está definido?#ifndef TAXA
#if contaa conta (só com números inteiros e macros) dá diferente de zero?#if NIVEL >= 2
#if defined(X)o mesmo que #ifdef, mas pode ser combinado#if defined(DEBUG) && NIVEL > 1
#elif / #elsesenão, se… / senão
#endiffim do bloco
Não use #if no lugar do if

O #if decide na hora de compilar, olhando só macros e números escritos no código. Ele não enxerga variáveis: #if idade >= 18 não funciona, porque a variável idade só ganha valor quando o programa roda. Para decisões do programa, use o if normal.

Experimente: rode como está. Depois apague (ou comente com //) a linha #define DEBUG e rode de novo: as linhas [debug] somem. Por último, troque #define NIVEL 2 por 1 e por 0.

#include <iostream>

#define DEBUG
#define NIVEL 2

int main() {
    int itens = 3;
    double total = 27.5;
#ifdef DEBUG
    std::cout << "[debug] itens = " << itens << '\n';
    std::cout << "[debug] total = " << total << '\n';
#endif
#if NIVEL >= 2
    std::cout << "Compra com " << itens << " itens: R$ " << total << '\n';
#elif NIVEL == 1
    std::cout << "R$ " << total << '\n';
#else
    std::cout << "(modo silencioso)\n";
#endif
}

#error: parar a compilação com uma explicação

O compilador também define macros sozinho. Uma delas, __cplusplus, diz qual versão do C++ está valendo: 202302 com -std=c++23 e 201703 (C++17) se você não pedir nenhuma, no g++ 15. Junto com #error, dá para avisar quem esqueceu a opção:

#include <iostream>

#if __cplusplus < 202302L
#error "Este programa precisa de C++23: compile com -std=c++23"
#endif

int main() {
    std::cout << "Ok, C++23!\n";
}
$ g++ -Wall -Wextra versao.cpp -o versao
versao.cpp:4:2: error: #error "Este programa precisa de C++23: compile com -std=c++23"
    4 | #error "Este programa precisa de C++23: compile com -std=c++23"
      |  ^~~~~
$ g++ -std=c++23 -Wall -Wextra versao.cpp -o versao && ./versao
Ok, C++23!

O L no fim de 202302L marca o número como long; é assim que o padrão escreve esses valores (veja Versões do C++).


5. Onde estou? __FILE__ e __LINE__

Algumas macros já vêm prontas e mudam de valor conforme o lugar onde aparecem. São úteis em mensagens de erro e de depuração:

#include <iostream>

int main() {
    std::cout << "Arquivo: " << __FILE__ << '\n';
    std::cout << "Linha: " << __LINE__ << '\n';
    std::cout << "Função: " << __func__ << '\n';
    std::cout << "Compilado em: " << __DATE__ << '\n';
    std::cout << "__cplusplus = " << __cplusplus << '\n';
}
$ g++ -std=c++23 -Wall -Wextra onde.cpp -o onde
$ ./onde
Arquivo: onde.cpp
Linha: 5
Função: main
Compilado em: Oct 16 2026
__cplusplus = 202302
NomeViraObservação
__FILE__o nome do arquivo, como textodo jeito que foi passado ao g++
__LINE__o número da linha onde está escritoum número inteiro
__func__o nome da função atualnão é macro (é uma variável que o compilador cria), mas é usado junto
__DATE__ / __TIME__data e hora da compilaçãodata em inglês: Oct = outubro
__cplusplusa versão do C++201703, 202002, 202302…

Os dois sublinhados no começo e no fim são uma convenção: nomes assim são reservados para o compilador e a biblioteca. Não crie nomes seus com __.

Uma alternativa moderna C++20

O cabeçalho <source_location> traz std::source_location::current(), que guarda arquivo, linha e função sem macros: loc.file_name(), loc.line(), loc.function_name(). Você vai vê-lo em código novo; as macros continuam por toda parte em código antigo e em bibliotecas.


6. #pragma once: "inclua só uma vez"

#pragma é uma diretiva de "pedidos especiais" ao compilador. A mais usada de longe é #pragma once ("uma vez só"), escrita na primeira linha de um arquivo .h: se o mesmo arquivo for incluído duas vezes, o pré-processador cola o conteúdo só na primeira.

#pragma once

constexpr double PRECO_PAO = 0.75;

Por que um arquivo seria incluído duas vezes, o que dá errado quando isso acontece e qual é a alternativa clássica (os include guards, feitos com #ifndef/#define/#endif) são assuntos da aula Arquivos .h e .cpp.


Mais armadilhas das macros

Mesmo com todos os parênteses, uma macro pode avaliar o parâmetro duas vezes, porque o texto é copiado duas vezes:

#include <iostream>

#define QUADRADO(x) ((x) * (x))

int main() {
    int i = 3;
    int q = QUADRADO(i++);
    std::cout << "q = " << q << ", i = " << i << '\n';
}
$ g++ -std=c++23 -Wall -Wextra quadrado.cpp -o quadrado
quadrado.cpp: In function ‘int main()’:
quadrado.cpp:7:23: warning: operation on ‘i’ may be undefined [-Wsequence-point]
    7 |     int q = QUADRADO(i++);
      |                       ^
quadrado.cpp:3:23: note: in definition of macro ‘QUADRADO’
    3 | #define QUADRADO(x) ((x) * (x))
      |                       ^
$ ./quadrado
q = 12, i = 5

A linha virou ((i++) * (i++)): o i foi aumentado duas vezes e o resultado (12) não é nem 9 nem 16. O aviso diz "a operação sobre ‘i’ pode ser indefinida": mexer duas vezes na mesma variável numa só conta é comportamento indefinido, e outro compilador pode dar outro número. A nota mostra que o problema nasceu dentro da macro. Com uma função (constexpr int quadrado(int x)), o i++ rodaria uma vez só.

Para…EvitePrefira
uma constante#define PRECO 10constexpr int PRECO = 10;
uma "função" pequena#define DOBRO(x) ((x) * 2)constexpr int dobro(int x) { return x * 2; }
colar um cabeçalho#include (não há outro jeito, fora os módulos)
código só de depuração, ou só para um sistema#ifdef / #if + -D
proteger um .h#pragma once ou include guards

Referência

Diretiva ou opçãoO que faz
#include <arq>cola o arquivo, procurando nas pastas do sistema
#include "arq"cola o arquivo, procurando primeiro na pasta do arquivo atual
#define NOME textotroca NOME por texto daqui até o fim do arquivo
#define F(x) ((x) * 2)macro com parâmetro
#undef NOMEesquece a macro
#ifdef / #ifndef / #if / #elif / #else / #endifmantém ou recorta trechos do código
#error "texto"para a compilação com a mensagem
#pragma onceinclui este arquivo só uma vez
g++ -E arq.cppsó pré-processa e mostra o resultado (-P tira as marcas #)
g++ -DNOME / -DNOME=valordefine uma macro na linha de comando
g++ -I pastaacrescenta uma pasta à busca do #include
__FILE__, __LINE__, __DATE__, __cplusplusmacros prontas

Exercícios

1. Ver o que o compilador recebe

Complete o comando que mostra o arquivo mini.cpp já pré-processado, sem compilar:

g++ mini.cpp

2. Ligar a depuração sem mexer no código

Complete para compilar debug.cpp com a macro DEBUG definida:

g++ -std=c++23 -Wall -Wextra debug.cpp -o debug

3. Com #define TRIPLO(x) x*3, quanto vale TRIPLO(2+1)?

4. A Ana escreveu #include <estoque.h> para um arquivo que está na mesma pasta do main.cpp e recebeu fatal error: estoque.h: Arquivo ou diretório inexistente. Qual é o conserto mais simples?

5. Qual destas tarefas só o pré-processador consegue fazer?

Missão final: conserte as macros da lojinha

Missão: o programa abaixo mostra três números errados por causa das macros. Conserte só as linhas #define (faltam parênteses) para que a saída fique assim:

DOBRO(1+2) = 6
100 / DOBRO(5) = 10
Preço com taxa: 110
#include <iostream>

#define DOBRO(x) x*2
#define COM_TAXA(p) p + p / 10

int main() {
    std::cout << "DOBRO(1+2) = " << DOBRO(1+2) << '\n';
    std::cout << "100 / DOBRO(5) = " << 100 / DOBRO(5) << '\n';
    std::cout << "Preço com taxa: " << COM_TAXA(50) * 2 << '\n';
}
#include <iostream>

#define DOBRO(x) ((x) * 2)
#define COM_TAXA(p) ((p) + (p) / 10)

int main() {
    std::cout << "DOBRO(1+2) = " << DOBRO(1+2) << '\n';
    std::cout << "100 / DOBRO(5) = " << 100 / DOBRO(5) << '\n';
    std::cout << "Preço com taxa: " << COM_TAXA(50) * 2 << '\n';
}

Missão bônus: sem mexer em nada dentro do main, acrescente linhas #define no topo para que o programa mostre Modo: teste e Taxa: 8. Dica: o #ifndef TAXA só põe 5 se ninguém tiver definido TAXA antes dele.

#include <iostream>

// escreva os seus #define aqui

#ifndef TAXA
#define TAXA 5
#endif

int main() {
#ifdef MODO_TESTE
    std::cout << "Modo: teste\n";
#else
    std::cout << "Modo: normal\n";
#endif
    std::cout << "Taxa: " << TAXA << '\n';
}
#include <iostream>

#define MODO_TESTE
#define TAXA 8

#ifndef TAXA
#define TAXA 5
#endif

int main() {
#ifdef MODO_TESTE
    std::cout << "Modo: teste\n";
#else
    std::cout << "Modo: normal\n";
#endif
    std::cout << "Taxa: " << TAXA << '\n';
}

Resumo

  • O pré-processador roda antes do compilador e só mexe em texto: cola arquivos, troca nomes, recorta trechos. As ordens para ele começam com # e não têm ;.
  • #include <x> procura nas pastas do sistema; #include "x" procura primeiro na pasta do arquivo. Use aspas para os seus arquivos.
  • #define troca texto, sem entender C++: DOBRO(1+2) com x*2 vira 1+2*2 = 5. Em macros, parênteses em cada parâmetro e em volta de tudo.
  • Para constantes e "funções", prefira constexpr: tem tipo, respeita escopo e dá erros claros.
  • #ifdef, #ifndef, #if… #endif ligam e desligam código na hora de compilar; g++ -DNOME define o nome sem mexer no arquivo.
  • g++ -E (com -P) mostra o que o compilador recebe de verdade: é o raio-x de qualquer problema com macros.
  • __FILE__ e __LINE__ dizem onde você está; #pragma once protege um .h de ser colado duas vezes.