💡 Pré-requisito: este capítulo assume que você já fez o Módulo 02: Lógica de Programação, onde tipos de dados, operadores, if/else, laços, vetores e matrizes em C já foram ensinados em profundidade. Aqui vamos direto ao que aquele módulo não cobre: ponteiros avançados, gestão manual de memória e automação de build com Makefile.

🛠️ Instalação e Configuração do Ambiente (VS Code)

Se você já configurou o Code::Blocks no Módulo 02, pode reaproveitar o mesmo compilador (GCC/MinGW). Para este capítulo, recomendamos também o VS Code com a extensão C/C++ da Microsoft, por ter um terminal integrado mais prático para rodar make.

  1. Compilador: GCC via MinGW-w64 — adicione a pasta bin do MinGW ao Path do Windows.
  2. VS Code: baixe em code.visualstudio.com e instale a extensão C/C++.

📋 Referência Rápida de Sintaxe C

Uma tabela de consulta rápida — não uma reexplicação. Se qualquer uma destas linhas parecer nova, volte ao Módulo 02 antes de continuar.

Categoria Sintaxe
Tipos primitivos int, double, float, char, char[] (texto)
Operadores aritméticos + - * / %
Operadores comparativos < > <= >= == !=
Operadores lógicos && \|\| !
Saída (printf) %d int · %f/%lf decimal · %c caractere · %s texto
Entrada (scanf) scanf("%d", &var); — sempre com & para tipos simples
Condicional if (cond) { ... } else { ... }
Laços while (cond) { } · for (i=0; i<n; i++) { } · do { } while (cond);
Vetor / Matriz int v[N]; · int m[L][C];

Exemplo Combinado (Revisão de 1 Minuto)

#include <stdio.h>

int main() {
    int n;
    printf("Quantos numeros? ");
    scanf("%d", &n);

    int soma = 0;
    for (int i = 0; i < n; i++) {
        int valor;
        printf("Valor #%d: ", i + 1);
        scanf("%d", &valor);
        soma += valor;
    }

    double media = (double) soma / n;
    printf("Soma = %d | Media = %.2f\n", soma, media);

    return 0;
}

📍 Ponteiros e Aritmética de Memória

Revisão Rápida: Endereço (&) e Dereferência (*)

#include <stdio.h>

int main() {
    int idade = 25;
    int *pIdade = &idade;

    printf("Valor de idade: %d\n", idade);
    printf("Valor apontado por pIdade (*pIdade): %d\n", *pIdade);

    *pIdade = 30; // Altera 'idade' indiretamente
    printf("Novo valor de idade: %d\n", idade);

    return 0;
}

Saída: Valor de idade: 25 → Valor apontado por pIdade (*pIdade): 25 → Novo valor de idade: 30

Ponteiro para Ponteiro (**)

Um ponteiro comum guarda o endereço de uma variável. Um ponteiro para ponteiro guarda o endereço de outro ponteiro — útil quando uma função precisa alocar memória e “devolver” o próprio ponteiro para quem a chamou:

#include <stdio.h>
#include <stdlib.h>

void alocarEIniciar(int **ponteiroParaPonteiro) {
    // *ponteiroParaPonteiro é o próprio ponteiro 'p' de quem chamou
    *ponteiroParaPonteiro = malloc(sizeof(int));
    **ponteiroParaPonteiro = 42;
}

int main() {
    int *p = NULL;

    alocarEIniciar(&p); // Passa o ENDEREÇO do ponteiro, para a função poder alterá-lo

    printf("Valor alocado: %d\n", *p);

    free(p);
    p = NULL;

    return 0;
}

Saída: Valor alocado: 42

⚠️ Por que não bastaria int *alocarEIniciar() com retorno? Retornar o ponteiro também funcionaria e é mais comum na prática — o exemplo acima existe porque, em C, muitas funções da biblioteca padrão (e bibliotecas de terceiros) usam esse padrão de “ponteiro de saída” quando a função já precisa retornar um código de erro (int) separadamente.

Ponteiro de Função: Callbacks com qsort

Uma função também tem um endereço de memória. Guardar esse endereço numa variável permite “passar comportamento” como parâmetro — a biblioteca padrão de C usa isso na função de ordenação genérica qsort:

#include <stdio.h>
#include <stdlib.h>

// qsort exige uma funcao de comparacao com esta assinatura exata
int compararCrescente(const void *a, const void *b) {
    int valorA = *(const int *) a;
    int valorB = *(const int *) b;
    return valorA - valorB;
}

int compararDecrescente(const void *a, const void *b) {
    int valorA = *(const int *) a;
    int valorB = *(const int *) b;
    return valorB - valorA;
}

void imprimirVetor(int vet[], int n) {
    for (int i = 0; i < n; i++) {
        printf("%d ", vet[i]);
    }
    printf("\n");
}

int main() {
    int numeros[] = {5, 2, 8, 1, 9, 3};
    int n = 6;

    // qsort recebe um PONTEIRO DE FUNCAO como ultimo argumento
    qsort(numeros, n, sizeof(int), compararCrescente);
    printf("Crescente:   ");
    imprimirVetor(numeros, n);

    qsort(numeros, n, sizeof(int), compararDecrescente);
    printf("Decrescente: ");
    imprimirVetor(numeros, n);

    return 0;
}

Saída:

Crescente:   1 2 3 5 8 9
Decrescente: 9 8 5 3 2 1

