Skip to content

C Folha de referência

Linguagem de baixo nível que impulsiona sistemas, embarcados e desenvolvimento de OS.

01

Introdução

Hello World

Todo programa em C começa em main(). #include <stdio.h> traz a biblioteca padrão de I/O (printf, scanf). main retorna 0 em caso de sucesso, não zero em caso de falha. A palavra-chave void declara explicitamente que main não recebe parâmetros.

c
#include <stdio.h>

int main(void) {
    printf("Hello, World!\n");
    return 0;
}

Variáveis & Tipos

C é estaticamente tipado. Tipos comuns: int, float, double, char. float precisa do sufixo f. char[] é uma string (array terminado em null). long e short são modificadores de tamanho. unsigned significa não negativo. Tamanhos variam por plataforma; use <stdint.h> para larguras fixas.

c
int age = 30;
float height = 5.7f;
double pi = 3.14159;
char grade = 'A';
char name[] = "Alice";
long big = 100000L;
unsigned int count = 42;
printf("%s is %d\n", name, age);

Entrada & Saída

scanf precisa do endereço (&) da variável para armazenar a entrada. Sempre limite o comprimento da entrada de string (%49s para um buffer de 50 caracteres) para prevenir buffer overflow. scanf para de ler strings em whitespace; use fgets para linhas completas.

c
int n;
printf("Enter a number: ");
scanf("%d", &n);
printf("You entered %d\n", n);

char name[50];
printf("Enter name: ");
scanf("%49s", name);  // limit to prevent overflow
printf("Hi, %s!\n", name);

Especificadores de Formato do printf

Especificadores de formato controlam a saída: %d int, %f float, %c char, %s string, %x hex, %p pointer. Largura e precisão (ex.: %5.2f) controlam alinhamento e casas decimais. Incompatibilidade entre especificador e tipo causa comportamento indefinido.

c
printf("%d\n", 42);        // integer
printf("%f\n", 3.14);       // float/double
printf("%.2f\n", 3.14159);  // 3.14 (2 decimals)
printf("%c\n", 'A');        // char
printf("%s\n", "hello");    // string
printf("%x\n", 255);        // ff (hex)
printf("%5d\n", 42);        // right-aligned, width 5
printf("%-5d|\n", 42);      // left-aligned

Pré-processador & Headers

O pré-processador executa antes da compilação. #include cola arquivos de header, #define cria macros e constantes. Sempre use include guards (#ifndef/#define/#endif) em headers para prevenir dupla inclusão. Macros são substituição de texto—use parênteses em torno dos parâmetros.

c
#include <stdio.h>   // system header
#include "myheader.h"  // local header

#define PI 3.14159
#define SQUARE(x) ((x) * (x))

#ifndef GUARD_H
#define GUARD_H
// header content
#endif
02

Strings & String.h

Básico de Strings

Strings em C são arrays de char terminados em null. strlen conta caracteres antes de '\0'; sizeof retorna o tamanho do buffer. strcpy copia até o terminador null—sempre garanta que o destino seja grande o suficiente para evitar overflow.

c
#include <string.h>
char s[20] = "Hello";
printf("Length: %zu\n", strlen(s));   // 5
printf("Size: %zu\n", sizeof(s));     // 20

char dest[20];
strcpy(dest, s);   // copy
printf("%s\n", dest);  // Hello

Concatenação & Comparação

strcat anexa (o destino deve ter espaço). strcmp compara lexicograficamente: retorna 0 se iguais, negativo se primeiro < segundo, positivo se primeiro > segundo. Nunca use == para comparar strings (isso compara ponteiros, não conteúdo).

c
#include <string.h>
char s[30] = "Hello";
strcat(s, ", World!");   // s = "Hello, World!"
printf("%s\n", s);

int cmp = strcmp("apple", "banana");
// returns <0 if a<b, 0 if equal, >0 if a>b
if (strcmp(s, "Hello") == 0) {
    printf("Equal!\n");
}

sprintf & sscanf

sprintf formata em um buffer de string (como printf, mas para uma string). sscanf faz parse de uma string em variáveis (como scanf, mas de uma string). Use snprintf em vez de sprintf para prevenir buffer overflow especificando o tamanho máximo.

c
char buf[100];
int age = 30;
char name[] = "Alice";
sprintf(buf, "%s is %d years old", name, age);
printf("%s\n", buf);

int a, b;
sscanf("10 20", "%d %d", &a, &b);
printf("a=%d, b=%d\n", a, b);  // a=10, b=20

strchr, strstr & strtok

strchr encontra um caractere, strstr encontra uma substring. strtok divide uma string por delimitadores, mas modifica a string original (insere terminadores null) e não é thread-safe—passe NULL em chamadas subsequentes para continuar a tokenização.

c
#include <string.h>
char s[] = "Hello, World!";
char *p = strchr(s, 'W');   // find first 'W'
printf("%s\n", p);          // World!

char *sub = strstr(s, "World");
printf("%s\n", sub);        // World!

char tokens[] = "a,b,c";
char *tok = strtok(tokens, ",");
while (tok) {
    printf("%s\n", tok);
    tok = strtok(NULL, ",");
}

fgets & Entrada Segura

fgets é a maneira segura de ler strings—recebe um limite de tamanho para prevenir overflow. Diferente do scanf, lê espaços. A nova linha é incluída no resultado; strcspn a encontra e remove. Sempre prefira fgets em vez de gets (que foi removido no C11).

c
char line[100];
printf("Enter text: ");
fgets(line, sizeof(line), stdin);
// removes trailing newline
line[strcspn(line, "\n")] = 0;
printf("You said: %s\n", line);
03

Números & Matemática

Tipos Inteiros & Limites

Use <stdint.h> para tipos de largura fixa (int32_t, int64_t) quando tamanhos exatos importam. <limits.h> fornece INT_MAX, INT_MIN, etc. para limites específicos da plataforma. O sufixo LL marca literais long long. Tamanhos de int/long variam por plataforma.

c
#include <stdint.h>
#include <limits.h>
int32_t a = 100;
int64_t big = 9223372036854775807LL;
uint8_t byte = 255;

printf("INT_MAX = %d\n", INT_MAX);     // 2147483647
printf("INT_MIN = %d\n", INT_MIN);     // -2147483648
printf("UINT_MAX = %u\n", UINT_MAX);   // 4294967295

Ponto Flutuante

float tem 4 bytes (6-7 dígitos de precisão), double tem 8 bytes (15-16 dígitos). Nunca compare floats com == devido a erros de arredondamento—use fabs(a - b) < epsilon. <float.h> fornece DBL_MAX, DBL_EPSILON para limites e precisão.

c
#include <float.h>
double d = 3.141592653589793;
float f = 3.14f;

printf("DBL_MAX = %e\n", DBL_MAX);
printf("DBL_EPSILON = %e\n", DBL_EPSILON);

if (d == 0.1 + 0.2) {
    // likely false! floating point imprecision
}

Funções Matemáticas

<math.h> fornece funções matemáticas padrão. pow e sqrt retornam double. fabs é a versão float de abs (abs é para int). Link com -lm em alguns sistemas. Para cálculos financeiros, evite ponto flutuante—use centavos inteiros em vez disso.

c
#include <math.h>
double x = 2.5;
pow(x, 3);      // 15.625
sqrt(x);        // 1.581
fabs(-5.0);     // 5.0
floor(3.7);     // 3.0
ceil(3.2);      // 4.0
fmod(10.5, 3);  // 1.5
exp(1);         // 2.718 (e^1)
log(2.718);     // 1.0 (natural log)

Números Aleatórios

rand() retorna um int pseudoaleatório de 0 a RAND_MAX. Semeie com srand() uma vez no início do programa (usando time(NULL)). rand() % N tem viés de módulo e baixa qualidade; para uso sério, leia /dev/urandom ou use uma biblioteca PRNG de terceiros.

c
#include <stdlib.h>
#include <time.h>

srand(time(NULL));  // seed once at start
int r = rand() % 100;       // 0-99
int dice = rand() % 6 + 1;  // 1-6

float fr = (float)rand() / RAND_MAX;  // 0.0 - 1.0

Conversão de Tipos & Casting

Casting é (type)value. Divisão de inteiros trunca—use um operando float para obter um resultado float. atoi/atof convertem strings em números, mas não fazem verificação de erros; prefira strtol/strtod que relatam erros de parse via errno.

c
int i = 65;
char c = (char)i;        // 'A'
double d = 3.99;
int truncated = (int)d;  // 3

// Implicit promotion
int a = 5;
double result = a / 2.0;  // 2.5 (promoted to double)
int bad = a / 2;          // 2 (integer division)

// String to number
int n = atoi("42");
double f = atof("3.14");
04

Fluxo de Controle

If / Else

if/else if/else é o condicional padrão. C trata 0 como false e qualquer valor não zero como true. Use chaves mesmo para instruções únicas para evitar bugs ao adicionar linhas depois. Não há tipo booleano em C89; C99 adiciona _Bool e <stdbool.h>.

c
int score = 85;
if (score >= 90) {
    printf("A\n");
} else if (score >= 80) {
    printf("B\n");
} else if (score >= 70) {
    printf("C\n");
} else {
    printf("F\n");
}

Switch

switch salta para um rótulo case correspondente. Sempre use break para prevenir fall-through (cases 6 e 7 compartilham código intencionalmente). default trata valores não correspondidos. switch só funciona com tipos inteiros e char, não strings ou floats.

c
int day = 3;
switch (day) {
    case 1: printf("Mon\n"); break;
    case 2: printf("Tue\n"); break;
    case 3: printf("Wed\n"); break;
    case 6:
    case 7: printf("Weekend\n"); break;
    default: printf("Invalid\n");
}

Loop For

O loop for tem init; condition; update. sizeof(nums)/sizeof(nums[0]) calcula o comprimento do array em tempo de compilação. Declarar i dentro do loop for exige C99 ou posterior. O corpo do loop executa zero vezes se a condição for inicialmente false.

c
for (int i = 0; i < 5; i++) {
    printf("%d\n", i);
}

// Iterate an array
int nums[] = {10, 20, 30};
int n = sizeof(nums) / sizeof(nums[0]);
for (int i = 0; i < n; i++) {
    printf("%d\n", nums[i]);
}

While & Do-While

while verifica antes de executar (pode rodar zero vezes). do-while executa o corpo primeiro, depois verifica (roda pelo menos uma vez). do-while é ideal para validação de entrada e loops de menu onde o prompt deve aparecer antes que a condição possa ser verificada.

c
int count = 0;
while (count < 3) {
    printf("%d\n", count++);
}

int x;
do {
    printf("Enter positive: ");
    scanf("%d", &x);
} while (x <= 0);  // runs at least once

Break, Continue & goto

break sai do loop/switch mais próximo; continue pula para a próxima iteração. C não tem labeled break, então goto é a maneira idiomática de sair de loops profundamente aninhados. goto é, de resto, desencorajado, mas aceitável para padrões de limpeza e saídas de loops aninhados.

c
for (int i = 0; i < 10; i++) {
    if (i == 3) continue;  // skip 3
    if (i == 7) break;     // stop at 7
    printf("%d ", i);      // 0 1 2 4 5 6
}

// goto for breaking nested loops
for (int i = 0; i < n; i++) {
    for (int j = 0; j < m; j++) {
        if (found) goto done;
    }
}
done: printf("exited\n");
05

Funções

Definir & Chamar

Funções devem ser declaradas (protótipo) ou definidas antes do uso. Tipo de retorno void significa nenhum valor de retorno. const char *name significa que a função não modificará a string. C passa argumentos por valor; use ponteiros para simular passagem por referência.

c
int add(int a, int b) {
    return a + b;
}

void greet(const char *name) {
    printf("Hello, %s!\n", name);
}

int main(void) {
    int sum = add(3, 4);
    greet("Alice");
    return 0;
}

Recursão

Recursão chama a si mesma com uma entrada menor. Toda função recursiva precisa de um caso base para parar. O fib ingênuo acima é O(2^n)—exponencial. Use memoização ou iteração para eficiência. Recursão profunda pode estourar a call stack.

c
int factorial(int n) {
    if (n <= 1) return 1;       // base case
    return n * factorial(n - 1); // recursive case
}

int fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}
// factorial(5) == 120

Ponteiros de Função

Ponteiros de função armazenam o endereço de uma função, permitindo callbacks e dispatch dinâmico. A sintaxe int (*op)(int, int) declara um ponteiro para uma função que recebe dois ints e retorna int. Usado em qsort, event handlers e sistemas de plugins.

c
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

int (*op)(int, int) = add;
printf("%d\n", op(3, 4));  // 7

op = sub;
printf("%d\n", op(3, 4));  // -1

// As a parameter
int apply(int (*f)(int, int), int a, int b) {
    return f(a, b);
}

Funções Variádicas

Funções variádicas aceitam um número variável de argumentos usando <stdarg.h>. va_start inicializa, va_arg recupera o próximo argumento, va_end limpa. Você precisa de uma maneira de saber a contagem (ex.: um parâmetro de contagem ou um valor sentinela). printf funciona assim.

c
#include <stdarg.h>
int sum(int count, ...) {
    va_list args;
    va_start(args, count);
    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);
    }
    va_end(args);
    return total;
}
// sum(3, 10, 20, 30) == 60

static & inline

static em uma função/variável a limita à unidade de tradução atual (arquivo). static em uma variável local faz com que ela persista entre chamadas (como uma global, mas com escopo). inline sugere que o compilador incorpore o corpo da função; compiladores modernos o ignoram e decidem por si mesmos.

c
// static: internal linkage (file-local)
static int counter = 0;
static int next_id(void) { return ++counter; }

// inline: hint to expand inline
static inline int square(int x) { return x * x; }

// static local: persists across calls
int call_count(void) {
    static int n = 0;
    return ++n;
}
06

Arrays & Ponteiros

Arrays

Arrays são de tamanho fixo, indexados a partir de zero e armazenados contiguamente na memória. sizeof(arr)/sizeof(arr[0]) calcula o comprimento, mas só funciona em arrays reais, não em ponteiros (arrays decaem para ponteiros quando passados para funções, perdendo informação de tamanho).

c
int nums[5] = {1, 2, 3, 4, 5};
printf("%d\n", nums[0]);     // 1
printf("%d\n", nums[4]);     // 5

int len = sizeof(nums) / sizeof(nums[0]);  // 5

// Array of strings
char *fruits[] = {"apple", "banana", "cherry"};
printf("%s\n", fruits[1]);   // banana

Ponteiros

Ponteiros armazenam endereços de memória. & obtém o endereço, * faz a dereferência. Sempre inicialize ponteiros (use NULL se ainda não atribuído). Dereferenciar um ponteiro NULL ou não inicializado é comportamento indefinido (geralmente um crash). Verifique NULL antes de dereferenciar.

c
int x = 10;
int *ptr = &x;     // ptr holds address of x
printf("%p\n", (void*)ptr);  // address
printf("%d\n", *ptr);        // 10 (dereference)

*ptr = 20;         // modify x through pointer
printf("%d\n", x);  // 20

int *p = NULL;     // null pointer (points to nothing)
if (p) { /* safe to dereference */ }

Aritmética de Ponteiros

A aritmética de ponteiros é dimensionada pelo tamanho do elemento: p+1 move para o próximo elemento, não para o próximo byte. Isso faz p[i] equivalente a *(p+i). Subtrair dois ponteiros para o mesmo array fornece a contagem de elementos. A aritmética de ponteiros só é válida dentro de um array.

c
int arr[] = {10, 20, 30, 40, 50};
int *p = arr;       // points to arr[0]

printf("%d\n", *p);       // 10
printf("%d\n", *(p + 1)); // 20
printf("%d\n", *(p + 2)); // 30

p += 3;              // now points to arr[3]
printf("%d\n", *p);  // 40

int diff = (p - arr); // 3 (number of elements)

Arrays vs Ponteiros

Nomes de arrays decaem para ponteiros quando passados para funções ou usados em expressões, perdendo informação de tamanho. É por isso que você deve passar o comprimento do array separadamente. sizeof(arr) fornece o tamanho total do array apenas quando arr é um array verdadeiro, não um ponteiro.

c
int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr;  // arr decays to &arr[0]

// These are equivalent
printf("%d\n", arr[2]);
printf("%d\n", ptr[2]);
printf("%d\n", *(arr + 2));

// But sizeof differs
printf("%zu\n", sizeof(arr));   // 20 (5 * 4 bytes)
printf("%zu\n", sizeof(ptr));   // 8 (pointer size)

Arrays Multidimensionais

Arrays 2D são arrays de arrays, armazenados em row-major. grid[i][j] acessa linha i, coluna j. Ao passar para funções, o número de colunas deve ser especificado: void foo(int arr[][3], int rows). Para arrays 2D dinâmicos, use arrays de ponteiros.

c
int grid[2][3] = {
    {1, 2, 3},
    {4, 5, 6}
};

