Macros são uma das ferramentas mais poderosas e, ao mesmo tempo, perigosas da linguagem C. Elas permitem a substituição de texto antes da compilação, possibilitando a criação de constantes simbólicas, funções inline e geração de código. No entanto, seu uso inadequado pode levar a erros difíceis de diagnosticar. Nesta aula, vamos mergulhar no universo das macros, entendendo sua sintaxe, seus truques e suas armadilhas, para que você possa utilizá-las com confiança e precisão.

Dominar macros é essencial para programadores C que desejam escrever código eficiente e reutilizável. Elas são amplamente utilizadas em bibliotecas, sistemas embarcados e projetos de grande porte. Ao final desta aula, você estará apto a criar macros robustas, evitar comportamentos inesperados e aplicar as melhores práticas no seu dia a dia.

Macros simples e com argumentos

Uma macro simples é uma substituição textual definida com a diretiva #define. Ela pode representar uma constante, uma expressão ou até mesmo um bloco de código. A forma mais básica é:

#define PI 3.14159
#define ERRO "Mensagem de erro"

No exemplo acima, toda ocorrência de PI no código será substituída por 3.14159 durante a pré-compilação. Isso é útil para dar nomes significativos a valores constantes, facilitando a manutenção.

Macros com argumentos funcionam como funções, mas a substituição é feita literalmente. Por exemplo:

#define SOMA(a, b) ((a) + (b))
#define QUADRADO(x) ((x) * (x))

Essas macros aceitam parâmetros e os expandem no corpo da definição. É importante notar que os parênteses são essenciais para evitar problemas de precedência, como veremos adiante. Macros com argumentos são frequentemente usadas para operações simples onde uma função teria overhead de chamada, embora compiladores modernos otimizem bem funções inline.

Um exemplo prático de uso:

#include <stdio.h>
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int main() {
    int x = 5, y = 10;
    printf("Maior: %d\n", MAX(x, y));
    return 0;
}

Neste código, MAX(x, y) é expandido para ((x) > (y) ? (x) : (y)), resultando em 10.

Parênteses e armadilhas

A maior armadilha das macros é a falta de parênteses, que pode causar erros de precedência. Considere a macro mal definida:

#define SOMA(a, b) a + b

Se você usá-la em uma expressão maior, como SOMA(1, 2) * 3, a expansão será 1 + 2 * 3, que resulta em 7 em vez de 9, porque a multiplicação tem precedência maior. A correção é envolver cada argumento e o resultado em parênteses:

#define SOMA(a, b) ((a) + (b))

Outra armadilha comum é a avaliação múltipla de argumentos. Se uma macro usa um argumento mais de uma vez, e esse argumento tem efeitos colaterais (como i++), ele será avaliado múltiplas vezes. Por exemplo:

#define QUADRADO(x) ((x) * (x))
int i = 2;
int resultado = QUADRADO(i++); // Expande para ((i++) * (i++))

Isso incrementa i duas vezes, resultando em comportamento indefinido. Para evitar isso, use funções inline ou variáveis temporárias. Se precisar de uma macro, documente essa limitação.

Também é importante evitar macros com efeitos colaterais em argumentos, como em MAX(a++, b). A solução é usar funções quando a operação for complexa ou os argumentos tiverem efeitos colaterais.

Outra dica: ao definir macros com várias linhas, use a barra invertida (\) para continuar a definição na próxima linha. Isso ajuda na legibilidade, mas cuidado com espaços após a barra.

#define TROCA(a, b) do { \
    int temp = (a); \
    (a) = (b); \
    (b) = temp; \
} while(0)

O padrão do { ... } while(0) garante que a macro se comporte como uma única instrução, evitando problemas em estruturas if sem chaves.

# e ## (stringify, concat)

O operador # (stringify) converte um argumento de macro em uma string literal. Por exemplo:

#define STR(x) #x
printf(STR(hello)); // Expande para printf("hello");

Isso é útil para depuração, permitindo imprimir o nome de uma variável ou expressão. Por exemplo:

#define PRINT_INT(x) printf(#x " = %d\n", x)
int a = 42;
PRINT_INT(a); // Imprime "a = 42"

O operador ## (concatenação) une dois tokens em um único token. É frequentemente usado para gerar nomes de variáveis ou funções dinamicamente. Exemplo:

#define CONCAT(a, b) a ## b
int CONCAT(var, 1) = 10; // Declara var1 = 10;
printf("%d\n", var1);