💡 Por que const void *? qsort é genérica — funciona para vetor de int, de struct, de char*, etc. Como ela não sabe de antemão o tipo dos elementos, recebe ponteiros “genéricos” (void *) e delega para a sua função de comparação a responsabilidade de reinterpretá-los ((const int *) a) e comparar do jeito certo.


💾 Alocação Dinâmica: malloc, realloc e free

Vetores comuns (int v[10];) têm tamanho fixo, decidido em tempo de compilação. Quando o tamanho só é conhecido durante a execução — ou precisa crescer depois — usamos o Heap:

#include <stdio.h>
#include <stdlib.h>

int main() {
    int capacidade = 2;
    int *vet = malloc(capacidade * sizeof(int));

    vet[0] = 10;
    vet[1] = 20;

    printf("Antes: capacidade=%d -> %d, %d\n", capacidade, vet[0], vet[1]);

    // O vetor esta cheio: realloc pede mais espaco, preservando o conteudo existente
    capacidade = 4;
    vet = realloc(vet, capacidade * sizeof(int));
    vet[2] = 30;
    vet[3] = 40;

    printf("Depois: capacidade=%d -> %d, %d, %d, %d\n", capacidade, vet[0], vet[1], vet[2], vet[3]);

    free(vet);
    return 0;
}

Saída:

Antes: capacidade=2 -> 10, 20
Depois: capacidade=4 -> 10, 20, 30, 40

⚠️ realloc pode mover o bloco de memória para um novo endereço se não houver espaço contíguo suficiente para crescer no lugar — por isso sempre reatribuímos o resultado (vet = realloc(vet, ...)). Usar o ponteiro antigo depois de um realloc é um bug clássico de dangling pointer.


🛠️ Automação de Build com Makefile

Até aqui, todo programa deste guia cabia em um único arquivo .c. Projetos reais são divididos em vários arquivos — e recompilar tudo manualmente a cada mudança não escala. O Makefile automatiza isso.

Projeto de Exemplo (3 arquivos)

calculos.h (declara a função):

#ifndef CALCULOS_H
#define CALCULOS_H

double calcularMedia(double a, double b);

#endif

calculos.c (implementa a função):

#include "calculos.h"

double calcularMedia(double a, double b) {
    return (a + b) / 2.0;
}

main.c (usa a função):

#include <stdio.h>
#include "calculos.h"

int main() {
    double media = calcularMedia(7.5, 8.5);
    printf("Media: %.2f\n", media);
    return 0;
}

O Makefile

CC = gcc
CFLAGS = -std=c99 -Wall -Wextra

# Alvo principal: como gerar o executavel a partir dos objetos
programa: main.o calculos.o
	$(CC) $(CFLAGS) main.o calculos.o -o programa

# Regra: como gerar cada .o a partir do .c correspondente
main.o: main.c calculos.h
	$(CC) $(CFLAGS) -c main.c

calculos.o: calculos.c calculos.h
	$(CC) $(CFLAGS) -c calculos.c

# Alvo de limpeza: remove os arquivos gerados
clean:
	rm -f *.o programa programa.exe

⚠️ Atenção aos TABs! As linhas de comando dentro de cada regra ($(CC) $(CFLAGS) ...) devem começar com um caractere de tabulação, não espaços. É o erro de sintaxe mais comum em Makefiles para quem está começando.

Rodando: make, Recompilação Incremental e make clean

$ make
gcc -std=c99 -Wall -Wextra -c main.c
gcc -std=c99 -Wall -Wextra -c calculos.c
gcc -std=c99 -Wall -Wextra main.o calculos.o -o programa

Agora, se você alterar apenas calculos.c e rodar make de novo, o make recompila só o que mudou (comparando datas de modificação dos arquivos), não o projeto inteiro:

$ make
gcc -std=c99 -Wall -Wextra -c calculos.c
gcc -std=c99 -Wall -Wextra main.o calculos.o -o programa

Repare que main.c não foi recompilado — só calculos.c (que mudou) e o link final (que depende de ambos os .o). Em um projeto com 50 arquivos, essa economia é a diferença entre esperar 1 segundo ou 2 minutos a cada teste.

Para apagar tudo que foi gerado e voltar ao estado original:

$ make clean
rm -f *.o programa programa.exe

💻 No Windows, mingw32-make (instalado junto com o MinGW) funciona de forma idêntica ao make do Linux/macOS — basta chamar mingw32-make em vez de make no terminal, ou criar um atalho/alias.


🐞 Depuração (Debugging) no VS Code

Debugging é o processo de encontrar e corrigir erros no seu código. O VS Code oferece uma ferramenta de depuração poderosa.

Passos Básicos:

  1. Marcar um Breakpoint: Clique na margem esquerda do editor, ao lado do número da linha. Uma bola vermelha aparecerá — o breakpoint é um ponto de parada onde a execução do programa será pausada.
  2. Iniciar a Depuração: Pressione F5 ou vá para a aba Run and Debug e clique no botão verde Start Debugging.
  3. Analisar Variáveis: Com a execução pausada, a janela VARIABLES no painel esquerdo mostrará o valor atual de todas as variáveis locais — incluindo o endereço guardado em cada ponteiro.
  4. Controlar a Execução:
    • Step Over (F10): Executa a linha atual e para na próxima.
    • Step Into (F11): Se a linha atual for uma chamada de função, entra na função para depurá-la.
    • Continue (F5): Continua a execução normal até o próximo breakpoint ou o fim do programa.
    • Stop (Shift+F5): Para a sessão de depuração.