printf("%d\n", grid[0][1]);  // 2
printf("%d\n", grid[1][2]);  // 6

for (int i = 0; i < 2; i++) {
    for (int j = 0; j < 3; j++) {
        printf("%d ", grid[i][j]);
    }
    printf("\n");
}
07

Structs & Unions

Structs

Structs agrupam variáveis relacionadas de tipos diferentes. Membros são acessados com o operador ponto (.). Inicialize com notação de chaves. Structs são passadas por valor (copiadas); passe por ponteiro (struct Point *) para evitar cópia e modificar a original.

c
struct Point {
    int x;
    int y;
};

struct Point p = {3, 4};
printf("(%d, %d)\n", p.x, p.y);  // (3, 4)

p.x = 10;
p.y = 20;
printf("(%d, %d)\n", p.x, p.y);  // (10, 20)

typedef

typedef cria um alias para um tipo, então você pode escrever Student em vez de struct Student. É comumente usado com structs para simplificar a sintaxe. typedef também pode criar alias para tipos de ponteiro de função, tornando callbacks muito mais legíveis.

c
typedef struct {
    char name[50];
    int age;
    float gpa;
} Student;

Student s = {"Alice", 20, 3.8};
printf("%s: %d, GPA %.1f\n", s.name, s.age, s.gpa);

// typedef for other types
typedef unsigned long ulong;
typedef int (*CompareFn)(const void*, const void*);

Ponteiros para Structs

Quando você tem um ponteiro para uma struct, use o operador seta (->) para acessar membros. ptr->x é uma abreviação para (*ptr).x. Passe ponteiros de struct para funções para eficiência (evita copiar structs grandes) e para permitir modificação.

c
typedef struct {
    int x, y;
} Point;

Point p = {3, 4};
Point *ptr = &p;

// Arrow operator (->) for pointer members
printf("%d\n", ptr->x);   // 3
ptr->y = 10;
printf("%d\n", p.y);      // 10

// Equivalent: (*ptr).x

Unions

Unions sobrepõem múltiplos tipos na mesma memória—apenas um membro é válido por vez. Definir um membro sobrescreve os outros. Útil para type punning (reinterpretar bits) e economizar memória quando apenas um de vários tipos é necessário por vez.

c
union Value {
    int i;
    float f;
    char bytes[4];
};

union Value v;
v.i = 65;
printf("%d\n", v.i);          // 65
printf("%c\n", v.bytes[0]);   // 'A' (same memory)

v.f = 3.14f;
printf("%d\n", v.i);  // reinterpreted bits!

Bit Fields & Enums

Bit fields empacotam múltiplos campos pequenos em um único int, economizando memória (comum em protocolos e registradores de hardware). Enums definem constantes inteiras nomeadas (0, 1, 2... por padrão). Use enums em vez de #define para melhor depuração e segurança de tipo.

c
struct Flags {
    unsigned int bold : 1;
    unsigned int italic : 1;
    unsigned int size : 6;  // 0-63
};

struct Flags f = {1, 0, 12};
printf("bold=%d, size=%d\n", f.bold, f.size);

enum Color { RED, GREEN, BLUE };
enum Color c = GREEN;
printf("%d\n", c);  // 1
08

Gerenciamento de Memória

malloc & free

malloc aloca memória heap e retorna um ponteiro void (ou NULL em caso de falha). Sempre verifique NULL. Todo malloc deve ser pareado com um free para evitar vazamentos de memória. Definir o ponteiro como NULL após free previne bugs de use-after-free.

c
#include <stdlib.h>
int *arr = malloc(5 * sizeof(int));
if (arr == NULL) {
    fprintf(stderr, "malloc failed\n");
    return 1;
}
for (int i = 0; i < 5; i++) arr[i] = i * 2;
free(arr);  // release memory
arr = NULL; // avoid dangling pointer

calloc & realloc

calloc aloca e zera a memória (mais seguro que malloc, que tem lixo). realloc redimensiona: pode mover o bloco, retornando um novo ponteiro. Se realloc falhar, retorna NULL, mas o bloco original ainda é válido—use um ponteiro temporário para evitar vazamentos.

c
#include <stdlib.h>
// calloc: zero-initialized
int *arr = calloc(5, sizeof(int));  // all zeros

// realloc: resize
arr = realloc(arr, 10 * sizeof(int));
if (!arr) { /* handle failure, original still valid */ }

free(arr);

Stack vs Heap

Memória stack é automática (alocada/liberada com chamadas de função) e rápida, mas limitada (frequentemente 1-8 MB). Memória heap é gerenciada manualmente via malloc/free, muito maior, mas mais lenta e propensa a vazamentos. Use stack para dados pequenos e de curta duração; heap para dados grandes ou de longa duração.

c
// Stack: automatic, fast, limited size
int local_var = 42;
int arr[100];  // on the stack

// Heap: manual, large, slower
int *heap_arr = malloc(1000000 * sizeof(int));

// Stack frame is freed when function returns
// Heap memory persists until explicitly freed

Vazamentos de Memória & Ponteiros Pendentes

Vazamentos de memória ocorrem quando você perde o único ponteiro para a memória alocada (não consegue liberá-la). Ponteiros pendentes apontam para memória liberada—dereferenciá-los é comportamento indefinido. Double-free também é indefinido. Ferramentas como Valgrind e AddressSanitizer detectam esses bugs.

c
// Memory leak: lost the pointer, can't free
void leak(void) {
    int *p = malloc(100 * sizeof(int));
    // function returns without free -> leaked!
}

// Dangling pointer: using freed memory
int *p = malloc(sizeof(int));
free(p);
*p = 42;  // UNDEFINED BEHAVIOR!

// Double free: also undefined
free(p);  // crash likely

Arrays & Strings Dinâmicos

Alocação dinâmica permite criar strings/arrays cujo tamanho é determinado em tempo de execução. O chamador é responsável por liberar a memória. Sempre aloque strlen+1 para strings (o terminador null). Esse padrão (alocar, retornar, chamador libera) é comum em APIs de C.

c
#include <stdlib.h>
#include <string.h>

// Dynamic string copy
char *dup_str(const char *s) {
    char *copy = malloc(strlen(s) + 1);  // +1 for null
    if (copy) strcpy(copy, s);
    return copy;  // caller must free
}

char *name = dup_str("Alice");
printf("%s\n", name);
free(name);
09

File I/O

fopen & fclose

fopen abre um arquivo e retorna um ponteiro FILE (ou NULL em caso de falha). Modos: r (leitura), w (escrita/trunca), a (anexar), r+ (leitura/escrita), b (binário). Sempre verifique NULL. fclose descarrega buffers e fecha o arquivo. fgets lê uma linha com segurança.

c
#include <stdio.h>
FILE *f = fopen("data.txt", "r");
if (!f) {
    perror("fopen failed");
    return 1;
}

char line[256];
while (fgets(line, sizeof(line), f)) {
    printf("%s", line);
}

fclose(f);

fprintf & fscanf

fprintf e fscanf funcionam como printf/scanf, mas em arquivos. fscanf é frágil—incompatibilidades de formato causam problemas. Para parsing robusto, leia linhas com fgets e depois faça parse com sscanf. Sempre feche arquivos ao terminar para descarregar buffers e liberar recursos.

c
FILE *f = fopen("output.txt", "w");
fprintf(f, "Name: %s\n", "Alice");
fprintf(f, "Age: %d\n", 30);
fclose(f);

FILE *in = fopen("output.txt", "r");
char name[50];
int age;
fscanf(in, "Name: %49s\n", name);
fscanf(in, "Age: %d\n", &age);
printf("%s, %d\n", name, age);
fclose(in);

fread & fwrite (Binário)

fread/fwrite leem/escrevem bytes brutos—ideal para dados binários e structs. Os argumentos são: buffer, tamanho do elemento, contagem, arquivo. Arquivos binários são compactos, mas não portáveis entre arquiteturas (endianness, padding de struct). Sempre abra arquivos binários com modo 'b'.

c
typedef struct { int id; float score; } Record;

Record r = {1, 95.5f};
FILE *f = fopen("data.bin", "wb");
fwrite(&r, sizeof(Record), 1, f);
fclose(f);

Record r2;
FILE *in = fopen("data.bin", "rb");
fread(&r2, sizeof(Record), 1, in);
printf("id=%d, score=%.1f\n", r2.id, r2.score);
fclose(in);

fseek, ftell & rewind

fseek move a posição do arquivo: SEEK_SET (a partir do início), SEEK_CUR (relativo), SEEK_END (a partir do fim). ftell retorna a posição atual. rewind é uma abreviação para fseek(f, 0, SEEK_SET). Estes permitem acesso aleatório em arquivos, útil para bancos de dados e buscas indexadas.

c
FILE *f = fopen("data.txt", "r");
fseek(f, 0, SEEK_END);   // jump to end
long size = ftell(f);     // get position = file size
printf("Size: %ld bytes\n", size);

rewind(f);                // back to start
// or: fseek(f, 0, SEEK_SET);

fseek(f, 10, SEEK_SET);   // 10 bytes from start
char c = fgetc(f);
printf("Char at 10: %c\n", c);
fclose(f);

stderr & Streams Padrão

Todo programa em C tem três streams: stdin (teclado), stdout (tela), stderr (tela, sem buffer). Escrever erros em stderr os separa da saída normal, permitindo redirecionamento: program 2> errors.log. stderr é sem buffer para que mensagens apareçam antes de crashes.

c
#include <stdio.h>
// Three standard streams: stdin, stdout, stderr
fprintf(stdout, "Normal output\n");
fprintf(stderr, "Error: something went wrong\n");

int c;
while ((c = fgetc(stdin)) != EOF) {
    fputc(c, stdout);  // echo input
}

// stderr is unbuffered (appears immediately)
// stdout is line-buffered (flushes on newline)
10

Pré-processador & Macros

#define Constantes & Macros

#define cria macros de substituição de texto. Constantes como PI melhoram a legibilidade e a manutenibilidade. Macros semelhantes a funções devem envolver parâmetros em parênteses para evitar bugs de precedência: SQUARE(2+3) sem parênteses seria 2+3*2+3=11. Prefira variáveis const e funções inline em vez de macros.

c
#define MAX_SIZE 100
#define PI 3.14159
#define VERSION "2.0"

// Function-like macro
#define SQUARE(x) ((x) * (x))
#define MAX(a, b) ((a) > (b) ? (a) : (b))

int area = SQUARE(5);    // 25
int big = MAX(3, 7);     // 7

Compilação Condicional