Também pode ser usado com macros para gerar código repetitivo, como em:

#define DECLARE_VAR(name, type) type var_ ## name
DECLARE_VAR(x, int); // int var_x;

Esses operadores são poderosos, mas devem ser usados com cuidado, pois a concatenação pode gerar tokens inválidos se os argumentos não forem adequados.

Exemplos

Vamos combinar vários conceitos em exemplos práticos. O primeiro exemplo mostra como usar macros para criar uma função de assert personalizada:

#include <stdio.h>
#include <stdlib.h>
#define ASSERT(cond) do { \
    if (!(cond)) { \
        fprintf(stderr, "Assert falhou: %s em %s:%d\n", #cond, __FILE__, __LINE__); \
        exit(EXIT_FAILURE); \
    } \
} while(0)

int main() {
    int x = 5;
    ASSERT(x == 5); // Passa
    ASSERT(x == 6); // Falha
    return 0;
}

O segundo exemplo mostra como gerar getters e setters para uma estrutura:

#include <stdio.h>
#define DEFINE_GETTER(type, struct_name, field) \
    type get_ ## field(struct struct_name *obj) { return obj->field; }
#define DEFINE_SETTER(type, struct_name, field) \
    void set_ ## field(struct struct_name *obj, type value) { obj->field = value; }

typedef struct { int idade; } Pessoa;
DEFINE_GETTER(int, Pessoa, idade)
DEFINE_SETTER(int, Pessoa, idade)

int main() {
    Pessoa p;
    set_idade(&p, 30);
    printf("Idade: %d\n", get_idade(&p));
    return 0;
}

Esses exemplos demonstram a versatilidade das macros, mas também evidenciam a necessidade de testar e documentar seu comportamento.

Boas práticas

Ao usar macros, siga estas recomendações:

  • Use parênteses em todos os argumentos e no resultado.
  • Evite avaliar argumentos mais de uma vez se eles tiverem efeitos colaterais.
  • Prefira funções inline ou enum para constantes quando possível.
  • Nomeie macros em MAIÚSCULAS para diferenciá-las de funções.
  • Use #undef para evitar conflitos em grandes projetos.
  • Documente macros complexas com comentários.

Lembre-se de que macros são processadas pelo pré-processador, antes da compilação, e não respeitam escopo. Isso pode causar efeitos globais indesejados.

Referências

Exercícios

  1. Defina uma macro MAIOR3(a,b,c) que retorna o maior de três valores. Teste com valores inteiros e decimais.

    ✓ Resposta:
    #define MAIOR3(a,b,c) ((a) > (b) ? ((a) > (c) ? (a) : (c)) : ((b) > (c) ? (b) : (c)))
  2. Explique o que acontece se você usar a macro QUADRADO(x) ((x)*(x)) com a expressão QUADRADO(++i). Qual é o problema e como corrigi-lo?

    ✓ Resposta: A macro expande para ((++i)*(++i)), incrementando i duas vezes, causando comportamento indefinido. A correção é usar uma função inline ou uma variável temporária: int temp = ++i; int quadrado = temp*temp;.
  3. Escreva uma macro PRINT_VAR(var) que imprime o nome da variável e seu valor inteiro, usando o operador #. Teste com uma variável int x = 7;.

    ✓ Resposta:
    #define PRINT_VAR(var) printf(#var " = %d\n", var)
    int x = 7;
    PRINT_VAR(x); // Imprime "x = 7"
  4. Crie uma macro DECLARE_ARRAY(type, name, size) que declara um array estático de um tipo e tamanho dados. Use ## para concatenar o nome do array com um sufixo, por exemplo, DECLARE_ARRAY(int, arr, 10) deve declarar int arr_array[10];.

    ✓ Resposta:
    #define DECLARE_ARRAY(type, name, size) type name ## _array[size]
    DECLARE_ARRAY(int, arr, 10); // declara int arr_array[10];
  5. Qual é a saída do seguinte programa? Justifique.

    #include <stdio.h>
    #define SOMA(a,b) a+b
    int main() {
        printf("%d\n", SOMA(2,3)*4);
        return 0;
    }

    ✓ Resposta: A saída é 14, pois a macro expande para 2+3*4, que é avaliado como 2+(3*4)=14. Para obter 20, defina a macro com parênteses: #define SOMA(a,b) ((a)+(b)).