🌍 Cap 01: Linguagem C — Ponteiros, Aritmética de Memória e Makefile
💡 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.
- Compilador: GCC via MinGW-w64 — adicione a pasta
bindo MinGW aoPathdo Windows. - 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 deint, destruct, dechar*, 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
⚠️
reallocpode 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 umreallocé 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 aomakedo Linux/macOS — basta chamarmingw32-makeem vez demakeno 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:
- 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.
- Iniciar a Depuração: Pressione
F5ou vá para a abaRun and Debuge clique no botão verdeStart Debugging. - Analisar Variáveis: Com a execução pausada, a janela
VARIABLESno painel esquerdo mostrará o valor atual de todas as variáveis locais — incluindo o endereço guardado em cada ponteiro. - 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.
- Step Over (