Compilação condicional (#if, #ifdef, #ifndef) inclui/exclui código em tempo de compilação. Isso é usado para código específico de plataforma, builds de depuração e feature flags. #ifdef verifica se uma macro está definida; #if avalia seu valor. #elif e #else fornecem alternativas.

c
#define DEBUG 1

#if DEBUG
    printf("Debug: x=%d\n", x);
#endif

#ifdef _WIN32
    // Windows-specific code
#elif defined(__linux__)
    // Linux-specific code
#endif

#ifndef BUFFER_SIZE
#define BUFFER_SIZE 1024
#endif

Include Guards

Include guards previnem dupla inclusão de headers, o que causaria erros de redefinição. O padrão #ifndef/#define/#endif é C padrão. #pragma once é uma alternativa mais simples e amplamente suportada (não é padrão, mas funciona em todos os principais compiladores).

c
// myheader.h
#ifndef MYHEADER_H
#define MYHEADER_H

struct Point { int x, y; };
void init_point(struct Point *p);

#endif // MYHEADER_H

// Alternative (non-standard but widely supported):
#pragma once

#pragma & Dicas do Compilador

#pragma fornece diretivas específicas do compilador. #pragma once é um include guard mais simples. #pragma pack controla o layout de memória da struct (importante para protocolos binários). __attribute__ (GCC/Clang) anota funções para otimização, depreciação e avisos.

c
#pragma once              // include guard
#pragma pack(1)            // struct packing (no padding)
#pragma GCC diagnostic ignored "-Wunused-variable"

// Common pragmas
#pragma message("Compiling " __FILE__)

// C99 __attribute__ (GCC/Clang)
__attribute__((deprecated)) void old_func(void);
__attribute__((noreturn)) void fatal(void);

Stringification & Token Pasting

# (stringification) transforma um argumento de macro em um literal de string. ## (token pasting) concatena tokens em um novo identificador. O padrão de dois níveis STR/XSTR garante que as macros sejam expandidas antes da stringification. Estes são usados em geração de código e macros de logging.

c
#define STR(x) #x
#define XSTR(x) STR(x)
#define CONCAT(a, b) a##b

printf("%s\n", STR(Hello World));  // "Hello World"
printf("%s\n", XSTR(VERSION));      // expands VERSION first

int CONCAT(foo, bar) = 42;  // creates variable foobar
printf("%d\n", foobar);     // 42
11

Operações de Bits

Operadores Bitwise Básicos

Operadores bitwise manipulam bits individuais. AND (&) mascara bits (mantém apenas bits definidos), OR (|) define bits, XOR (^) alterna bits, NOT (~) inverte todos os bits. Left shift (<<) multiplica por potências de 2, right shift (>>) divide (para unsigned). Sempre use tipos unsigned para manipulação de bits—right shift com sinal é implementation-defined (pode fazer sign-extension). Operações de bits são extremamente rápidas (ciclo único de CPU) e usadas em flags, registradores de hardware, compressão e criptografia. Literais binários (prefixo 0b) são C23/C++14; use hex (0x) ou decimal em C mais antigo.

c
#include <stdio.h>

int main() {
    unsigned int a = 0b1100;  // 12
    unsigned int b = 0b1010;  // 10

    // AND: both bits must be 1
    printf("%u\n", a & b);   // 8  (0b1000)

    // OR: either bit is 1
    printf("%u\n", a | b);   // 14 (0b1110)

    // XOR: bits differ (exclusive or)
    printf("%u\n", a ^ b);   // 6  (0b0110)

    // NOT: flip all bits
    printf("%u\n", ~a);      // 4294967283 (on 32-bit)

    // Left shift: multiply by 2^n
    printf("%u\n", a << 2);  // 48 (12 * 4)

    // Right shift: divide by 2^n (unsigned)
    printf("%u\n", a >> 1);  // 6  (12 / 2)

    return 0;
}

Definindo, Limpando & Alternando Bits

Bit flags empacotam múltiplas opções booleanas em um único inteiro, economizando memória. As três operações principais: SET (|= mask), CLEAR (&= ~mask), TOGGLE (^= mask), CHECK (& mask). Use #define com (1 << n) para nomes de flags legíveis. Esse padrão é onipresente em programação de sistemas (permissões de arquivo, controle de dispositivos, opções de configuração). Por exemplo, permissões de arquivo Unix (rwxr-xr-x = 0755) usam bit flags. Sempre use inteiros unsigned para flags para evitar problemas de sign-extension. Isso é mais eficiente em memória que um array de bools (1 bit vs 8 bits por flag).

c
#include <stdio.h>

// Flag definitions (powers of 2)
#define FLAG_READ    (1 << 0)  // 0b0001
#define FLAG_WRITE   (1 << 1)  // 0b0010
#define FLAG_EXECUTE (1 << 2)  // 0b0100
#define FLAG_ADMIN   (1 << 3)  // 0b1000

int main() {
    unsigned int permissions = 0;

    // SET a bit (OR with mask)
    permissions |= FLAG_READ | FLAG_WRITE;  // 0b0011

    // CHECK if a bit is set (AND, compare to 0)
    if (permissions & FLAG_READ) {
        printf("Read permission granted\n");
    }

    // CLEAR a bit (AND with inverted mask)
    permissions &= ~FLAG_WRITE;  // 0b0001

    // TOGGLE a bit (XOR with mask)
    permissions ^= FLAG_EXECUTE;  // 0b0101 (execute now on)
    permissions ^= FLAG_EXECUTE;  // 0b0001 (execute now off)

    // SET multiple bits at once
    permissions = FLAG_READ | FLAG_EXECUTE | FLAG_ADMIN;

    printf("Permissions: 0x%X\n", permissions);  // 0xD
    return 0;
}

Truques de Manipulação de Bits

Truques de bits exploram a representação binária para velocidade. x & 1 testa par/ímpar (mais rápido que módulo). x & (x-1) limpa o bit definido mais baixo—útil para verificar potências de 2 e contar bits. __builtin_popcount (GCC/Clang) ou __popcnt (MSVC) contam bits definidos em uma instrução em CPUs modernas. XOR swap (a^=b; b^=a; a^=b) evita uma variável temporária, mas é mais lento em CPUs modernas e menos legível—evite-o. O truque de 'arredondar para cima para potência de 2' propaga o bit definido mais alto para todos os bits inferiores, depois adiciona 1. Esses truques são úteis em sistemas embarcados, game engines e código crítico de desempenho.

c
#include <stdio.h>

int main() {
    int x = 42;

    // Check if odd/even (faster than x % 2)
    if (x & 1) printf("odd\n"); else printf("even\n");

    // Check if power of 2 (only one bit set)
    // x & (x-1) clears the lowest set bit
    if (x && !(x & (x - 1))) printf("power of 2\n");

    // Count set bits (popcount / Hamming weight)
    unsigned int n = 0b10110110;
    int count = 0;
    while (n) { count += n & 1; n >>= 1; }
    printf("Set bits: %d\n", count);  // 6
    // Or use __builtin_popcount(n) (GCC/Clang)

    // Swap two values without temp (XOR swap)
    int a = 5, b = 10;
    a ^= b; b ^= a; a ^= b;
    // a=10, b=5 (avoid in practice — less readable)

    // Get lowest set bit
    unsigned int lowest = x & (-x);  // isolates lowest 1-bit

    // Round up to next power of 2
    unsigned int v = 5;
    v--; v |= v >> 1; v |= v >> 2; v |= v >> 4; v |= v >> 8; v |= v >> 16; v++;

    return 0;
}

Bit Fields em Structs

Bit fields empacotam múltiplos valores pequenos em uma única struct, economizando memória. A sintaxe de dois pontos (unsigned int field : N) especifica a largura do bit. O compilador lida com extração/inserção de bits automaticamente. Isso é útil para sistemas com restrição de memória, protocolos de rede e mapeamento de registradores de hardware. No entanto, o layout de bit fields é implementation-defined (ordem de bytes, alinhamento, padding)—não use bit fields para compatibilidade binária multiplataforma. Use mascaramento de bits explícito (#define + & |) para formatos binários portáveis. Campos sem nome (: 5) adicionam padding. O tamanho total é arredondado para o alinhamento da struct.

c
#include <stdio.h>

// Bit fields: pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31  (5 bits)
    unsigned int month : 4;   // 0-15  (4 bits)
    unsigned int year  : 12;  // 0-4095 (12 bits)
    unsigned int is_leap : 1; // 0 or 1 (1 bit)
};  // Total: 22 bits (padded to 32)

struct Flags {
    unsigned int visible  : 1;
    unsigned int editable : 1;
    unsigned int locked   : 1;
    unsigned int          : 5;  // unnamed padding (5 bits)
    unsigned int priority : 4;  // 0-15
};

int main() {
    struct Date d = { 15, 6, 2024, 0 };
    printf("Size: %zu bytes\n", sizeof(d));  // 4 bytes
    printf("Date: %u/%u/%u\n", d.day, d.month, d.year);

    struct Flags f = { .visible = 1, .editable = 0, .locked = 1, .priority = 7 };
    printf("Size: %zu bytes\n", sizeof(f));  // 4 bytes

    return 0;
}

Manipulação Prática de Bits (Cor RGB)

Empacotar múltiplos valores em um único inteiro é comum em gráficos, rede e sistemas embarcados. Cores RGB empacotam três canais de 8 bits em 24 bits (0xRRGGBB). Shift left (<<) posiciona cada canal, OR (|) os combina. Shift right (>>) e mascaramento (& 0xFF) extraem canais individuais. Isso economiza memória (1 int vs 3 bytes) e permite operações atômicas. O mesmo padrão se aplica à ordenação de bytes de rede, acesso a registradores de hardware e compressão de dados. Sempre use tipos de largura fixa (uint8_t, uint32_t) para portabilidade—o tamanho de int varia por plataforma.

c
#include <stdio.h>
#include <stdint.h>

// Pack RGB into a single 32-bit integer (0xRRGGBB)
uint32_t make_color(uint8_t r, uint8_t g, uint8_t b) {
    return ((uint32_t)r << 16) | ((uint32_t)g << 8) | b;
}

// Extract components
uint8_t get_red(uint32_t color)   { return (color >> 16) & 0xFF; }
uint8_t get_green(uint32_t color) { return (color >> 8)  & 0xFF; }
uint8_t get_blue(uint32_t color)  { return color & 0xFF; }

// Blend two colors (50/50 mix)
uint32_t blend(uint32_t c1, uint32_t c2) {
    uint8_t r = (get_red(c1) + get_red(c2)) / 2;
    uint8_t g = (get_green(c1) + get_green(c2)) / 2;
    uint8_t b = (get_blue(c1) + get_blue(c2)) / 2;
    return make_color(r, g, b);
}

int main() {
    uint32_t red   = make_color(255, 0, 0);    // 0xFF0000
    uint32_t blue  = make_color(0, 0, 255);    // 0x0000FF
    uint32_t purple = blend(red, blue);          // 0x7F007F

    printf("Red:   0x%06X\n", red);
    printf("Blue:  0x%06X\n", blue);
    printf("Mix:   0x%06X (R=%d G=%d B=%d)\n",
           purple, get_red(purple), get_green(purple), get_blue(purple));

    return 0;
}
12

Tratamento de Sinais

Tratamento Básico de Sinais

Sinais são interrupções de software enviadas a um processo (ex.: Ctrl+C envia SIGINT, divisão por zero envia SIGFPE). signal() registra uma função handler. Dentro de um handler, apenas funções async-signal-safe são permitidas—printf, malloc e a maioria das funções stdlib NÃO são seguras porque o programa principal pode ser interrompido no meio de uma chamada. Use write() para saída. Sinais comuns: SIGINT (Ctrl+C), SIGTERM (pedido de terminação), SIGKILL (kill forçado, não pode ser capturado), SIGSEGV (segfault), SIGALRM (timer). Prefira sigaction() a signal() para portabilidade e controle.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

// Signal handler function (must match signature)
void handler(int sig) {
    // WARNING: only async-signal-safe functions allowed here!
    // printf is NOT safe — use write() instead
    const char *msg = "Caught SIGINT\n";
    write(STDOUT_FILENO, msg, 14);
}

int main() {
    // Register handler for Ctrl+C (SIGINT)
    signal(SIGINT, handler);

    // Ignore SIGINT entirely
    // signal(SIGINT, SIG_IGN);

    // Reset to default behavior (terminate)
    // signal(SIGINT, SIG_DFL);

    printf("PID %d waiting. Press Ctrl+C...\n", getpid());
    while (1) {
        sleep(1);
    }
    return 0;
}

sigaction (Tratamento Portátil de Sinais)

sigaction() é a maneira moderna e portátil de tratar sinais (o comportamento de signal() varia por plataforma). O handler sa_sigaction recebe um siginfo_t com detalhes: si_pid (PID do remetente), si_uid (UID do remetente), si_signo (número do sinal), si_code (motivo). A flag SA_SIGINFO habilita o handler de três argumentos. sa_mask bloqueia sinais especificados durante a execução do handler (previne interrupções aninhadas). Outras flags: SA_RESTART (reinicia syscalls interrompidas automaticamente), SA_NOCLDWAIT (sem processos zumbi). Sempre use sigaction() em código de produção—signal() é não confiável em algumas plataformas.

c
#include <stdio.h>
#include <signal.h>
#include <string.h>

void handler(int sig, siginfo_t *info, void *context) {
    // siginfo_t provides details about the signal
    printf("Signal %d from PID %d\n", sig, info->si_pid);
}

int main() {
    struct sigaction sa;
    memset(&sa, 0, sizeof(sa));

    sa.sa_sigaction = handler;  // use sa_sigaction (not sa_handler)
    sa.sa_flags = SA_SIGINFO;   // enable siginfo_t parameter

    // Block other signals during handler execution
    sigemptyset(&sa.sa_mask);
    sigaddset(&sa.sa_mask, SIGQUIT);  // block SIGQUIT during handler

    // Register (more portable than signal())
    sigaction(SIGINT, &sa, NULL);

    // Send a signal to self
    raise(SIGINT);  // like kill(getpid(), SIGINT)

    printf("Done\n");
    return 0;
}

Enviando Sinais & Alarm

alarm(seconds) agenda um SIGALRM após o tempo especificado—útil para timeouts. pause() bloqueia até que qualquer sinal chegue. volatile sig_atomic_t é a única maneira segura de compartilhar dados entre um handler de sinal e o código principal—volatile previne otimização do compilador, sig_atomic_t garante acesso atômico. kill(pid, signal) envia um sinal a outro processo. raise(sig) envia um sinal a si mesmo. SIGKILL (9) e SIGSTOP não podem ser capturados ou ignorados—eles sempre funcionam. SIGTERM (15) é o pedido de terminação educado (programas podem capturá-lo para limpar). Use alarm() para timeouts simples; use setitimer()/timer_create() para mais controle.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>

volatile sig_atomic_t got_alarm = 0;

void alarm_handler(int sig) {
    got_alarm = 1;  // safe: sig_atomic_t is atomic
}

int main() {
    signal(SIGALRM, alarm_handler);

    // Set a timer: deliver SIGALRM after 3 seconds
    alarm(3);
    printf("Waiting for alarm...\n");

    // Wait for the alarm
    while (!got_alarm) {
        pause();  // sleep until any signal arrives
    }
    printf("Alarm fired!\n");

    // Send signal to another process
    // kill(pid, SIGTERM);  // request termination
    // kill(pid, SIGKILL);  // force kill (can't be caught)

    // Send signal to self
    raise(SIGUSR1);

    return 0;
}

Referência de Sinais Comuns

Entender sinais é essencial para programação Unix. SIGKILL (9) e SIGSTOP não podem ser capturados—são o último recurso. SIGTERM é o sinal padrão de desligamento gracioso (capture-o para salvar estado). SIGINT é Ctrl+C (interrupção interativa). SIGCHLD dispara quando um filho sai—se você não fizer wait() por ele, o filho se torna um zumbi. Definir SIGCHLD como SIG_IGN faz auto-reap dos filhos (ou use SA_NOCLDWAIT). SIGPIPE dispara ao escrever em um pipe/socket fechado—a maioria dos servidores o ignora (signal(SIGPIPE, SIG_IGN)) e verifica valores de retorno de write() em vez disso. Use _exit() (não exit()) em handlers de sinal—exit() executa handlers atexit que podem não ser signal-safe.

c
#include <signal.h>

// Common POSIX signals:
// SIGINT  (2)  - Ctrl+C interrupt (terminate)
// SIGQUIT (3)  - Ctrl+\ quit (core dump)
// SIGKILL (9)  - Force kill (CANNOT be caught/ignored)
// SIGSEGV (11) - Segmentation fault (invalid memory access)
// SIGPIPE (13) - Write to broken pipe
// SIGTERM (15) - Termination request (graceful shutdown)
// SIGSTOP (19) - Pause process (CANNOT be caught/ignored)
// SIGCONT (18) - Resume paused process
// SIGCHLD (17) - Child process exited
// SIGALRM (14) - Timer alarm
// SIGUSR1 (10) - User-defined signal 1
// SIGUSR2 (12) - User-defined signal 2
// SIGFPE  (8)  - Arithmetic error (divide by zero)
// SIGBUS  (7)  - Bus error (misaligned access)

// Graceful shutdown pattern:
void cleanup_handler(int sig) {
    // Save state, close files, release resources
    // Then exit cleanly
    _exit(0);  // use _exit in signal handlers (not exit)
}

// In main:
signal(SIGTERM, cleanup_handler);
signal(SIGINT, cleanup_handler);

// Prevent zombie children:
signal(SIGCHLD, SIG_IGN);  // auto-reap children

Truque Self-Pipe (Wakeup Signal-Safe)

O truque self-pipe resolve um problema fundamental: handlers de sinal não podem fazer trabalho complexo com segurança, mas você precisa responder a sinais no loop principal. A solução: o handler escreve um byte em um pipe, e o loop principal usa select()/poll() para detectá-lo. Isso integra sinais com o event loop com segurança. O handler só chama write() (async-signal-safe). Alternativas modernas: signalfd() (específico do Linux, transforma sinais em file descriptors diretamente) ou pselect() (bloqueia sinais atomicamente durante select). Esse padrão é usado em servidores event-driven (nginx, Redis) para tratar sinais sem race conditions.

c
#include <stdio.h>
#include <signal.h>
#include <unistd.h>
#include <fcntl.h>

int pipe_fd[2];  // [0]=read, [1]=write

void handler(int sig) {
    // Write one byte to the pipe — wakes up select()/poll()
    write(pipe_fd[1], &sig, sizeof(sig));
}

int main() {
    pipe(pipe_fd);
    // Make read end non-blocking
    fcntl(pipe_fd[0], F_SETFL, O_NONBLOCK);

    signal(SIGINT, handler);
    signal(SIGTERM, handler);

    printf("Waiting (select-based)...\n");

    while (1) {
        fd_set readfds;
        FD_ZERO(&readfds);
        FD_SET(pipe_fd[0], &readfds);

        // select() blocks until pipe is writable (signal received)
        int ready = select(pipe_fd[0] + 1, &readfds, NULL, NULL, NULL);
        if (ready > 0 && FD_ISSET(pipe_fd[0], &readfds)) {
            int sig;
            read(pipe_fd[0], &sig, sizeof(sig));
            printf("Handled signal %d in main loop\n", sig);
            if (sig == SIGTERM) break;
        }
    }
    return 0;
}
13

Process Fork & Exec

Básico do fork()

fork() cria uma cópia exata do processo atual—a única diferença é o valor de retorno: 0 no filho, PID do filho no pai. Ambos os processos continuam a partir da chamada fork(). O filho recebe uma cópia da memória do pai (copy-on-write otimiza isso). Sempre verifique todos os três casos: pid < 0 (erro), pid == 0 (filho), pid > 0 (pai). waitpid() bloqueia até que o filho saia e recupera seu status. WIFEXITED verifica se saiu normalmente, WEXITSTATUS obtém o código de saída. Se você não fizer wait(), o filho se torna um zumbi até ser reaped.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();  // create a child process

    if (pid < 0) {
        perror("fork failed");
        return 1;
    } else if (pid == 0) {
        // CHILD process (fork returned 0)
        printf("Child: PID=%d, Parent PID=%d\n",
               getpid(), getppid());
        sleep(2);
        printf("Child exiting\n");
        return 42;  // child exit code
    } else {
        // PARENT process (fork returned child's PID)
        printf("Parent: PID=%d, Child PID=%d\n",
               getpid(), pid);

        int status;
        waitpid(pid, &status, 0);  // wait for child

        if (WIFEXITED(status)) {
            printf("Child exited with code %d\n",
                   WEXITSTATUS(status));
        }
    }
    return 0;
}

Família exec (Substituir Imagem do Processo)

exec substitui a imagem do processo atual por um novo programa—o PID permanece o mesmo, mas o código, dados e stack são substituídos. exec só retorna em caso de falha. A convenção de nomenclatura: 'l' = lista de argumentos (variádico, terminado em NULL), 'v' = vetor/array de argumentos, 'p' = procura no PATH pelo executável, 'e' = ambiente personalizado. O primeiro argumento é, por convenção, o nome do programa (argv[0]). fork()+exec() é a maneira Unix de lançar programas—fork cria o processo, exec carrega o novo programa. Essa separação permite configurar file descriptors, ambiente e sinais entre fork e exec.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: replace self with a new program
        // exec never returns on success (only on failure)

        // execlp: search PATH, list arguments
        execlp("ls", "ls", "-la", "/tmp", NULL);

        // execvp: search PATH, array of arguments
        char *args[] = {"ls", "-la", "/tmp", NULL};
        execvp("ls", args);

        // execl: full path, list arguments
        execl("/bin/ls", "ls", "-la", NULL);

        // Only reached if exec failed
        perror("exec failed");
        _exit(1);
    } else {
        wait(NULL);  // parent waits for child
        printf("Child finished\n");
    }
    return 0;
}

// exec variants:
// execl  (path, arg1, arg2, ..., NULL)        — list args, full path
// execlp (file, arg1, arg2, ..., NULL)        — list args, search PATH
// execv  (path, argv[])                        — array args, full path
// execvp (file, argv[])                        — array args, search PATH
// execve (path, argv[], envp[])                — array args, custom env

Processos Zumbi & Órfãos

Zumbis ocorrem quando um filho sai, mas o pai não chamou wait()—o kernel mantém a entrada da tabela de processos (PID, status de saída) até ser reaped. Zumbis desperdiçam PIDs e podem esgotar a tabela de processos. Correção: sempre faça wait() pelos filhos, ou defina SIGCHLD como SIG_IGN (o kernel faz auto-reap). Órfãos ocorrem quando o pai sai antes do filho—init/systemd (PID 1) adota o órfão e o reaps quando ele sai. O padrão double-fork (fork, filho faz fork novamente, primeiro filho sai) cria um daemon que é automaticamente reparented para init, desvinculando do terminal. Monitore zumbis com 'ps aux | grep Z' ou 'top'.

c
#include <stdio.h>
#include <unistd.h>
#include <sys/wait.h>

int main() {
    pid_t pid = fork();

    if (pid == 0) {
        printf("Child PID=%d\n", getpid());
        _exit(0);  // child exits immediately
    }

    // If parent doesn't wait(), child becomes a ZOMBIE
    // (process table entry remains until reaped)
    sleep(5);  // parent sleeps — child is now a zombie
    // Run 'ps' during this window to see the zombie (state 'Z')

    // Reap the zombie:
    int status;
    waitpid(pid, &status, 0);
    printf("Zombie reaped\n");

    // Orphan: if parent exits before child
    pid_t pid2 = fork();
    if (pid2 == 0) {
        sleep(3);  // parent will exit first
        printf("Orphan adopted by init (PID 1), new parent=%d\n",
               getppid());
        _exit(0);
    }
    // Parent exits immediately — child becomes orphan
    // init/systemd (PID 1) adopts and reaps it

    // Prevent zombies: ignore SIGCHLD
    // signal(SIGCHLD, SIG_IGN);  // kernel auto-reaps children
    // Or use SA_NOCLDWAIT with sigaction

    return 0;
}

Criação de Processo Daemon

Daemons são processos em segundo plano que rodam sem um terminal (ex.: servidores web, bancos de dados). As etapas de daemonização: fork+exit para desvincular do shell, setsid() para criar uma nova sessão (sem terminal de controle), fork novamente por segurança, chdir('/') para evitar segurar um sistema de arquivos, set umask para permissões de arquivo previsíveis, e feche/redirecione stdio para /dev/null. O double-fork é uma convenção Unix para prevenir que o daemon readquira um terminal via open(). Sistemas modernos fornecem arquivos de serviço systemd para gerenciamento de daemons, mas entender daemonização manual ainda é importante para sistemas embarcados e código portátil. Faça log para arquivos (não stdout), já que stdout é /dev/null.

c
#include <stdio.h>
#include <stdlib.h>
#include <unistd.h>
#include <sys/stat.h>
#include <fcntl.h>

void daemonize() {
    // 1. Fork and exit parent (child continues in background)
    pid_t pid = fork();
    if (pid > 0) exit(0);  // parent exits
    if (pid < 0) exit(1);

    // 2. Create new session (detach from controlling terminal)
    setsid();

    // 3. Fork again (prevent reacquiring a terminal)
    pid = fork();
    if (pid > 0) exit(0);
    if (pid < 0) exit(1);

    // 4. Change working directory to / (don't hold filesystem)
    chdir("/");

    // 5. Set umask to 0 (full control over file permissions)
    umask(0);

    // 6. Close standard file descriptors (detach from terminal)
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    // 7. Redirect them to /dev/null (in case code writes to them)
    open("/dev/null", O_RDWR);  // fd 0 = stdin
    dup(0);                      // fd 1 = stdout
    dup(0);                      // fd 2 = stderr
}

int main() {
    daemonize();
    // Now running as a daemon (background, no terminal)
    while (1) {
        // Daemon work here (e.g., log to file, listen on socket)
        sleep(60);
    }
    return 0;
}

Visão Geral de Comunicação Inter-Processos (IPC)

IPC permite que processos se comuniquem. Pipes são os mais simples (pai-filho, unidirecional). Named pipes (FIFOs) funcionam entre processos não relacionados via um caminho de sistema de arquivos. Memória compartilhada é a mais rápida (zero-copy), mas exige sincronização (semáforos/mutexes). Sockets são os mais flexíveis (bidirecionais, com capacidade de rede). Message queues fornecem comunicação estruturada com limites de mensagem. Sinais são mínimos (apenas um número). Escolha com base nas suas necessidades: pipes para pai-filho simples, memória compartilhada para compartilhamento de dados de alto desempenho, sockets para comunicação de rede. System V IPC (shmget, semget) é mais antigo; POSIX IPC (shm_open, sem_open) é mais limpo, mas menos universalmente disponível.

c
#include <stdio.h>
// C provides several IPC mechanisms:

// 1. PIPES: unidirectional byte stream between parent/child
//    pipe(fd) creates fd[0]=read, fd[1]=write
//    Only works between related processes (fork)

// 2. NAMED PIPES (FIFOs): like pipes but have a filesystem path
//    mkfifo("/tmp/myfifo", 0666);
//    Works between unrelated processes

// 3. SHARED MEMORY: fastest IPC (both processes access same RAM)
//    shmget/shmat (System V) or shm_open/mmap (POSIX)

// 4. MESSAGE QUEUES: structured messages (not byte stream)
//    msgget/msgsnd/msgrcv (System V) or mq_open (POSIX)

// 5. SEMAPHORES: synchronization (not data transfer)
//    semget/semop (System V) or sem_open (POSIX)

// 6. SOCKETS: bidirectional, works across machines (network)
//    socket/bind/listen/accept/connect

// 7. SIGNALS: minimal data (just signal number)
//    kill(pid, SIGUSR1)

// Choosing IPC:
// - Same machine, related processes → pipes
// - Same machine, unrelated processes → named pipes, shared memory
// - Different machines → sockets
// - Need synchronization → semaphores, mutexes
// - Need structured messages → message queues
14

Pipes & IPC

Pipes Anônimos (Pai-Filho)

Pipes fornecem comunicação unidirecional entre processos relacionados (criados por fork). pipe(fd) cria dois file descriptors: fd[0] para leitura, fd[1] para escrita. Crítico: feche a extremidade não utilizada em cada processo—o pai fecha a extremidade de leitura, o filho fecha a extremidade de escrita. Se a extremidade de escrita não for fechada, o read() do filho bloqueia para sempre (esperando por mais dados). read() retorna 0 (EOF) apenas quando todas as extremidades de escrita são fechadas. Pipes têm um buffer fixo (tipicamente 64KB)—write() bloqueia se o buffer estiver cheio. Pipes são ideais para comunicação pai-filho e encadeamento de comandos do shell (ls | grep).

c
#include <stdio.h>
#include <unistd.h>
#include <string.h>
#include <sys/wait.h>

int main() {
    int fd[2];  // fd[0]=read end, fd[1]=write end
    pipe(fd);   // create pipe

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: read from pipe
        close(fd[1]);  // close unused write end

        char buf[256];
        int n = read(fd[0], buf, sizeof(buf));
        buf[n] = '\0';
        printf("Child received: %s", buf);

        close(fd[0]);
    } else {
        // PARENT: write to pipe
        close(fd[0]);  // close unused read end

        const char *msg = "Hello from parent!\n";
        write(fd[1], msg, strlen(msg));

        close(fd[1]);  // close write end → child's read returns 0 (EOF)
        wait(NULL);
    }
    return 0;
}

Named Pipes (FIFOs)

Named pipes (FIFOs) são pipes com um nome de sistema de arquivos—funcionam entre processos não relacionados. mkfifo() cria o arquivo de pipe; open() bloqueia até que tanto um leitor quanto um escritor estejam conectados (sincronização integrada). FIFOs persistem até unlink()'d (diferente de pipes anônimos que somem quando os processos saem). Eles são úteis para IPC simples entre programas separados. O comportamento de bloqueio em open() garante que o escritor não comece até que um leitor esteja pronto. Use O_NONBLOCK para aberturas não bloqueantes. FIFOs são unidirecionais—para comunicação bidirecional, use dois FIFOs ou sockets. Named pipes são comumente usados em scripts shell e serviços do sistema.

c
#include <stdio.h>
#include <fcntl.h>
#include <sys/stat.h>
#include <unistd.h>
#include <string.h>

// Process A (writer):
int writer_main() {
    mkfifo("/tmp/myfifo", 0666);  // create named pipe

    int fd = open("/tmp/myfifo", O_WRONLY);
    write(fd, "Hello via FIFO!", 15);
    close(fd);
    return 0;
}

// Process B (reader) — can be a completely separate program:
int reader_main() {
    int fd = open("/tmp/myfifo", O_RDONLY);
    char buf[256];
    int n = read(fd, buf, sizeof(buf));
    buf[n] = '\0';
    printf("Received: %s\n", buf);
    close(fd);
    return 0;
}

// Named pipes persist in the filesystem (use unlink to remove):
// unlink("/tmp/myfifo");

// open() blocks until BOTH a reader and writer are connected
// (unless O_NONBLOCK is used)

Memória Compartilhada (IPC Mais Rápido)

Memória compartilhada é o IPC mais rápido—ambos os processos mapeiam a mesma RAM física, então a transferência de dados é zero-copy. shmget() cria um segmento, shmat() o anexa ao espaço de endereço do processo, shmdt() desanexa, shmctl(IPC_RMID) destrói. A ressalva crítica: memória compartilhada NÃO fornece sincronização—se ambos os processos acessarem simultaneamente, você terá data races. Você DEVE usar semáforos, mutexes ou outra sincronização para coordenar o acesso. ftok() gera uma chave a partir de um caminho de arquivo (ambos os processos devem concordar com a chave). Sempre destrua a memória compartilhada ao terminar (ela persiste após os processos saírem, vazando memória). Memória compartilhada POSIX (shm_open/mmap) é uma alternativa mais limpa.

c
#include <stdio.h>
#include <sys/shm.h>
#include <sys/ipc.h>
#include <string.h>
#include <unistd.h>

#define SHM_SIZE 1024

int main() {
    key_t key = ftok("/tmp/shmfile", 65);  // generate unique key

    // Create shared memory segment
    int shmid = shmget(key, SHM_SIZE, 0666 | IPC_CREAT);

    pid_t pid = fork();

    if (pid == 0) {
        // CHILD: attach and read
        char *shared = (char *)shmat(shmid, NULL, 0);
        sleep(1);  // wait for parent to write
        printf("Child reads: %s\n", shared);
        shmdt(shared);  // detach
    } else {
        // PARENT: attach and write
        char *shared = (char *)shmat(shmid, NULL, 0);
        strcpy(shared, "Hello from shared memory!");
        printf("Parent wrote to shared memory\n");
        shmdt(shared);
        wait(NULL);

        // Destroy shared memory after use
        shmctl(shmid, IPC_RMID, NULL);
    }
    return 0;
}

// WARNING: shared memory has NO synchronization!
// Use semaphores or mutexes to prevent race conditions.

dup2 & Redirecionamento

dup2(oldfd, newfd) faz de newfd uma cópia de oldfd—é assim que o redirecionamento do shell funciona. Para redirecionar stdout para um pipe: dup2(pipe_write, STDOUT_FILENO)—agora printf/write para stdout vai para o pipe. Para redirecionar stdin a partir de um pipe: dup2(pipe_read, STDIN_FILENO)—agora scanf/read de stdin vem do pipe. É exatamente assim que o shell implementa pipes (ls | sort), redirecionamento (ls > file) e entrada (sort < file). Após dup2, feche o fd original (ele foi duplicado). Esse padrão é fundamental para construir pipelines Unix programaticamente e é usado por shells, popen() e bibliotecas de gerenciamento de processos.

c
#include <stdio.h>
#include <fcntl.h>
#include <unistd.h>
#include <sys/wait.h>

// Implement shell-like pipeline: ls | sort
int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid1 = fork();
    if (pid1 == 0) {
        // First child: ls (writes to pipe instead of stdout)
        close(pipefd[0]);                    // close read end
        dup2(pipefd[1], STDOUT_FILENO);      // stdout → pipe write
        close(pipefd[1]);                    // close original (dup'd)
        execlp("ls", "ls", NULL);
    }

    pid_t pid2 = fork();
    if (pid2 == 0) {
        // Second child: sort (reads from pipe instead of stdin)
        close(pipefd[1]);                    // close write end
        dup2(pipefd[0], STDIN_FILENO);       // stdin → pipe read
        close(pipefd[0]);                    // close original (dup'd)
        execlp("sort", "sort", NULL);
    }

    // Parent: close both ends and wait
    close(pipefd[0]);
    close(pipefd[1]);
    waitpid(pid1, NULL, 0);
    waitpid(pid2, NULL, 0);

    return 0;
}

popen (Pipe de Alto Nível)

popen() é um wrapper de alto nível em torno de fork+pipe+exec+shell—executa um comando via /bin/sh e retorna um FILE* para leitura de saída ('r') ou escrita de entrada ('w'). É muito mais simples que fork/pipe/exec manual, mas executa através de um shell, então NUNCA passe entrada não confiável (risco de injeção de shell). Use fgets/fprintf no FILE* retornado como um arquivo regular. pclose() fecha o pipe e espera o filho sair (retorna seu status). Para entrada não confiável, use fork+execvp diretamente (sem shell). popen é perfeito para scripts rápidos, ferramentas de administração do sistema e leitura de saída de comandos. Para comunicação bidirecional, use socketpair() ou dois pipes.

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

int main() {
    // popen opens a process with a pipe (like shell command | ...)
    FILE *fp = popen("ls -la /tmp", "r");
    if (fp == NULL) {
        perror("popen failed");
        return 1;
    }

    // Read command output line by line
    char buf[256];
    while (fgets(buf, sizeof(buf), fp) != NULL) {
        printf(">> %s", buf);
    }

    pclose(fp);  // closes pipe and waits for child

    // Writing to a process (like ... | command):
    FILE *wp = popen("grep hello", "w");
    fprintf(wp, "hello world\n");
    fprintf(wp, "goodbye\n");
    pclose(wp);  // grep outputs "hello world"

    return 0;
}

// popen is simpler than fork+pipe+exec but:
// - Runs via /bin/sh (shell injection risk with user input!)
// - Less control over the child process
// - Use fork+exec directly for untrusted input
15

Makefile & Ferramentas de Build

Estrutura Básica do Makefile

Make automatiza a compilação. Um Makefile tem regras: target (arquivo a construir), prerequisites (dependências) e recipe (comandos shell, indentados com TAB). Variáveis (CC, CFLAGS) centralizam a configuração. Variáveis automáticas: $@ (nome do target), $< (primeiro pré-requisito), $^ (todos os pré-requisitos). Pattern rules (%.o: %.c) generalizam a compilação para todos os arquivos fonte. .PHONY declara targets que não são arquivos (clean, all, install). A primeira regra é o padrão (make sem argumentos constrói 'all'). Make rastreia timestamps de arquivos—só reconstrói se um pré-requisito for mais novo que o target. Esse building incremental economiza tempo em projetos grandes.

c
# Makefile — build automation for C/C++ projects
# Rule syntax: target: prerequisites
#               recipe (must start with TAB, not spaces)

# Variables
CC = gcc
CFLAGS = -Wall -Wextra -g -O2
TARGET = myapp
SRCS = main.c utils.c parser.c
OBJS = $(SRCS:.c=.o)  # substitute .c with .o

# Default target (first rule)
all: $(TARGET)

# Link object files into executable
$(TARGET): $(OBJS)
	$(CC) $(CFLAGS) -o $@ $^

# Compile each .c to .o
%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Clean build artifacts
clean:
	rm -f $(OBJS) $(TARGET)

# Run the program
run: $(TARGET)
	./$(TARGET)

# Phony targets (not files)
.PHONY: all clean run

# Usage:
# $ make        — builds 'all' (the default)
# $ make clean  — removes build files
# $ make run    — builds and runs
# $ make -j4    — parallel build (4 jobs)

Variáveis Automáticas & Pattern Rules

Variáveis automáticas tornam os Makefiles concisos e manuteníveis. $@ (target), $< (primeiro pré-requisito) e $^ (todos os pré-requisitos) são os mais comuns. Pattern rules (%.o: %.c) permitem escrever uma regra para todos os arquivos fonte—o % corresponde a qualquer string. $(wildcard) encontra arquivos correspondentes a um glob, $(patsubst) transforma strings—juntos, eles descobrem fontes automaticamente. O prefixo @ suprime o eco do comando. Static pattern rules (target: %.o: %.c) se aplicam a uma lista específica. Entender esses recursos elimina regras repetitivas e faz os Makefiles escalar para projetos grandes. Sempre use TAB (não espaços) para indentação do recipe—Make é estrito quanto a isso.

c
# Automatic variables in recipes:
# $@   — the target filename
# $<   — the first prerequisite
# $^   — all prerequisites (no duplicates)
# $+   — all prerequisites (with duplicates)
# $?   — prerequisites newer than the target
# $*   — the stem (matching % part)

# Example showing all automatic variables:
program: main.o utils.o
	@echo "Target: $@"        # program
	@echo "First dep: $<"     # main.o
	@echo "All deps: $^"      # main.o utils.o
	@echo "Newer deps: $?"    # (whichever changed)
	gcc -o $@ $^

# Pattern rule: compile any .c to .o
%.o: %.c
	gcc -c $< -o $@
# $< = source (.c file), $@ = target (.o file)

# Static pattern rule (specific files):
$(OBJS): %.o: %.c
	gcc -c $< -o $@

# Built-in functions:
SRCS = $(wildcard src/*.c)           # find all .c files
OBJS = $(patsubst src/%.c,build/%.o,$(SRCS))  # path substitution
DIRS = $(sort $(dir $(SRCS)))         # unique directories

Dependências & Arquivos de Header

Rastrear dependências de header é crucial—sem isso, alterar um arquivo .h não dispara a recompilação dos arquivos .c que o incluem, levando a builds obsoletos. A solução: gcc -MMD -MP gera arquivos .d listando todas as dependências (incluindo headers). -include puxa esses arquivos para o Makefile. -MP adiciona targets fictícios para headers (evita erros se um header for excluído). Essa é a abordagem padrão para projetos C/C++. Sem isso, você teria que listar manualmente cada dependência de header—gerenciável para projetos grandes. O primeiro build não terá arquivos .d (o '-' em -include suprime o erro); eles são criados durante a compilação e usados em builds subsequentes.

c
# When a header file changes, dependent .c files must recompile
# Make doesn't track this automatically — use gcc -MMD

CC = gcc
CFLAGS = -Wall -MMD -MP  # generate .d dependency files

SRCS = $(wildcard src/*.c)
OBJS = $(SRCS:.c=.o)
DEPS = $(OBJS:.o=.d)    # dependency files

all: myapp

myapp: $(OBJS)
	$(CC) -o $@ $^

%.o: %.c
	$(CC) $(CFLAGS) -c $< -o $@

# Include auto-generated dependency files
-include $(DEPS)  # '-' means don't error if missing

clean:
	rm -f $(OBJS) $(DEPS) myapp

.PHONY: all clean

# How it works:
# 1. gcc -MMD creates main.d next to main.o
# 2. main.d contains: main.o: main.c utils.h parser.h
# 3. -include pulls these in, so Make knows header dependencies
# 4. If utils.h changes, main.o rebuilds automatically

Makefile para Projeto Multi-Diretório

Projetos reais abrangem múltiplos diretórios. Este Makefile descobre fontes automaticamente (wildcard), mapeia-as para um diretório de build (patsubst) e cria diretórios conforme necessário. A sintaxe | cria pré-requisitos somente de ordem—$(BUILDDIR) é criado antes da compilação, mas a mudança de seu timestamp não dispara rebuilds (sem |, criar o diretório faria tudo ser reconstruído a cada vez). -Iinclude diz ao gcc onde encontrar headers. -MMD gera arquivos de dependência no diretório de build. Essa estrutura mantém diretórios de fonte, build e binário separados—fácil de limpar (rm -rf build) e não polui a árvore de fontes. Para projetos muito grandes, considere CMake ou Meson.

c
# Project structure:
# project/
#   src/       — source files
#   include/   — header files
#   build/     — object files (created by make)
#   bin/       — final executable

CC = gcc
CFLAGS = -Wall -Iinclude -g
SRCDIR = src
INCDIR = include
BUILDDIR = build
BINDIR = bin

TARGET = $(BINDIR)/myapp
SRCS = $(wildcard $(SRCDIR)/*.c)
OBJS = $(patsubst $(SRCDIR)/%.c,$(BUILDDIR)/%.o,$(SRCS))
DEPS = $(OBJS:.o=.d)

all: $(TARGET)

$(TARGET): $(OBJS) | $(BINDIR)
	$(CC) -o $@ $^

# Order-only prerequisite: create build dir before compiling
$(BUILDDIR)/%.o: $(SRCDIR)/%.c | $(BUILDDIR)
	$(CC) $(CFLAGS) -MMD -c $< -o $@

$(BUILDDIR):
	mkdir -p $(BUILDDIR)

$(BINDIR):
	mkdir -p $(BINDIR)

-include $(DEPS)

clean:
	rm -rf $(BUILDDIR) $(BINDIR)

.PHONY: all clean

# Order-only prerequisites (| syntax) create directories
# without triggering rebuilds when the dir timestamp changes

Básico do CMake (Alternativa ao Make)

CMake é um meta-build system—gera Makefiles (ou projetos Ninja, Visual Studio, Xcode) a partir de um arquivo CMakeLists.txt. É o padrão de fato para projetos C/C++ porque lida com compilação multiplataforma, detecção de dependências e integração com IDE. Comandos principais: project() define o nome do projeto, add_executable() define um target de build, target_include_directories() adiciona caminhos de header, target_link_libraries() linka bibliotecas. Builds out-of-source (mkdir build && cd build && cmake ..) mantêm a árvore de fontes limpa. CMake detecta compiladores e flags automaticamente por plataforma. Para novos projetos C/C++, prefira CMake a Makefiles crus—é mais manutenível e portátil.

c
# CMakeLists.txt — CMake is a cross-platform build generator
# It generates Makefiles (or Ninja, VS, Xcode projects)

cmake_minimum_required(VERSION 3.10)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -Wall -Wextra")

# Add executable from source files
add_executable(myapp src/main.c src/utils.c src/parser.c)

# Include directory
target_include_directories(myapp PRIVATE include)

# Link a library
target_link_libraries(myapp m)  # math library (-lm)

# Build type flags
set(CMAKE_C_FLAGS_DEBUG "-g -O0")
set(CMAKE_C_FLAGS_RELEASE "-O3 -DNDEBUG")

# Usage:
# $ mkdir build && cd build
# $ cmake ..            # generate Makefiles
# $ make                # build
# $ make install        # install (optional)
# $ cmake .. -DCMAKE_BUILD_TYPE=Debug  # debug build

# Out-of-source builds keep source tree clean
# CMake is the standard for C/C++ cross-platform projects
16

Ponteiros de Função Aprofundado

Sintaxe & Uso de Ponteiros de Função

Ponteiros de função armazenam o endereço de uma função, permitindo dispatch em tempo de execução. A sintaxe int (*fp)(int, int) é notoriamente confusa—leia como 'fp é um ponteiro para uma função que recebe (int, int) e retorna int'. typedef simplifica isso: typedef int (*math_func)(int, int) cria um alias legível. Nomes de funções decaem para ponteiros (como nomes de arrays), então 'add' e '&add' são equivalentes. Ponteiros de função permitem callbacks, event handlers, strategy patterns e dispatch tables (arrays de ponteiros de função para dispatch semelhante a switch). Eles são a base do comparador do qsort e de sistemas de eventos de GUI.

c
#include <stdio.h>

// Function pointer syntax: return_type (*name)(param_types)
int add(int a, int b) { return a + b; }
int subtract(int a, int b) { return a - b; }

int main() {
    // Declare a function pointer
    int (*operation)(int, int);

    // Assign (function name decays to pointer)
    operation = add;          // or &add
    printf("5 + 3 = %d\n", operation(5, 3));  // 8

    operation = subtract;
    printf("5 - 3 = %d\n", operation(5, 3));  // 2

    // typedef for readability
    typedef int (*math_func)(int, int);
    math_func fn = add;
    printf("Result: %d\n", fn(10, 20));

    // Array of function pointers (dispatch table)
    math_func ops[] = {add, subtract};
    printf("op[0](4,2)=%d  op[1](4,2)=%d\n",
           ops[0](4, 2), ops[1](4, 2));

    return 0;
}

Callbacks (Exemplo qsort)

qsort é o exemplo clássico de ponteiros de função como callbacks. O comparador recebe ponteiros const void* (genéricos) e retorna um inteiro indicando a ordem. qsort chama seu comparador para decidir a ordenação dos elementos—você controla o comportamento de ordenação passando funções diferentes. Esse é o strategy pattern em C: o algoritmo (qsort) é fixo, mas a lógica de comparação é injetada. void* permite programação genérica (ordenar qualquer tipo). O comparador deve ser uma função pura (sem efeitos colaterais) e consistente (se a<b e b<c então a<c). Esse padrão é usado em toda a biblioteca padrão do C (bsearch, atexit, signal).

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

// Comparator function for qsort
// Returns: negative if a<b, 0 if equal, positive if a>b
int compare_asc(const void *a, const void *b) {
    return (*(int *)a - *(int *)b);
}

int compare_desc(const void *a, const void *b) {
    return (*(int *)b - *(int *)a);
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3, 7, 4, 6};
    int n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer as the comparator
    qsort(arr, n, sizeof(int), compare_asc);
    printf("Ascending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    // Same array, different comparator (descending)
    qsort(arr, n, sizeof(int), compare_desc);
    printf("Descending: ");
    for (int i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");

    return 0;
}

Structs com Ponteiros de Função (OOP em C)

C pode simular OOP usando structs com ponteiros de função—é assim que as vtables do C++ funcionam internamente. A struct contém um ponteiro para uma 'vtable' (tabela de funções virtuais) que armazena ponteiros de função. Cada 'subclasse' (Circle, Square) tem sua própria vtable com suas implementações. Fazer cast de Circle* para Shape* permite polimorfismo—print_shape() chama a função area() correta através da vtable. Esse padrão é usado em código C real: kernel do Linux (drivers de dispositivo), GObject (GTK) e SQLite. Ele fornece encapsulamento, herança (via struct embedding) e polimorfismo. Embora verboso comparado ao C++, dá controle total sobre o layout de memória e o dispatch virtual.

c
#include <stdio.h>

// Simulating OOP with structs + function pointers
typedef struct Shape Shape;

// Virtual function table (vtable)
typedef struct {
    double (*area)(Shape *);
    double (*perimeter)(Shape *);
    void (*describe)(Shape *);
} ShapeVTable;

struct Shape {
    const ShapeVTable *vtable;  // pointer to virtual functions
    char name[32];
};

// Circle implementation
typedef struct {
    Shape base;      // inherit from Shape
    double radius;
} Circle;

double circle_area(Shape *s) {
    return 3.14159 * ((Circle *)s)->radius * ((Circle *)s)->radius;
}

double circle_perimeter(Shape *s) {
    return 2 * 3.14159 * ((Circle *)s)->radius;
}

static const ShapeVTable circle_vtable = {
    circle_area, circle_perimeter, NULL
};

Circle *circle_create(double r) {
    Circle *c = malloc(sizeof(Circle));
    c->base.vtable = &circle_vtable;
    strcpy(c->base.name, "Circle");
    c->radius = r;
    return c;
}

// Polymorphic function (works with any Shape)
void print_shape(Shape *s) {
    printf("%s: area=%.2f, perimeter=%.2f\n",
           s->name, s->vtable->area(s), s->vtable->perimeter(s));
}

Programação Event-Driven com Callbacks

Ponteiros de função permitem arquitetura event-driven em C—o padrão publish/subscribe. Handlers registram via on_event() (subscribe), e emit_event() chama todos os handlers registrados (publish). Isso desacopla produtores de eventos de consumidores—o emissor não sabe o que os handlers fazem. Esse padrão é fundamental para frameworks GUI (clique de botão → handler), game engines (colisão → callback) e I/O assíncrono (dados prontos → handler de leitura). A assinatura do handler (nome do evento + void* data) é genérica o suficiente para qualquer tipo de evento. Em produção, adicione tratamento de erros (e se um handler crashar?), ordenação por prioridade e a capacidade de cancelar inscrição. É assim que libuv, libevent e Node.js funcionam internamente.

c
#include <stdio.h>

// Event system using function pointers
typedef void (*EventHandler)(const char *event, void *data);

// Simple event emitter
#define MAX_HANDLERS 10
static EventHandler handlers[MAX_HANDLERS];
static int handler_count = 0;

void on_event(EventHandler handler) {
    if (handler_count < MAX_HANDLERS) {
        handlers[handler_count++] = handler;
    }
}

void emit_event(const char *event, void *data) {
    for (int i = 0; i < handler_count; i++) {
        handlers[i](event, data);  // call each registered handler
    }
}

// Concrete handlers
void log_handler(const char *event, void *data) {
    printf("[LOG] Event: %s\n", event);
}

void alert_handler(const char *event, void *data) {
    if (strcmp(event, "error") == 0) {
        printf("[ALERT] Error occurred!\n");
    }
}

int main() {
    // Register handlers (subscribe)
    on_event(log_handler);
    on_event(alert_handler);

    // Emit events (publish)
    emit_event("click", NULL);
    emit_event("error", NULL);
    emit_event("scroll", NULL);

    return 0;
}

Armadilhas de Ponteiros de Função

Ponteiros de função têm várias armadilhas. Chamar um ponteiro de função NULL causa crash (segfault)—sempre verifique NULL antes de chamar. Fazer cast para a assinatura errada é comportamento indefinido (a convenção de chamada pode diferir). Comparar ponteiros de função para igualdade é válido (mesma função), mas ordenação (<, >) é indefinida. Use typedef de forma consistente—a sintaxe de ponteiro de função é propensa a erros, e typedefs tornam as declarações legíveis e manuteníveis. Em C, ponteiros de função são a única maneira de alcançar polimorfismo em tempo de execução e callbacks, então dominá-los é essencial. C++ adiciona std::function, lambdas e funções virtuais como alternativas mais seguras.

c
#include <stdio.h>

// PITFALL 1: Calling a NULL function pointer (crash!)
void bad_call() {
    void (*fp)(void) = NULL;
    fp();  // SEGFAULT — always check for NULL
    if (fp) fp();  // safe
}

// PITFALL 2: Wrong signature (undefined behavior)
void takes_int(int x) { printf("%d\n", x); }
void wrong_sig() {
    void (*fp)(void) = (void (*)(void))takes_int;  // WRONG cast
    fp();  // UB: missing argument, garbage value
}

// PITFALL 3: Function pointer to a local function (dangling)
typedef int (*callback_t)(int);
callback_t get_callback() {
    // Returning pointer to local function is OK (functions aren't local)
    // But returning a pointer to a local VARIABLE is not
    return NULL;  // functions have static storage, safe to return
}

// PITFALL 4: Comparing function pointers
int f1(int x) { return x; }
int f2(int x) { return x; }
void compare_fps() {
    int (*p1)(int) = f1;
    int (*p2)(int) = f1;
    if (p1 == p2) printf("Same function\n");  // OK
    // Comparing p1 == f2 is valid but they're different functions
}

// GOOD: Always use typedef for complex function pointers
// typedef int (*comparator_t)(const void *, const void *);
// This makes declarations readable and consistent
17

Argumentos Variáveis (varargs)

Funções Variádicas Básicas (stdarg)

Funções variádicas aceitam um número variável de argumentos usando stdarg.h. va_list contém a lista de argumentos, va_start a inicializa (requer o último parâmetro nomeado antes de ...), va_arg recupera o próximo argumento com um tipo especificado, va_end limpa. A função deve saber quantos argumentos ler—seja via um parâmetro de contagem (como a format string do printf) ou um valor sentinela (terminador NULL). O '...' deve ser sempre o último parâmetro. va_arg não faz verificação de tipo—passar o tipo errado é comportamento indefinido. É assim que printf, fprintf e execl funcionam.

c
#include <stdio.h>
#include <stdarg.h>

// Variadic function: takes variable number of arguments
// The '...' must be the LAST parameter
int sum(int count, ...) {
    va_list args;           // argument list type
    va_start(args, count);  // initialize (needs last named param)

    int total = 0;
    for (int i = 0; i < count; i++) {
        int val = va_arg(args, int);  // get next argument (as int)
        total += val;
    }

    va_end(args);  // cleanup
    return total;
}

int main() {
    printf("%d\n", sum(3, 10, 20, 30));      // 60
    printf("%d\n", sum(5, 1, 2, 3, 4, 5));   // 15
    printf("%d\n", sum(0));                    // 0
    return 0;
}

// The 'count' parameter tells the function how many args follow.
// Without it, the function can't know when to stop.

Implementando um printf Personalizado

vprintf/vfprintf/vsprintf são helpers variádicos que recebem um va_list em vez de ...—eles permitem construir funções personalizadas semelhantes ao printf. O exemplo log_msg envolve printf com um prefixo de nível de log. O exemplo print_values mostra como lidar com tipos mistos: passe uma tag de tipo antes de cada valor, depois faça switch na tag para chamar va_arg com o tipo correto. Isso é necessário porque va_arg exige o tipo exato—não há informação de tipo em tempo de execução. O padrão de type-tag é usado em APIs C polimórficas (ex.: funções bind do SQLite). Sempre corresponda os tipos de va_arg exatamente—int vs long, float vs double (floats são promovidos a double em varargs).

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Custom logging function with format string
void log_msg(const char *level, const char *format, ...) {
    printf("[%s] ", level);

    va_list args;
    va_start(args, format);

    // vprintf: like printf but takes va_list instead of ...
    vprintf(format, args);

    va_end(args);
    printf("\n");
}

// Variadic function with mixed types
void print_values(int count, ...) {
    va_list args;
    va_start(args, count);

    for (int i = 0; i < count; i++) {
        int type = va_arg(args, int);  // type tag
        switch (type) {
            case 0:  // int
                printf("int: %d\n", va_arg(args, int));
                break;
            case 1:  // double
                printf("double: %f\n", va_arg(args, double));
                break;
            case 2:  // string
                printf("string: %s\n", va_arg(args, char *));
                break;
        }
    }
    va_end(args);
}

int main() {
    log_msg("INFO", "User %s logged in from %s", "Alice", "192.168.1.1");
    log_msg("ERROR", "Failed to open %s (code %d)", "config.txt", 13);

    print_values(2, 0, 42, 2, "hello", 1, 3.14);
    return 0;
}

Funções Variádicas Terminadas por Sentinela

Funções variádicas terminadas por sentinela usam um valor especial (geralmente NULL) para marcar o fim dos argumentos, em vez de uma contagem. Isso é mais limpo para APIs com muitas strings—o chamador não precisa contar argumentos. A família exec (execl, execlp) usa NULL como sentinela. A desvantagem: se o chamador esquecer o NULL, a função lê memória lixo (comportamento indefinido). Alguns compiladores (GCC) suportam __attribute__((sentinel)) para avisar sobre sentinelas ausentes. Sempre documente que NULL é necessário. O parâmetro de tamanho do buffer previne buffer overflows—sempre passe o tamanho do destino e verifique os limites antes de strcat.

c
#include <stdio.h>
#include <stdarg.h>
#include <string.h>

// Sentinel-terminated: last argument is NULL
void concat_strings(char *dest, size_t size, ...) {
    va_list args;
    va_start(args, size);

    dest[0] = '\0';  // start with empty string
    size_t used = 0;

    while (1) {
        char *s = va_arg(args, char *);
        if (s == NULL) break;  // sentinel — stop reading

        size_t len = strlen(s);
        if (used + len < size) {
            strcat(dest, s);
            used += len;
        }
    }

    va_end(args);
}

int main() {
    char result[256];
    concat_strings(result, sizeof(result),
                   "Hello, ", "world", "! ", "How are you?", NULL);
    printf("%s\n", result);
    // Hello, world! How are you?

    // Common C APIs using sentinels:
    // execl("/bin/ls", "ls", "-l", NULL);  // exec family
    // sqlite3_exec(db, sql, callback, NULL, NULL);
    return 0;
}

Encaminhando Argumentos Variádicos

Encaminhar argumentos variádicos exige va_copy (não atribuição)—va_list pode ser um tipo opaco que não pode ser copiado com =. va_copy permite percorrer a lista de argumentos várias vezes (ex.: primeiro para medir, depois para imprimir). A macro LOG usa __VA_ARGS__ para encaminhar todos os argumentos para fprintf. A extensão ##__VA_ARGS__ do GCC remove a vírgula precedente quando nenhum argumento variádico é fornecido (então LOG("msg") funciona sem vírgula final). Esse padrão é onipresente em macros de logging em C. C99 exige pelo menos um argumento antes de ...; C11/C23 e GCC permitem zero. Para alternativas type-safe em C++, use templates variádicos ou std::format.

c
#include <stdio.h>
#include <stdarg.h>

// Wrapper that forwards varargs to another function
// Use va_copy for the copy (needed for multiple passes)
void custom_printf(const char *format, ...) {
    va_list args1, args2;
    va_start(args1, format);
    va_copy(args2, args1);  // copy for second use

    // First pass: count characters that would be printed
    int len = vsnprintf(NULL, 0, format, args1);
    printf("[len=%d] ", len);

    // Second pass: actually print
    vprintf(format, args2);

    va_end(args1);
    va_end(args2);
}

// Macro forwarding (common pattern for logging)
#define LOG(fmt, ...) \
    fprintf(stderr, "[LOG] %s:%d: " fmt "\n", \
            __FILE__, __LINE__, ##__VA_ARGS__)

// The ## operator removes the comma if __VA_ARGS__ is empty
// (GCC extension, widely supported)

int main() {
    custom_printf("Value: %d, Name: %s\n", 42, "test");
    LOG("Simple message");           // no extra args
    LOG("With value: %d", 100);      // with args
    return 0;
}

Macros Variádicas (C99)

Macros variádicas do C99 usam __VA_ARGS__ para capturar todos os argumentos correspondentes ao ... na definição da macro. ##__VA_ARGS__ (extensão GCC, agora padrão em C20) remove a vírgula quando nenhum argumento variádico é passado. A macro COUNT usa um truque inteligente: mapeia N argumentos para N, 5, 4, 3, 2, 1 e a N-ésima posição fornece a contagem. O idioma do { ... } while (0) em ASSERT faz a macro se comportar como uma única instrução (segura em if/else sem chaves). # faz stringify de argumentos de macro. Macros variádicas são essenciais para logging, depuração e programação genérica em C. Elas são a base de muitas APIs de biblioteca e do sistema de logging do kernel do Linux.

c
#include <stdio.h>

// C99 variadic macros: __VA_ARGS__ captures all extra args
#define DEBUG_PRINT(fmt, ...) \
    printf("DEBUG: " fmt "\n", ##__VA_ARGS__)

#define MAX(...) (max_of(__VA_ARGS__))

// Assert macro with message
#define ASSERT(cond, fmt, ...) \
    do { \
        if (!(cond)) { \
            fprintf(stderr, "Assertion failed: %s\n" fmt "\n", \
                    #cond, ##__VA_ARGS__); \
            exit(1); \
        } \
    } while (0)

// Count arguments (GCC __VA_OPT__ or recursive macros)
#define COUNT(...) COUNT_N(__VA_ARGS__, 5, 4, 3, 2, 1)
#define COUNT_N(_1, _2, _3, _4, _5, N, ...) N

// Stringification of all args
#define STR(...) #__VA_ARGS__

int main() {
    DEBUG_PRINT("x = %d", 42);
    DEBUG_PRINT("no args");  // ## removes comma

    ASSERT(x > 0, "x was %d", x);

    printf("Count: %d\n", COUNT(a, b, c));  // 3
    printf("Stringified: %s\n", STR(hello, world));  // "hello, world"

    return 0;
}
18

Truques de Manipulação de Bits

Truques de Bits Comuns

Manipulação de bits opera diretamente em representações binárias. n & 1 verifica o bit menos significativo para par/ímpar. Left shift (<<) multiplica por 2; right shift (>>) divide. XOR swap evita uma variável temporária, mas é menos legível. n & (n-1) limpa o bit definido mais baixo, útil para verificações de potência de 2 e popcount. Esses truques são rápidos, mas priorize a legibilidade no código de aplicação.

c
// Check if odd
int is_odd(int n) { return n & 1; }

// Multiply/divide by powers of 2
int doubled = x << 1;    // x * 2
int halved = x >> 1;     // x / 2
int times8 = x << 3;     // x * 8

// Swap without temp variable
void swap(int *a, int *b) {
    *a ^= *b; *b ^= *a; *a ^= *b;
}

// Check if power of 2
int is_pow2(int n) { return n > 0 && (n & (n-1)) == 0; }

// Count set bits (population count)
int popcount(unsigned int n) {
    int count = 0;
    while (n) { n &= (n-1); count++; }
    return count;
}

Bit Flags & Máscaras

Bit flags empacotam múltiplas opções booleanas em um único inteiro, economizando memória. Cada flag é uma potência de 2 (um bit). OR (|) define flags, AND (&) verifica flags, XOR (^) alterna, AND NOT (&= ~) limpa. Esse padrão é onipresente em programação de sistemas: permissões de arquivo (O_RDONLY, O_CREAT), opções de socket e estado de GPU. Use constantes nomeadas para legibilidade.

c
#define FLAG_READ    (1 << 0)  // 0x01
#define FLAG_WRITE   (1 << 1)  // 0x02
#define FLAG_EXECUTE (1 << 2)  // 0x04

// Set flags
unsigned int perms = FLAG_READ | FLAG_WRITE;

// Check if flag is set
if (perms & FLAG_WRITE) { /* write allowed */ }

// Toggle a flag
perms ^= FLAG_EXECUTE;

// Clear a flag
perms &= ~FLAG_APPEND;

// Check if ALL flags in mask are set
int has_all = (perms & (FLAG_READ|FLAG_WRITE))
            == (FLAG_READ|FLAG_WRITE);

Bit Fields em Structs

Bit fields empacotam valores pequenos em um número mínimo de bits dentro de structs. A sintaxe de dois pontos especifica a largura do bit. Isso economiza memória para estruturas de dados com muitos campos pequenos (datas, flags, registradores de hardware). No entanto, o layout de bit fields é implementation-dependent: ordem de bytes, padding e alinhamento variam entre compiladores. Evite bit fields para formatos de dados portáveis; use máscaras de bits explícitas em vez disso.

c
// Pack multiple small fields into one int
struct Date {
    unsigned int day   : 5;   // 0-31 (5 bits)
    unsigned int month : 4;   // 0-15 (4 bits)
    unsigned int year  : 23;  // 0-8M (23 bits)
};  // Total: 32 bits = 4 bytes

struct Date today = { 21, 6, 2025 };
printf("Size: %zu bytes\n", sizeof(today));  // 4

today.day = 15;
if (today.month == 12) {
    today.year++;
    today.month = 1;
}

Conversão de Endianness

Endianness determina a ordem dos bytes: little-endian (x86, ARM padrão) armazena LSB primeiro; big-endian (rede, alguns MIPS) armazena MSB primeiro. Protocolos de rede usam big-endian (ordem de bytes de rede). Use htonl/ntohl para código de rede portátil. Byte swaps manuais com shifts e máscaras funcionam em qualquer plataforma. Detecte endianness em tempo de compilação com __BYTE_ORDER__ para caminhos de código otimizados.

c
#include <arpa/inet.h>  // htonl, ntohl

// Host to network byte order (big-endian) and back
uint32_t host_val = 0x12345678;
uint32_t net_val = htonl(host_val);
uint32_t back = ntohl(net_val);

// Manual byte swap (portable)
uint32_t swap32(uint32_t v) {
    return ((v & 0xFF000000) >> 24) |
           ((v & 0x00FF0000) >> 8)  |
           ((v & 0x0000FF00) << 8)  |
           ((v & 0x000000FF) << 24);
}

// Detect endianness at compile time
#if __BYTE_ORDER__ == __ORDER_LITTLE_ENDIAN__
    printf("Little-endian system\n");
#endif

Hacks Bitwise

Hacks de bits sem branch evitam saltos condicionais para desempenho em loops tight. O truque abs usa right shift aritmético para criar uma máscara. next_pow2 preenche todos os bits abaixo do bit definido mais alto, depois adiciona 1. Bit reversal usa divide-and-conquer: troca nibbles, depois pares, depois bits únicos. Esses são úteis em criptografia, hashing e DSP. CPUs modernas frequentemente têm instruções integradas (POPCNT, LZCNT) que são mais rápidas.

c
// Absolute value without branching
int abs_val(int n) {
    int mask = n >> (sizeof(int)*8 - 1);
    return (n ^ mask) - mask;
}

// Round up to next power of 2
unsigned int next_pow2(unsigned int n) {
    n--;
    n |= n >> 1;  n |= n >> 2;
    n |= n >> 4;  n |= n >> 8;
    n |= n >> 16;
    return n + 1;
}

// Reverse bits in a byte
uint8_t reverse_byte(uint8_t b) {
    b = (b >> 4) | (b << 4);
    b = ((b & 0xCC) >> 2) | ((b & 0x33) << 2);
    b = ((b & 0xAA) >> 1) | ((b & 0x55) << 1);
    return b;
}
19

Tratamento de Sinais Avançado

Conjuntos de Sinais & Bloqueio

sigprocmask bloqueia sinais para que sejam enfileirados (não perdidos) e entregues depois. Isso protege seções críticas de interrupção. SIG_BLOCK adiciona à máscara, SIG_UNBLOCK remove, SIG_SETMASK substitui. Use sigpending para verificar sinais enfileirados. Bloqueie sinais apenas brevemente; bloqueio longo pode perder eventos importantes. Máscaras de sinal são por processo e herdadas através do fork.

c
#include <signal.h>

sigset_t mask, oldmask;
sigemptyset(&mask);
sigaddset(&mask, SIGINT);
sigaddset(&mask, SIGTERM);

// Block signals (they will be queued)
sigprocmask(SIG_BLOCK, &mask, &oldmask);

// Critical section - signals are deferred
do_critical_work();

// Unblock - pending signals are now delivered
sigprocmask(SIG_SETMASK, &oldmask, NULL);

// Check for pending signals
sigpending(&mask);
if (sigismember(&mask, SIGINT)) {
    printf("SIGINT is pending\n");
}

Handlers de Sinal Seguros

Handlers de sinal devem ser async-signal-safe: use apenas funções reentrant (write, _exit, signal). Evite printf, malloc e a maioria das funções de biblioteca—elas podem ser interrompidas no meio de uma operação e corromper o estado. Use volatile sig_atomic_t para flags definidas pelo handler. SA_RESTART reinicia automaticamente chamadas de sistema interrompidas. sigaction é preferível a signal para comportamento portátil e bem definido.

c
#include <signal.h>
#include <unistd.h>

// Only safe type in handlers
volatile sig_atomic_t got_signal = 0;

void handler(int sig) {
    // ONLY use async-signal-safe functions!
    // write() is safe; printf() is NOT
    const char msg[] = "Signal received\n";
    write(STDERR_FILENO, msg, sizeof(msg)-1);
    got_signal = 1;
}

int main() {
    struct sigaction sa;
    sa.sa_handler = handler;
    sigemptyset(&sa.sa_mask);
    sa.sa_flags = SA_RESTART;  // Restart interrupted syscalls
    sigaction(SIGINT, &sa, NULL);

    while (!got_signal) pause();
    return 0;
}

Self-Pipe & signalfd

signalfd (Linux) converte sinais em file descriptors, integrando-os em event loops (epoll, select). Bloqueie o sinal primeiro, depois crie o signalfd. O truque self-pipe é portátil: o handler escreve um byte em um pipe, e o loop principal o lê. Ambas as abordagens movem o tratamento de sinais do contexto restrito do handler para código normal, onde você pode chamar qualquer função com segurança.

c
#include <sys/signalfd.h>

// signalfd: handle signals as file descriptors (Linux)
int setup_signalfd() {
    sigset_t mask;
    sigemptyset(&mask);
    sigaddset(&mask, SIGINT);
    sigaddset(&mask, SIGTERM);
    sigprocmask(SIG_BLOCK, &mask, NULL);  // Must block first

    int fd = signalfd(-1, &mask, SFD_CLOEXEC);
    return fd;
}

// In event loop:
struct signalfd_siginfo si;
read(signalfd_fd, &si, sizeof(si));
printf("Got signal %d\n", si.ssi_signo);

// Self-pipe trick (portable):
// Write to pipe in handler, read in main loop
int pipefd[2];
void pipe_handler(int sig) {
    write(pipefd[1], &sig, sizeof(sig));
}

Timers & SIGALRM

setitimer entrega SIGALRM em intervalos. ITIMER_REAL usa tempo real; ITIMER_VIRTUAL usa tempo de CPU; ITIMER_PROF usa tempo de CPU + sistema. O timer se repete até ser cancelado. Para código moderno, prefira timer_create com SIGEV_THREAD para timers por thread, ou use um fd de timer dedicado (timerfd_create no Linux) para integração com event loops. Sempre trate o sinal para evitar terminação padrão.

c
#include <sys/time.h>

volatile sig_atomic_t timer_fired = 0;

void timer_handler(int sig) { timer_fired = 1; }

int main() {
    struct sigaction sa = {0};
    sa.sa_handler = timer_handler;
    sigaction(SIGALRM, &sa, NULL);

    // Set interval timer: 2 sec initial, 1 sec repeat
    struct itimerval timer;
    timer.it_value.tv_sec = 2;
    timer.it_interval.tv_sec = 1;
    setitimer(ITIMER_REAL, &timer, NULL);

    int count = 0;
    while (count < 5) {
        pause();
        if (timer_fired) {
            printf("Timer %d\n", ++count);
            timer_fired = 0;
        }
    }
    return 0;
}

Enviando Sinais

kill envia um sinal a um processo por PID. kill(0, sig) envia a todo o grupo de processos. raise envia um sinal ao processo chamador. sigqueue envia um sinal com dados anexados (siginfo). Sempre use waitpid para reap processos filhos e verificar o status de saída. WIFSIGNALED distingue morte por sinal de saída normal. Enviar SIGTERM (não SIGKILL) permite desligamento gracioso.

c
#include <signal.h>
#include <sys/wait.h>

pid_t child = fork();
if (child == 0) {
    while (1) { sleep(1); }
} else {
    sleep(3);
    kill(child, SIGTERM);  // Send SIGTERM to child

    int status;
    waitpid(child, &status, 0);
    if (WIFSIGNALED(status))
        printf("Killed by signal %d\n", WTERMSIG(status));
}

// Send signal to self
raise(SIGSTOP);  // Stop (resume with SIGCONT)

// Send to process group
kill(0, SIGUSR1);  // 0 = own process group

// Send with data (sigqueue)
union sigval value = { .sival_int = 42 };
sigqueue(child, SIGUSR1, value);
20

Gerenciamento Avançado de Processos

Padrões fork & exec

O padrão fork-exec cria um processo filho (fork) e substitui sua imagem por um novo programa (exec). fork duplica o processo; exec carrega um novo programa. O filho deve chamar _exit (não exit) em caso de falha do exec para evitar descarregar buffers do pai. waitpid bloqueia até que o filho saia. WEXITSTATUS extrai o código de saída. É assim que os shells executam comandos.

c
#include <unistd.h>
#include <sys/wait.h>

int run_program(const char *path, char *const args[]) {
    pid_t pid = fork();
    if (pid < 0) { perror("fork"); return -1; }

    if (pid == 0) {
        // Child: exec replaces process image
        execvp(path, args);
        perror("execvp");  // Only on failure
        _exit(127);
    }

    // Parent: wait for child
    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

// Usage
char *args[] = {"ls", "-la", "/tmp", NULL};
int result = run_program("ls", args);

Daemonização

setsid cria uma nova sessão e grupo de processos, desvinculando do terminal de controle. O padrão double-fork impede que o daemon readquira um terminal. Após daemonizar, feche file descriptors padrão e redirecione para /dev/null ou arquivos de log. chdir para / previne bloquear unmounts. umask(0) garante permissões de arquivo previsíveis. Esse é o padrão de daemon padrão.

c
#include <unistd.h>

int main() {
    pid_t child = fork();
    if (child > 0) _exit(0);  // Parent exits
    if (child < 0) return 1;

    // First child: create new session
    setsid();

    // Second fork (prevent reacquiring terminal)
    pid_t grandchild = fork();
    if (grandchild > 0) _exit(0);

    // Daemon process
    chdir("/");
    umask(0);
    close(STDIN_FILENO);
    close(STDOUT_FILENO);
    close(STDERR_FILENO);

    while (1) {
        sleep(60);
        // Do daemon work
    }
    return 0;
}

Alternativa posix_spawn

posix_spawn é uma alternativa mais eficiente a fork+exec em sistemas sem MMU (embarcados) ou com grandes pegadas de memória (fork copia tabelas de páginas). Combina criação de processo e exec em uma chamada, com ações de arquivo (redirecionar, fechar) aplicadas atomicamente. Use posix_spawn quando não precisa modificar o estado do filho entre fork e exec. É padrão POSIX e disponível em Linux, macOS e a maioria dos sistemas Unix.

c
#include <spawn.h>
#include <sys/wait.h>

extern char **environ;

int spawn_child(const char *cmd, char *const argv[]) {
    pid_t pid;
    posix_spawn_file_actions_t actions;
    posix_spawn_file_actions_init(&actions);
    posix_spawn_file_actions_addclose(&actions, STDIN_FILENO);

    posix_spawnattr_t attr;
    posix_spawnattr_init(&attr);

    int ret = posix_spawnp(&pid, cmd, &actions, &attr, argv, environ);

    posix_spawn_file_actions_destroy(&actions);
    posix_spawnattr_destroy(&attr);

    if (ret != 0) return -1;

    int status;
    waitpid(pid, &status, 0);
    return WIFEXITED(status) ? WEXITSTATUS(status) : -1;
}

Prevenção de Zumbis

Processos zumbis ocorrem quando um filho sai antes de o pai chamar wait. Evite-os por: (1) tratar SIGCHLD com waitpid em um loop (WNOHANG evita bloqueio), (2) definir SIGCHLD como SIG_IGN (o kernel faz auto-reap), ou (3) fazer double-fork para que o neto seja órfão e adotado por init (PID 1), que o reaps automaticamente. Sempre salve e restaure errno em handlers de sinal.

c
#include <sys/wait.h>
#include <signal.h>

// Reap zombies with SIGCHLD handler
void sigchld_handler(int sig) {
    int saved_errno = errno;
    while (waitpid(-1, NULL, WNOHANG) > 0) {
        // Reap all available zombies
    }
    errno = saved_errno;
}

// Setup: handle SIGCHLD
struct sigaction sa;
sa.sa_handler = sigchld_handler;
sigemptyset(&sa.sa_mask);
sa.sa_flags = SA_RESTART | SA_NOCLDSTOP;
sigaction(SIGCHLD, &sa, NULL);

// Alternative: explicitly ignore (auto-reap)
// signal(SIGCHLD, SIG_IGN);

// Double-fork to prevent zombies
pid_t inter = fork();
if (inter == 0) {
    if (fork() == 0) {
        // Grandchild does the work
        execlp("sleep", "sleep", "10", NULL);
        _exit(1);
    }
    _exit(0);  // First child exits immediately
}
waitpid(inter, NULL, 0);

Limites de Recursos

setrlimit impõe limites de recursos a um processo: tempo de CPU (RLIMIT_CPU), memória virtual (RLIMIT_AS), tamanho de arquivo (RLIMIT_FSIZE), arquivos abertos (RLIMIT_NOFILE), tamanho de stack e tamanho de core dump. O soft limit é aplicado; o hard limit é o teto. Exceder o tempo de CPU envia SIGXCPU; exceder a memória faz malloc falhar. Use limites em processos filhos para prevenir esgotamento de recursos por bugs ou ataques.

c
#include <sys/resource.h>

void set_limits() {
    struct rlimit lim;

    // Limit CPU time to 10 seconds
    lim.rlim_cur = 10; lim.rlim_max = 10;
    setrlimit(RLIMIT_CPU, &lim);

    // Limit memory to 256 MB
    lim.rlim_cur = 256*1024*1024;
    lim.rlim_max = 256*1024*1024;
    setrlimit(RLIMIT_AS, &lim);

    // Limit open files
    lim.rlim_cur = 32; lim.rlim_max = 64;
    setrlimit(RLIMIT_NOFILE, &lim);
}

void show_limits() {
    struct rlimit lim;
    getrlimit(RLIMIT_NOFILE, &lim);
    printf("Max files: soft=%lu, hard=%lu\n",
           lim.rlim_cur, lim.rlim_max);
}
21

Pipes & IPC Avançado

Pipes Anônimos

Pipes anônimos fornecem comunicação unidirecional entre processos pai e filho. Sempre feche a extremidade não utilizada: o escritor deve fechar a extremidade de leitura e vice-versa. Fechar a extremidade de escrita sinaliza EOF ao leitor (read retorna 0). Pipes têm um buffer fixo (tipicamente 64KB); escritas bloqueiam quando cheio. Pipes são apenas para processos relacionados (pai-filho). Para processos não relacionados, use named pipes (FIFOs) ou sockets.

c
#include <unistd.h>

int main() {
    int pipefd[2];
    pipe(pipefd);

    pid_t pid = fork();
    if (pid == 0) {
        // Child: read from pipe
        close(pipefd[1]);  // Close unused write end
        char buf[256];
        ssize_t n = read(pipefd[0], buf, sizeof(buf)-1);
        buf[n] = '\0';
        printf("Child received: %s", buf);
        close(pipefd[0]);
    } else {
        // Parent: write to pipe
        close(pipefd[0]);  // Close unused read end
        const char *msg = "Hello from parent!\n";
        write(pipefd[1], msg, strlen(msg));
        close(pipefd[1]);  // EOF signal to reader
        wait(NULL);
    }
    return 0;
}

Named Pipes (FIFOs)

Named pipes (FIFOs) são arquivos especiais que atuam como pipes entre processos não relacionados. mkfifo cria o arquivo; open bloqueia até que tanto um leitor quanto um escritor estejam presentes. Use O_NONBLOCK para aberturas não bloqueantes. FIFOs persistem no sistema de arquivos até serem unlinked. São úteis para IPC simples entre programas independentes, mas para comunicação complexa, considere Unix domain sockets ou message queues.

c
#include <sys/stat.h>
#include <fcntl.h>

// Create a named pipe (persists in filesystem)
mkfifo("/tmp/myfifo", 0666);

// Writer process
int fd = open("/tmp/myfifo", O_WRONLY);
write(fd, "Hello FIFO", 10);
close(fd);

// Reader process (can be unrelated to writer)
int fd = open("/tmp/myfifo", O_RDONLY);
char buf[256];
ssize_t n = read(fd, buf, sizeof(buf));
close(fd);

// Non-blocking open
int fd = open("/tmp/myfifo", O_RDONLY | O_NONBLOCK);

// Clean up
unlink("/tmp/myfifo");

Memória Compartilhada

Memória compartilhada é o IPC mais rápido: processos mapeiam a mesma memória física em seus espaços de endereço. shm_open cria um objeto de memória compartilhada POSIX; mmap o mapeia. Mudanças são imediatamente visíveis para todos os mapeadores. Use semáforos ou mutexes (com PTHREAD_PROCESS_SHARED) para sincronização. Sempre desmapeie e unlink para evitar vazamentos. Memória compartilhada é ideal para grandes volumes de dados; a sobrecarga é apenas o mapeamento inicial.

c
#include <sys/mman.h>
#include <fcntl.h>

// Create shared memory object
int fd = shm_open("/my_shm", O_CREAT | O_RDWR, 0666);
ftruncate(fd, 4096);  // Set size

// Map into process address space
char *shared = mmap(NULL, 4096, PROT_READ | PROT_WRITE,
                    MAP_SHARED, fd, 0);
close(fd);  // Can close after mmap

// Write data (visible to other processes)
sprintf(shared, "Shared data at %p", (void*)shared);

// Synchronize (flush to backing store)
msync(shared, 4096, MS_SYNC);

// Unmap and clean up
munmap(shared, 4096);
shm_unlink("/my_shm");

Unix Domain Sockets

Unix domain sockets fornecem IPC bidirecional orientado a streams na mesma máquina. São mais rápidos que TCP (sem sobrecarga de rede) e suportam passagem de file descriptors entre processos via SCM_RIGHTS. Use SOCK_STREAM para streams confiáveis, SOCK_DGRAM para datagramas. O caminho do socket é uma entrada de sistema de arquivos; unlink antes de bind para evitar erros de endereço em uso. Unix sockets são a base da comunicação do Docker, X11 e systemd.

c
#include <sys/socket.h>
#include <sys/un.h>

// Server
int sfd = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
unlink("/tmp/mysocket");
bind(sfd, (struct sockaddr*)&addr, sizeof(addr));
listen(sfd, 5);

int cfd = accept(sfd, NULL, NULL);
char buf[256];
read(cfd, buf, sizeof(buf));
write(cfd, "Response", 8);
close(cfd);

// Client
int sock = socket(AF_UNIX, SOCK_STREAM, 0);
struct sockaddr_un addr;
addr.sun_family = AF_UNIX;
strcpy(addr.sun_path, "/tmp/mysocket");
connect(sock, (struct sockaddr*)&addr, sizeof(addr));
write(sock, "Request", 7);
close(sock);

unlink("/tmp/mysocket");

Message Queues

POSIX message queues fornecem IPC baseado em mensagens com ordenação por prioridade. Cada mensagem tem uma prioridade; mensagens de prioridade mais alta são recebidas primeiro. mq_send e mq_receive são atômicos para mensagens únicas. Use O_NONBLOCK para operação não bloqueante ou mq_timedreceive para timeouts. Message queues persistem até serem unlinked, diferentemente de pipes que morrem com os processos. São ideais para dispatching de tarefas e notificação de eventos.

c
#include <mqueue.h>

// Create/open a message queue
struct mq_attr attr = {
    .mq_maxmsg = 10,
    .mq_msgsize = 256
};
mqd_t mq = mq_open("/my_queue", O_CREAT | O_RDWR, 0666, &attr);

// Send a message (priority-based)
mq_send(mq, "Hello MQ", 8, 1);  // priority 1

// Receive a message (highest priority first)
char buf[256];
unsigned int prio;
ssize_t n = mq_receive(mq, buf, sizeof(buf), &prio);
printf("Received (priority %u): %.*s\n", prio, (int)n, buf);

// Non-blocking receive
mqd_t mq_nb = mq_open("/my_queue", O_RDONLY | O_NONBLOCK);

// Timed receive
struct timespec ts = { .tv_sec = time(NULL) + 5 };
mq_timedreceive(mq, buf, sizeof(buf), &prio, &ts);

mq_close(mq);
mq_unlink("/my_queue");
22

Makefile & Build Avançado

Variáveis Automáticas & Padrões

Variáveis automáticas tornam os Makefiles concisos e manuteníveis. Pattern rules (%.o: %.c) definem como construir qualquer arquivo correspondente a um padrão. -MM gera arquivos de dependência (.d) que rastreiam dependências de header, então editar um header dispara a recompilação dos arquivos .c dependentes. A diretiva -include inclui silenciosamente arquivos de dependência se existirem. Essa é a base de sistemas de build robustos em C/C++.

c
# Automatic variables:
# $@ = target name
# $< = first prerequisite
# $^ = all prerequisites
# $? = prerequisites newer than target

# Pattern rule: compiles any .c to .o
%.o: %.c %.h
	$(CC) $(CFLAGS) -c $< -o $@

OBJS = main.o utils.o parser.o

program: $(OBJS)
	$(CC) $(LDFLAGS) $^ -o $@ $(LDLIBS)

# Automatic dependency generation
%.d: %.c
	@$(CC) -MM $< > $@

-include $(OBJS:.o=.d)

clean:
	rm -f $(OBJS) program

Variáveis & Condicionais

Use := para avaliação imediata (mais rápido, previsível) e = para avaliação preguiçosa (permite forward references). ?= define uma variável apenas se não estiver definida, permitindo overrides do usuário a partir da linha de comando. Condicionais (ifeq, ifdef) permitem builds de depuração/release. O truque Q silencia o eco de comandos a menos que VERBOSE esteja definido. MAKECMDGOALS contém os targets da linha de comando.

c
# = recursive (evaluated when used)
# := simple (evaluated when defined)
# ?= conditional (only if not already set)

CC := gcc
CFLAGS ?= -Wall -Wextra -O2
DEBUG := $(filter debug,$(MAKECMDGOALS))

ifeq ($(DEBUG),debug)
    CFLAGS += -g -DDEBUG -O0
else
    CFLAGS += -DNDEBUG
endif

ifdef VERBOSE
    Q =
else
    Q = @
endif

build:
	$(Q)$(CC) $(CFLAGS) -c main.c

Funções & Processamento de Texto

Funções do Make permitem transformação de texto: patsubst para substituição de padrão, filter para selecionar arquivos, wildcard para globbing, foreach para iteração. A função shell executa comandos em tempo de parse—útil para incorporar informações de versão. Substitution references ($(VAR:.c=.o)) são uma alternativa concisa a patsubst para mudanças simples de sufixo.

c
SRCS = main.c utils.c parser.c

# Substitution
OBJS = $(patsubst %.c,%.o,$(SRCS))  # main.o utils.o parser.o
# Or shorthand:
OBJS = $(SRCS:.c=.o)

# Filter files by extension
C_SRCS = $(filter %.c,$(wildcard *.c))

# Add prefix
FLAGS = $(addprefix -I,$(INCLUDE_DIRS))

# Foreach
DIRS = src lib include
CREATE = $(foreach dir,$(DIRS),mkdir -p $(dir);)

# Shell function
GIT_HASH = $(shell git rev-parse --short HEAD 2>/dev/null || echo "unknown")
CFLAGS += -DGIT_HASH=\"$(GIT_HASH)\"

Subdiretórios & Make Recursivo

Make recursivo (Makefiles de subdiretório) é tradicional, mas pode ser lento e propenso a erros com builds paralelos. A abordagem não recursiva (Makefile único com vpath) é preferida por correção e velocidade. Se usar make recursivo, passe variáveis explicitamente e use MAKECMDGOALS para propagar targets. Para projetos grandes, considere CMake ou Meson em vez de Make cru para melhor rastreamento de dependências e suporte a IDE.

c
SUBDIRS = lib src tests

.PHONY: all $(SUBDIRS) clean test

all: $(SUBDIRS)

# Pass variables to sub-makes
$(SUBDIRS):
	$(MAKE) -C $@ $(MAKECMDGOALS)

# Parallel build: make -j4

clean: $(SUBDIRS)
	rm -f *.o program

# Non-recursive alternative (single Makefile)
vpath %.c src:lib
vpath %.h include

CFLAGS += -Iinclude -Ilib

program: main.o lib/utils.o
	$(CC) $^ -o $@

Integração com CMake

CMake gera Makefiles (ou projetos Ninja, VS, Xcode) a partir de um CMakeLists.txt declarativo. CMake moderno usa comandos baseados em target (target_include_directories, target_link_libraries) em vez de variáveis globais. Generator expressions ($<$<CONFIG:Debug>:...) permitem flags por configuração. CMake é o padrão de fato para projetos C/C++, com melhor integração com IDE e suporte multiplataforma que Makefiles crus.

c
# CMakeLists.txt (modern CMake 3.15+)
cmake_minimum_required(VERSION 3.15)
project(MyApp C)

set(CMAKE_C_STANDARD 11)
set(CMAKE_C_STANDARD_REQUIRED ON)

add_executable(myapp src/main.c src/utils.c)

target_include_directories(myapp PRIVATE include)
target_compile_options(myapp PRIVATE -Wall -Wextra)

# Per-configuration flags
target_compile_options(myapp PRIVATE
    $<$<CONFIG:Debug>:-g -O0 -DDEBUG>
    $<$<CONFIG:Release>:-O2 -DNDEBUG>
)

# Find and link library
find_package(MATH REQUIRED)
target_link_libraries(myapp PRIVATE m)

install(TARGETS myapp DESTINATION bin)
23

Depuração com GDB

Início & Breakpoints

Compile com -g para incorporar símbolos de depuração e -O0 para desativar otimizações (caso contrário, variáveis podem ser otimizadas). break define breakpoints em funções, linhas ou condições. watch (data breakpoints) disparam quando uma variável muda—poderoso para encontrar corrupção de memória. Breakpoints condicionais (break func if cond) só disparam quando a condição é verdadeira, útil para loops. tbreak é um breakpoint de uso único.

c
# Compile with debug symbols
gcc -g -O0 -o program program.c

# Start GDB
gdb ./program
gdb --args ./program arg1 arg2

# Breakpoints
(gdb) break main           # Function
(gdb) break 42             # Line 42
(gdb) break file.c:50      # Specific file
(gdb) break func if x > 10 # Conditional
(gdb) tbreak main          # Temporary (removed after hit)
(gdb) watch x              # Break when x changes

# Manage breakpoints
(gdb) info breakpoints
(gdb) delete 2             # Delete breakpoint #2
(gdb) disable 1            # Temporarily disable
(gdb) enable 1

Stepping & Inspeção

next passa por cima de chamadas de função; step entra nelas. finish executa até o final da função atual. Formatos de print: /x (hex), /c (char), /s (string), /t (binário). O operador @ imprime slices de array: arr@5 mostra 5 elementos. display auto-imprime variáveis a cada parada. backtrace mostra a call stack; frame N alterna o contexto para inspecionar aquele frame.

c
(gdb) run                  # Start program
(gdb) continue             # Continue to next breakpoint
(gdb) next                 # Step over (no function entry)
(gdb) step                 # Step into (enters functions)
(gdb) finish               # Run until function returns
(gdb) until 50             # Run until line 50

# Inspect variables
(gdb) print x              # Print variable
(gdb) print *ptr           # Dereference pointer
(gdb) print arr[0]@5       # First 5 elements
(gdb) print/x x            # Print in hex
(gdb) display x            # Auto-print at each stop
(gdb) info locals          # All local variables

# Backtrace
(gdb) backtrace            # Call stack
(gdb) frame 2              # Switch to frame #2
(gdb) up / down            # Navigate stack

Examinando Memória

O comando examine (x) inspeciona memória bruta. O formato especifica contagem, formato de exibição e tamanho da unidade. x/10i $pc desmonta 10 instruções a partir do program counter. x/s trata a memória como uma string terminada em null. info proc mappings mostra o layout da memória virtual (text, data, heap, stack, bibliotecas compartilhadas). Isso é essencial para depurar buffer overflows e corrupção de memória.

c
# x/[count][format][size] address
(gdb) x/10x &arr           # 10 words in hex
(gdb) x/20cb &str          # 20 bytes as chars
(gdb) x/4xw ptr            # 4 words in hex
(gdb) x/s str              # String

# Format: x(hex) d(decimal) u(unsigned) o(octal)
#         t(binary) f(float) a(addr) i(instr) c(char) s(string)
# Size: b(byte) h(half/2) w(word/4) g(giant/8)

# Disassemble
(gdb) disassemble main
(gdb) disassemble /r main  # With raw bytes

# Memory map
(gdb) info proc mappings   # Process memory layout

Core Dumps

Core dumps capturam o estado do processo no momento do crash para depuração post-mortem. Habilite-os com ulimit -c unlimited. Carregue o arquivo core com gdb program core. O backtrace mostra onde o crash ocorreu; info locals mostra valores de variáveis. Para programas multithread, thread apply all bt mostra todos os estados das threads—essencial para análise de deadlock.

c
# Enable core dumps
ulimit -c unlimited
echo "core.%p" > /proc/sys/kernel/core_pattern

# Run program until it crashes
./program                  # Produces core.12345

# Analyze core dump
gdb ./program core.12345

(gdb) bt                   # Backtrace at crash
(gdb) bt full              # With local variables
(gdb) frame 0              # Top frame (crash location)
(gdb) info locals          # Variables at crash
(gdb) print *ptr           # What was the pointer?

# Multithreaded
(gdb) info threads         # List all threads
(gdb) thread 3             # Switch to thread 3
(gdb) thread apply all bt  # Backtraces for ALL threads

Scripts GDB & Automação

.gdbinit automatiza configurações comuns na inicialização. define cria comandos personalizados para tarefas repetitivas. commands anexa ações a breakpoints (ex.: registrar uma variável e continuar). O GDB suporta scripting em Python para análise complexa: automatizar execuções de teste, visualizar estruturas de dados ou extrair estatísticas. Scripts Python podem acessar os internals do GDB via módulo gdb. Use scripts para padronizar fluxos de trabalho de depuração em uma equipe.

c
# .gdbinit file (loaded on startup)
set pagination off
set print pretty on
set print element 0        # No limit on string display

# Define custom commands
define print_array
    set $i = 0
    while $i < $arg0
        printf "[%d] = %d\n", $i, $arg1[$i]
        set $i = $i + 1
    end
end
# Usage: print_array 10 my_array

# Commands attached to breakpoints
break main
commands 1
  silent
  printf "x = %d\n", x
  continue
end

# Python scripting (GDB 7+)
python
import gdb
gdb.execute("break main")
gdb.execute("run")
val = gdb.parse_and_eval("x")
print(f"x = {int(val)}")
end
24

Alinhamento de Memória & Bitfields

Alinhamento de Struct & Padding

O compilador insere padding para que cada membro seja naturalmente alinhado (tipicamente ao seu tamanho: char=1, short=2, int=4, double=8). Reordenar membros do maior para o menor minimiza o padding. Use offsetof para inspecionar o layout. Em sistemas de 64 bits, ponteiros precisam de alinhamento de 8 bytes. Padding excessivo desperdiça memória e prejudica o desempenho de cache. Sempre ordene membros de struct por tamanho decrescente.

c
#include <stddef.h>

// Without alignment consideration (padded)
struct Bad {
    char a;     // 1 byte + 7 padding
    double b;   // 8 bytes
    char c;     // 1 byte + 7 padding
};  // sizeof = 24

// Reordered for efficiency
struct Good {
    double b;   // 8 bytes
    char a;     // 1 byte
    char c;     // 1 byte + 6 padding
};  // sizeof = 16

printf("Bad: a=%zu b=%zu c=%zu total=%zu\n",
    offsetof(struct Bad, a),
    offsetof(struct Bad, b),
    offsetof(struct Bad, c),
    sizeof(struct Bad));

Controlando Alinhamento

C11 alignas especifica alinhamento mínimo para tipos ou variáveis—útil para SIMD (alinhamento de 16/32 bytes) e DMA. Structs packed (__attribute__((packed)) ou #pragma pack) removem todo o padding, economizando espaço, mas potencialmente lentas no acesso (acesso a memória não alinhado pode falhar em algumas arquiteturas). Use packed para protocolos de rede e formatos de arquivo onde o layout exato importa. Nunca faça pack de structs que precisam de acesso rápido.

c
#include <stdalign.h>

// C11 explicit alignment
struct alignas(16) Aligned16 {
    int data[4];  // 16 bytes, 16-byte aligned
};

// Check alignment
printf("Alignment of int: %zu\n", alignof(int));     // 4
printf("Alignment of struct: %zu\n",
       alignof(struct Aligned16));  // 16

// Packed struct (no padding) - GCC/Clang
struct __attribute__((packed)) Packed {
    char a;
    int b;    // No padding after a
    char c;
};  // sizeof = 6

// MSVC packed
#pragma pack(push, 1)
struct PackedMSVC { char a; int b; char c; };
#pragma pack(pop)

Membros de Array Flexíveis

Membros de array flexíveis (C99) permitem que uma struct tenha um array de comprimento variável como último membro. Aloque com malloc(sizeof(struct) + desired_length). O array compartilha a única alocação, então um free libera tudo. Isso é mais eficiente e limpo que um ponteiro separado + malloc. Comum em arrays dinâmicos, strings e cabeçalhos de pacotes de rede. sizeof(struct) exclui o array flexível.

c
#include <stdlib.h>

// C99 flexible array member (last member with no size)
struct Buffer {
    size_t size;
    char data[];  // Flexible array
};

// Allocate with space for data
size_t data_len = 1024;
struct Buffer *buf = malloc(sizeof(struct Buffer) + data_len);
buf->size = data_len;
memset(buf->data, 0, data_len);

// Use data
strcpy(buf->data, "Hello");

// Single free (no separate allocation for data)
free(buf);

Union Type Punning

Unions sobrepõem membros na mesma memória, permitindo type punning (reinterpretar bits como um tipo diferente). Ler um membro da union diferente do último escrito é permitido em C (implementation-defined). Union type punning é legal sob strict aliasing, diferentemente de pointer casting. Unions anônimas (C11) expõem membros diretamente sem um nome de membro. Use unions para variants com tag e acessar internals de float IEEE 754.

c
#include <stdio.h>

// Union: members share the same memory
union Data {
    int i;
    float f;
    char bytes[4];
};

int main() {
    union Data d;
    d.i = 0x41424344;

    printf("As int: %d\n", d.i);
    printf("As float: %f\n", d.f);
    printf("As bytes: %02x %02x %02x %02x\n",
           d.bytes[0], d.bytes[1], d.bytes[2], d.bytes[3]);

    // Safe type punning (legal in C)
    union { float f; int i; } u;
    u.f = 3.14f;
    printf("Float bits: 0x%08x\n", u.i);

    // Anonymous unions (C11)
    struct Variant {
        int type;
        union { int i; float f; char *s; };
    };
}

Layout de Memória & Endianness

Endianness determina a ordem dos bytes na memória: little-endian (x86, ARM padrão) armazena LSB primeiro; big-endian armazena MSB primeiro. Ao escrever formatos binários portáteis, serialize com shifts explícitos em vez de memcpy. Use dump_hex para inspecionar memória bruta durante a depuração. Protocolos de rede usam big-endian (ordem de bytes de rede); use htonl/ntohl para código portátil. Sempre teste em ambos os endianness ao escrever I/O binário portátil.

c
#include <stdint.h>

void dump_hex(void *ptr, size_t len) {
    unsigned char *bytes = ptr;
    for (size_t i = 0; i < len; i++) {
        printf("%02x ", bytes[i]);
        if ((i + 1) % 16 == 0) printf("\n");
    }
}

int main() {
    uint32_t val = 0x12345678;
    printf("Value: 0x%08x\n", val);
    printf("Bytes: ");
    dump_hex(&val, sizeof(val));

    // Little-endian: 78 56 34 12 (LSB first)
    // Big-endian:    12 34 56 78 (MSB first)

    // Serialize to big-endian (network order)
    unsigned char be[4];
    be[0] = (val >> 24) & 0xFF;
    be[1] = (val >> 16) & 0xFF;
    be[2] = (val >> 8)  & 0xFF;
    be[3] = val & 0xFF;

    // Deserialize from big-endian
    uint32_t recovered = (be[0]<<24)|(be[1]<<16)|(be[2]<<8)|be[3];
    printf("Recovered: 0x%08x\n", recovered);
    return 0;
}
25

Funções Variádicas Avançado

Básico de va_list

Funções variádicas usam va_list para acessar argumentos variáveis. va_start inicializa a lista com o último parâmetro nomeado. va_arg recupera o próximo argumento com o tipo especificado. va_end limpa. O chamador deve comunicar a contagem e os tipos (ex.: printf usa especificadores de formato). Funções variádicas não têm segurança de tipo—tipos incompatíveis causam comportamento indefinido.

c
#include <stdarg.h>

// Variadic function: variable number of arguments
int sum(int count, ...) {
    va_list args;
    va_start(args, count);  // Initialize with last named param

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);  // Get next argument
    }

    va_end(args);  // Cleanup
    return total;
}

// Usage
int result = sum(3, 10, 20, 30);  // 60
int result2 = sum(5, 1, 2, 3, 4, 5);  // 15

Variádicas Terminadas por Sentinela

Um valor sentinela (frequentemente NULL) marca o fim da lista de argumentos, eliminando a necessidade de um parâmetro de contagem. Isso é comum em APIs C como execl. GCC __attribute__((sentinel)) avisa se o último argumento não for NULL. Sempre documente a sentinela esperada. A desvantagem é que a sentinela não pode aparecer como um valor de dados válido.

c
#include <stdarg.h>

// Sentinel value marks the end
void log_messages(const char *first, ...) {
    va_list args;
    va_start(args, first);

    const char *msg = first;
    while (msg != NULL) {  // NULL is the sentinel
        printf("%s\n", msg);
        msg = va_arg(args, const char *);
    }

    va_end(args);
}

// Usage: NULL terminates the list
log_messages("Error 1", "Error 2", "Error 3", NULL);

// GCC attribute to enforce sentinel
void log_messages(const char *first, ...)
    __attribute__((sentinel));

vfprintf & Format Strings

vprintf/vfprintf/vsnprintf aceitam um va_list em vez de ..., permitindo funções personalizadas semelhantes ao printf. Sempre use vsnprintf (com limite) em vez de vsprintf para prevenir buffer overflows. Encaminhe o va_list diretamente. Esse padrão é usado em bibliotecas de logging, relatórios de erro e formatadores personalizados. A vulnerabilidade de format string (formato controlado pelo usuário) é um risco de segurança—nunca passe entrada do usuário como formato.

c
#include <stdarg.h>
#include <stdio.h>

// Custom printf-like function
void logf(const char *format, ...) {
    va_list args;
    va_start(args, format);

    // Add timestamp prefix
    printf("[LOG] ");
    vprintf(format, args);  // Pass va_list to vprintf
    printf("\n");

    va_end(args);
}

// Write to string with vsnprintf
void format_msg(char *buf, size_t size, const char *fmt, ...) {
    va_list args;
    va_start(args, fmt);
    vsnprintf(buf, size, fmt, args);  // Safe: bounded
    va_end(args);
}

// Usage
logf("User %s logged in (id=%d)", username, id);
char msg[256];
format_msg(msg, sizeof(msg), "Error %d: %s", code, text);

Ponteiros de Função & Callbacks

Ponteiros de função permitem callbacks e polimorfismo em C. A sintaxe return_type (*name)(params) declara um ponteiro para uma função. qsort usa um callback de comparação para ordenação genérica. Arrays de ponteiros de função implementam dispatch tables (alternativa ao switch). Sempre garanta que a assinatura do callback corresponda exatamente. Ponteiros de função são a base de event handlers, plugins e do strategy pattern em C.

c
#include <stdlib.h>

// Function pointer type
typedef int (*compare_fn)(const void *, const void *);

// Comparison function for qsort
int cmp_int(const void *a, const void *b) {
    return *(const int *)a - *(const int *)b;
}

int main() {
    int arr[] = {5, 2, 8, 1, 9, 3};
    size_t n = sizeof(arr) / sizeof(arr[0]);

    // qsort takes a function pointer
    qsort(arr, n, sizeof(int), cmp_int);

    // Array of function pointers
    double (*ops[])(double, double) = {
        add, subtract, multiply, divide
    };
    double result = ops[2](10.0, 3.0);  // multiply

    // Function pointer as parameter
    void apply(int *arr, size_t n, int (*fn)(int)) {
        for (size_t i = 0; i < n; i++) arr[i] = fn(arr[i]);
    }
    apply(arr, n, square);
    return 0;
}

Macros Variádicas

Macros variádicas (__VA_ARGS__) aceitam argumentos variáveis, úteis para logging e depuração. __VA_OPT__ (C2x) lida com o caso de zero argumentos incluindo condicionalmente a vírgula. A extensão ##__VA_ARGS__ do GCC remove a vírgula precedente quando nenhum argumento é passado. Macros de depuração que compilam para nada em builds de release eliminam sobrecarga sem mudanças de código. Sempre proteja format strings para prevenir ataques de format string.

c
// C99 variadic macros
#define LOG(fmt, ...) printf("[LOG] " fmt "\n", __VA_ARGS__)

// Usage
LOG("Value: %d", x);
LOG("User %s, age %d", name, age);

// GCC __VA_OPT__ (C99/C2x): handle zero arguments
#define LOG2(fmt, ...)     printf("[LOG] " fmt "\n" __VA_OPT__(,) __VA_ARGS__)

// Count arguments (GCC extension)
#define COUNT(...) NARG_(__VA_ARGS__, 5, 4, 3, 2, 1)
#define NARG_(_1, _2, _3, _4, _5, N, ...) N

// Debug macro that compiles out in release
#ifdef NDEBUG
    #define DEBUG(fmt, ...) ((void)0)
#else
    #define DEBUG(fmt, ...) fprintf(stderr, fmt, ##__VA_ARGS__)
#endif

// ## removes comma if no variadic args (GCC extension)
DEBUG("just a message");
DEBUG("value = %d", x);

Was this helpful?

Learning path

Learn from scratch

Learn this language from the ground up with structured lessons.