Primeros Pasos
Hola Mundo
Todo programa C comienza en main(). #include <stdio.h> trae la biblioteca estándar de I/O (printf, scanf). main devuelve 0 en éxito, no-cero en fallo. La palabra clave void declara explícitamente que main no toma parámetros.
#include <stdio.h>
int main(void) {
printf("Hello, World!\n");
return 0;
}Variables y Tipos
C es de tipado estático. Tipos comunes: int, float, double, char. float necesita el sufijo f. char[] es una cadena (array terminado en null). long y short son modificadores de tamaño. unsigned significa no-negativo. Los tamaños varían por plataforma; use <stdint.h> para anchos fijos.
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 y Salida
scanf necesita la dirección (&) de la variable para almacenar entrada. Siempre limite la longitud de entrada de cadena (%49s para un buffer de 50 caracteres) para prevenir buffer overflow. scanf deja de leer cadenas en espacios en blanco; use fgets para líneas completas.
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 printf
Los especificadores de formato controlan la salida: %d int, %f float, %c char, %s cadena, %x hex, %p puntero. Anchura y precisión (p. ej. %5.2f) controlan alineación y decimales. Desajustar el especificador y el tipo causa comportamiento indefinido.
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-alignedPreprocesador y Cabeceras
El preprocesador se ejecuta antes de la compilación. #include pega archivos de cabecera, #define crea macros y constantes. Siempre use include guards (#ifndef/#define/#endif) en cabeceras para prevenir doble inclusión. Las macros son sustitución de texto — use paréntesis alrededor de los parámetros.
#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
#endifCadenas y String.h
Fundamentos de Cadenas
Las cadenas de C son arrays de char terminados en null. strlen cuenta caracteres antes de '\0'; sizeof devuelve el tamaño del buffer. strcpy copia hasta el terminador null — siempre asegure que el destino sea suficientemente grande para evitar overflow.
#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); // HelloConcatenación y Comparación
strcat añade (el destino debe tener espacio). strcmp compara lexicográficamente: devuelve 0 si iguales, negativo si primero < segundo, positivo si primero > segundo. Nunca use == para comparar cadenas (eso compara punteros, no contenido).
#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 y sscanf
sprintf formatea en un buffer de cadena (como printf pero a una cadena). sscanf parsea una cadena en variables (como scanf pero desde una cadena). Use snprintf en lugar de sprintf para prevenir buffer overflow especificando el tamaño máximo.
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=20strchr, strstr y strtok
strchr encuentra un carácter, strstr encuentra una subcadena. strtok divide una cadena por delimitadores pero modifica la cadena original (inserta terminadores null) y no es thread-safe — pase NULL en llamadas subsiguientes para continuar tokenizando.
#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 y Entrada Segura
fgets es la forma segura de leer cadenas — toma un límite de tamaño para prevenir overflow. A diferencia de scanf, lee espacios. La nueva línea se incluye en el resultado; strcspn la encuentra y elimina. Siempre prefiera fgets sobre gets (que se eliminó en C11).
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);Números y Matemáticas
Tipos Enteros y Límites
Use <stdint.h> para tipos de ancho fijo (int32_t, int64_t) cuando los tamaños exactos importan. <limits.h> proporciona INT_MAX, INT_MIN, etc. para límites específicos de plataforma. El sufijo LL marca literales long long. Los tamaños de int/long varían por plataforma.
#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); // 4294967295Coma Flotante
float es 4 bytes (6-7 dígitos de precisión), double es 8 bytes (15-16 dígitos). Nunca compare floats con == debido a errores de redondeo — use fabs(a - b) < epsilon. <float.h> proporciona DBL_MAX, DBL_EPSILON para límites y precisión.
#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
}Funciones Matemáticas
<math.h> proporciona funciones matemáticas estándar. pow y sqrt devuelven double. fabs es la versión float de abs (abs es para int). Enlace con -lm en algunos sistemas. Para cálculos financieros, evite coma flotante — use centavos enteros en su lugar.
#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 Aleatorios
rand() devuelve un int pseudo-aleatorio de 0 a RAND_MAX. Inicialice con srand() una vez al inicio del programa (usando time(NULL)). rand() % N tiene sesgo de módulo y baja calidad; para uso serio, lea /dev/urandom o use una biblioteca PRNG de terceros.
#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.0Conversión de Tipo y Casting
El casting es (type)value. La división entera trunca — use un operando float para obtener un resultado float. atoi/atof convierten cadenas a números pero no hacen verificación de errores; prefiera strtol/strtod que reportan errores de parseo vía errno.
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");Flujo de Control
If / Else
if/else if/else es el condicional estándar. C trata 0 como falso y cualquier valor no-cero como verdadero. Use llaves incluso para sentencias únicas para prevenir bugs al añadir líneas más tarde. No hay tipo booleano en C89; C99 añade _Bool y <stdbool.h>.
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 a una etiqueta case coincidente. Siempre use break para prevenir caída (los casos 6 y 7 comparten código intencionalmente). default maneja valores no coincidentes. switch solo funciona en tipos entero y char, no cadenas o floats.
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");
}Bucle For
El bucle for tiene init; condition; update. sizeof(nums)/sizeof(nums[0]) calcula la longitud del array en tiempo de compilación. Declarar i dentro del bucle for requiere C99 o posterior. El cuerpo del bucle se ejecuta cero veces si la condición es inicialmente falsa.
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 y Do-While
while verifica antes de ejecutar (puede ejecutarse cero veces). do-while ejecuta el cuerpo primero, luego verifica (se ejecuta al menos una vez). do-while es ideal para validación de entrada y bucles de menú donde el prompt debe aparecer antes de que la condición pueda verificarse.
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 onceBreak, Continue y goto
break sale del bucle/switch más cercano; continue salta a la siguiente iteración. C no tiene break etiquetado, así que goto es la forma idiomática de salir de bucles profundamente anidados. goto por lo demás se desaconseja pero es aceptable para patrones de limpieza y salidas de bucles anidados.
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");Funciones
Definir y Llamar
Las funciones deben declararse (prototipo) o definirse antes de usar. El tipo de retorno void significa sin valor de retorno. const char *name significa que la función no modificará la cadena. C pasa argumentos por valor; use punteros para simular pasaje por referencia.
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;
}Recursión
La recursión se llama a sí misma con una entrada más pequeña. Cada función recursiva necesita un caso base para parar. El fib naive anterior es O(2^n) — exponencial. Use memoización o iteración para eficiencia. La recursión profunda puede desbordar la pila de llamadas.
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) == 120Punteros a Función
Los punteros a función almacenan la dirección de una función, habilitando callbacks y dispatch dinámico. La sintaxis int (*op)(int, int) declara un puntero a una función que toma dos ints y devuelve int. Usado en qsort, manejadores de eventos y sistemas de plugins.
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);
}Funciones Variadic
Las funciones variadic aceptan un número variable de argumentos usando <stdarg.h>. va_start inicializa, va_arg recupera el siguiente argumento, va_end limpia. Necesita una forma de saber el conteo (p. ej., un parámetro de conteo o un valor centinela). printf funciona así.
#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) == 60static e inline
static en una función/variable la limita a la unidad de traducción actual (archivo). static en una variable local hace que persista a través de llamadas (como una global pero con scope). inline sugiere al compilador embeber el cuerpo de la función; los compiladores modernos lo ignoran y deciden ellos mismos.
// 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;
}Arrays y Punteros
Arrays
Los arrays son de tamaño fijo, índice cero y almacenados contiguamente en memoria. sizeof(arr)/sizeof(arr[0]) calcula la longitud pero solo funciona en arrays reales, no punteros (los arrays decaen a punteros cuando se pasan a funciones, perdiendo información de tamaño).
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]); // bananaPunteros
Los punteros almacenan direcciones de memoria. & obtiene la dirección, * desreferencia. Siempre inicialice punteros (use NULL si aún no está asignado). Desreferenciar un puntero NULL o no inicializado es comportamiento indefinido (usualmente un crash). Verifique NULL antes de desreferenciar.
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 Punteros
La aritmética de punteros escala por el tamaño del elemento: p+1 se mueve al siguiente elemento, no al siguiente byte. Esto hace p[i] equivalente a *(p+i). Restar dos punteros al mismo array da el conteo de elementos. La aritmética de punteros solo es válida dentro de un array.
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 Punteros
Los nombres de array decaen a punteros cuando se pasan a funciones o se usan en expresiones, perdiendo información de tamaño. Por eso debe pasar la longitud del array por separado. sizeof(arr) da el tamaño completo del array solo cuando arr es un array verdadero, no un puntero.
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 Multidimensionales
Los arrays 2D son arrays de arrays, almacenados row-major. grid[i][j] accede fila i, columna j. Al pasar a funciones, el número de columnas debe especificarse: void foo(int arr[][3], int rows). Para arrays 2D dinámicos, use arrays de punteros.
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");
}Structs y Unions
Structs
Los structs agrupan variables relacionadas de diferentes tipos. Los miembros se acceden con el operador punto (.). Inicialice con notación de llaves. Los structs se pasan por valor (copiados); pase por puntero (struct Point *) para evitar copiar y para modificar el original.
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 crea un alias para un tipo, así que puede escribir Student en lugar de struct Student. Se usa comúnmente con structs para simplificar la sintaxis. typedef también puede alias tipos de puntero a función, haciendo los callbacks mucho más legibles.
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*);Punteros a Structs
Cuando tiene un puntero a un struct, use el operador flecha (->) para acceder a los miembros. ptr->x es abreviatura de (*ptr).x. Pase punteros a struct a funciones por eficiencia (evita copiar structs grandes) y para permitir modificación.
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).xUnions
Las unions superponen múltiples tipos en la misma memoria — solo un miembro es válido a la vez. Establecer un miembro sobrescribe los otros. Útil para type punning (reinterpretar bits) y ahorrar memoria cuando solo se necesita uno de varios tipos a la vez.
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 y Enums
Los bit fields empaquetan múltiples campos pequeños en un único int, ahorrando memoria (común en protocolos y registros de hardware). Los enums definen constantes enteras con nombre (0, 1, 2... por defecto). Use enums en lugar de #define para mejor depuración y seguridad de tipo.
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); // 1Gestión de Memoria
malloc y free
malloc asigna memoria del heap y devuelve un puntero void (o NULL en fallo). Siempre verifique NULL. Cada malloc debe emparejarse con un free para evitar fugas de memoria. Establecer el puntero a NULL después de free previene bugs use-after-free.
#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 pointercalloc y realloc
calloc asigna y pone a cero memoria (más seguro que malloc que tiene basura). realloc redimensiona: puede mover el bloque, devolviendo un nuevo puntero. Si realloc falla, devuelve NULL pero el bloque original sigue siendo válido — use un puntero temporal para evitar fugas.
#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
La memoria del stack es automática (asignada/liberada con llamadas a función) y rápida pero limitada (a menudo 1-8 MB). La memoria del heap se gestiona manualmente vía malloc/free, mucho más grande, pero más lenta y propensa a fugas. Use stack para datos pequeños y de corta duración; heap para datos grandes o de larga duración.
// 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 freedFugas de Memoria y Punteros Dangling
Las fugas de memoria ocurren cuando pierde el único puntero a memoria asignada (no puede liberarla). Los punteros dangling apuntan a memoria liberada — desreferenciarlos es comportamiento indefinido. El double-free también es indefinido. Herramientas como Valgrind y AddressSanitizer detectan estos bugs.
// 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 likelyArrays y Cadenas Dinámicos
La asignación dinámica le permite crear cadenas/arrays cuyo tamaño se determina en runtime. El llamador es responsable de liberar la memoria. Siempre asigne strlen+1 para cadenas (el terminador null). Este patrón (asignar, devolver, llamador libera) es común en APIs de 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);File I/O
fopen y fclose
fopen abre un archivo y devuelve un puntero FILE (o NULL en fallo). Modos: r (leer), w (escribir/truncar), a (añadir), r+ (leer/escribir), b (binario). Siempre verifique NULL. fclose vacía los buffers y cierra el archivo. fgets lee una línea de forma segura.
#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 y fscanf
fprintf y fscanf funcionan como printf/scanf pero en archivos. fscanf es frágil — desajustes de formato causan problemas. Para parseo robusto, lea líneas con fgets luego parse con sscanf. Siempre cierre archivos cuando termine para vaciar buffers y liberar recursos.
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 y fwrite (Binario)
fread/fwrite leen/escriben bytes raw — ideales para datos binarios y structs. Los argumentos son: buffer, tamaño de elemento, conteo, archivo. Los archivos binarios son compactos pero no portables entre arquitecturas (endianness, padding de struct). Siempre abra archivos binarios con modo 'b'.
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 y rewind
fseek mueve la posición del archivo: SEEK_SET (desde inicio), SEEK_CUR (relativo), SEEK_END (desde el final). ftell devuelve la posición actual. rewind es abreviatura de fseek(f, 0, SEEK_SET). Estos habilitan acceso aleatorio en archivos, útil para bases de datos y lookups indexados.
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 y Streams Estándar
Todo programa C tiene tres streams: stdin (teclado), stdout (pantalla), stderr (pantalla, sin buffer). Escribir errores a stderr los separa de la salida normal, habilitando redirección: program 2> errors.log. stderr es sin buffer para que los mensajes aparezcan antes de crashes.
#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)Preprocesador y Macros
Constantes #define y Macros
#define crea macros de sustitución de texto. Constantes como PI mejoran legibilidad y mantenibilidad. Las macros tipo función deben envolver parámetros en paréntesis para evitar bugs de precedencia: SQUARE(2+3) sin paréntesis sería 2+3*2+3=11. Prefiera variables const y funciones inline sobre macros.
#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); // 7Compilación Condicional
La compilación condicional (#if, #ifdef, #ifndef) incluye/excluye código en tiempo de compilación. Esto se usa para código específico de plataforma, builds de depuración y feature flags. #ifdef verifica si una macro está definida; #if evalúa su valor. #elif y #else proporcionan alternativas.
#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
#endifInclude Guards
Los include guards previenen doble inclusión de cabeceras, lo cual causaría errores de redefinición. El patrón #ifndef/#define/#endif es C estándar. #pragma once es una alternativa más simple y ampliamente soportada (no estándar pero funciona en todos los compiladores principales).
// 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 y Hints del Compilador
#pragma proporciona directivas específicas del compilador. #pragma once es un include guard más simple. #pragma pack controla el layout de memoria de struct (importante para protocolos binarios). __attribute__ (GCC/Clang) anota funciones para optimización, deprecación y advertencias.
#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);Stringificación y Token Pasting
# (stringificación) convierte un argumento macro en un literal de cadena. ## (token pasting) concatena tokens en un nuevo identificador. El patrón de dos niveles STR/XSTR asegura que las macros se expandan antes de la stringificación. Estos se usan en generación de código y macros de logging.
#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); // 42Operaciones de Bits
Operadores Bitwise Básicos
Los operadores bitwise manipulan bits individuales. AND (&) enmascara bits (mantiene solo bits establecidos), OR (|) establece bits, XOR (^) conmuta bits, NOT (~) invierte todos los bits. Left shift (<<) multiplica por potencias de 2, right shift (>>) divide (para unsigned). Siempre use tipos unsigned para manipulación de bits — el right shift con signo es implementation-defined (puede sign-extender). Las operaciones de bits son extremadamente rápidas (un ciclo de CPU) y se usan en flags, registros de hardware, compresión y criptografía. Los literales binarios (prefijo 0b) son C23/C++14; use hex (0x) o decimal en C más antiguo.
#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;
}Establecer, Limpiar y Conmutar Bits
Los bit flags empaquetan múltiples opciones booleanas en un único entero, ahorrando memoria. Las tres operaciones core: SET (|= mask), CLEAR (&= ~mask), TOGGLE (^= mask), CHECK (& mask). Use #define con (1 << n) para nombres de flag legibles. Este patrón es ubicuo en programación de sistemas (permisos de archivo, control de dispositivos, opciones de configuración). Por ejemplo, los permisos de archivo Unix (rwxr-xr-x = 0755) usan bit flags. Siempre use enteros unsigned para flags para evitar problemas de sign-extension. Esto es más eficiente en memoria que un array de bools (1 bit vs 8 bits por flag).
#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;
}Trucos de Manipulación de Bits
Los trucos de bits explotan la representación binaria por velocidad. x & 1 verifica par/impar (más rápido que módulo). x & (x-1) limpia el bit establecido más bajo — útil para verificar potencias de 2 y contar bits. __builtin_popcount (GCC/Clang) o __popcnt (MSVC) cuentan bits establecidos en una instrucción en CPUs modernas. XOR swap (a^=b; b^=a; a^=b) evita una variable temporal pero es más lento en CPUs modernas y menos legible — evítelo. El truco 'redondear a potencia de 2' propaga el bit establecido más alto a todos los bits inferiores, luego añade 1. Estos trucos son útiles en sistemas embedded, motores de juegos y código crítico de rendimiento.
#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 en Structs
Los bit fields empaquetan múltiples valores pequeños en un único struct, ahorrando memoria. La sintaxis de dos puntos (unsigned int field : N) especifica el ancho de bits. El compilador maneja la extracción/inserción de bits automáticamente. Esto es útil para sistemas con memoria restringida, protocolos de red y mapeo de registros de hardware. Sin embargo, el layout de bit fields es implementation-defined (orden de bytes, alineación, padding) — no use bit fields para compatibilidad binaria multiplataforma. Use enmascaramiento de bits explícito (#define + & |) para formatos binarios portables. Los campos sin nombre (: 5) añaden padding. El tamaño total se redondea a la alineación del struct.
#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;
}Manipulación Práctica de Bits (Color RGB)
Empaquetar múltiples valores en un entero es común en gráficos, redes y sistemas embedded. Los colores RGB empaquetan tres canales de 8 bits en 24 bits (0xRRGGBB). Left shift (<<) posiciona cada canal, OR (|) los combina. Right shift (>>) y enmascaramiento (& 0xFF) extraen canales individuales. Esto ahorra memoria (1 int vs 3 bytes) y habilita operaciones atómicas. El mismo patrón se aplica a orden de bytes de red, acceso a registros de hardware y compresión de datos. Siempre use tipos de ancho fijo (uint8_t, uint32_t) para portabilidad — el tamaño de int varía por plataforma.
#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;
}Manejo de Señales
Manejo Básico de Señales
Las señales son interrupciones de software enviadas a un proceso (p. ej., Ctrl+C envía SIGINT, división por cero envía SIGFPE). signal() registra una función manejadora. Dentro de un manejador, solo se permiten funciones async-signal-safe — printf, malloc y la mayoría de funciones stdlib NO son seguras porque el programa principal puede ser interrumpido a mitad de llamada. Use write() para salida. Señales comunes: SIGINT (Ctrl+C), SIGTERM (petición de terminación), SIGKILL (kill forzoso, no se puede atrapar), SIGSEGV (segfault), SIGALRM (timer). Prefiera sigaction() sobre signal() para portabilidad y control.
#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 (Manejo Portátil de Señales)
sigaction() es la forma moderna y portátil de manejar señales (el comportamiento de signal() varía por plataforma). El manejador sa_sigaction recibe un siginfo_t con detalles: si_pid (PID del remitente), si_uid (UID del remitente), si_signo (número de señal), si_code (razón). El flag SA_SIGINFO habilita el manejador de tres argumentos. sa_mask bloquea señales especificadas durante la ejecución del manejador (previene interrupciones anidadas). Otros flags: SA_RESTART (auto-reinicia syscalls interrumpidos), SA_NOCLDWAIT (sin hijos zombie). Siempre use sigaction() en código de producción — signal() no es fiable en algunas plataformas.
#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;
}Enviar Señales y Alarm
alarm(seconds) programa un SIGALRM después del tiempo especificado — útil para timeouts. pause() bloquea hasta que llegue cualquier señal. volatile sig_atomic_t es la única forma segura de compartir datos entre un manejador de señal y el código principal — volatile previene la optimización del compilador, sig_atomic_t garantiza acceso atómico. kill(pid, signal) envía una señal a otro proceso. raise(sig) envía una señal a usted mismo. SIGKILL (9) y SIGSTOP no se pueden atrapar ni ignorar — siempre funcionan. SIGTERM (15) es la petición educada de terminación (los programas pueden atraparla para limpiar). Use alarm() para timeouts simples; use setitimer()/timer_create() para más control.
#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;
}Referencia de Señales Comunes
Entender las señales es esencial para programación Unix. SIGKILL (9) y SIGSTOP no se pueden atrapar — son el último recurso. SIGTERM es la señal estándar de apagado graceful (atrápela para guardar estado). SIGINT es Ctrl+C (interrupción interactiva). SIGCHLD se dispara cuando un hijo sale — si no hace wait() por él, el hijo se vuelve zombie. Establecer SIGCHLD a SIG_IGN auto-reaps hijos (o use SA_NOCLDWAIT). SIGPIPE se dispara al escribir a un pipe/socket cerrado — la mayoría de servidores lo ignoran (signal(SIGPIPE, SIG_IGN)) y verifican valores de retorno de write() en su lugar. Use _exit() (no exit()) en manejadores de señal — exit() ejecuta handlers atexit que pueden no ser signal-safe.
#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 childrenTruco Self-Pipe (Wakeup Signal-Safe)
El truco self-pipe resuelve un problema fundamental: los manejadores de señal no pueden hacer trabajo complejo de forma segura, pero necesita responder a señales en su bucle principal. La solución: el manejador escribe un byte a un pipe, y el bucle principal usa select()/poll() para detectarlo. Esto integra señales con el event loop de forma segura. El manejador solo llama write() (async-signal-safe). Alternativas modernas: signalfd() (específico de Linux, convierte señales en descriptores de archivo directamente) o pselect() (bloquea señales atómicamente durante select). Este patrón se usa en servidores event-driven (nginx, Redis) para manejar señales sin race conditions.
#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;
}Fork de Procesos y Exec
Fundamentos de fork()
fork() crea una copia exacta del proceso actual — la única diferencia es el valor de retorno: 0 en el hijo, PID del hijo en el padre. Ambos procesos continúan desde la llamada fork(). El hijo obtiene una copia de la memoria del padre (copy-on-write optimiza esto). Siempre verifique los tres casos: pid < 0 (error), pid == 0 (hijo), pid > 0 (padre). waitpid() bloquea hasta que el hijo sale y recupera su estado. WIFEXITED verifica si salió normalmente, WEXITSTATUS obtiene el código de salida. Si no hace wait(), el hijo se vuelve zombie hasta ser reaped.
#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;
}Familia exec (Reemplazar Imagen del Proceso)
exec reemplaza la imagen del proceso actual con un nuevo programa — el PID se mantiene, pero el código, datos y pila se reemplazan. exec solo devuelve en fallo. La convención de nombres: 'l' = lista de argumentos (variadic, terminada en NULL), 'v' = vector/array de argumentos, 'p' = buscar PATH para el ejecutable, 'e' = entorno personalizado. El primer argumento es convencionalmente el nombre del programa (argv[0]). fork()+exec() es la forma Unix de lanzar programas — fork crea el proceso, exec carga el nuevo programa. Esta separación habilita configurar descriptores de archivo, entorno y señales entre fork y exec.
#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 envProcesos Zombie y Huérfanos
Los zombies ocurren cuando un hijo sale pero el padre no ha llamado wait() — el kernel mantiene la entrada de la tabla de procesos (PID, estado de salida) hasta ser reaped. Los zombies desperdician PIDs y pueden agotar la tabla de procesos. Solución: siempre haga wait() por los hijos, o establezca SIGCHLD a SIG_IGN (el kernel auto-reaps). Los huérfanos ocurren cuando el padre sale antes que el hijo — init/systemd (PID 1) adopta al huérfano y lo reaps cuando sale. El patrón double-fork (fork, el hijo hace fork de nuevo, el primer hijo sale) crea un daemon que se reparenta automáticamente a init, desvinculándose del terminal. Monitoree zombies con 'ps aux | grep Z' o 'top'.
#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;
}Creación de Proceso Daemon
Los daemons son procesos en segundo plano que se ejecutan sin terminal (p. ej., servidores web, bases de datos). Los pasos de daemonización: fork+exit para desvincularse del shell, setsid() para crear una nueva sesión (sin terminal de control), fork de nuevo por seguridad, chdir('/') para evitar mantener un sistema de archivos, establecer umask para permisos de archivo predecibles, y cerrar/redirigir stdio a /dev/null. El double-fork es una convención Unix para prevenir que el daemon readquiera un terminal vía open(). Los sistemas modernos proporcionan archivos de servicio systemd para gestión de daemons, pero entender la daemonización manual sigue siendo importante para sistemas embedded y código portable. Loguee a archivos (no stdout) ya que stdout es /dev/null.
#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;
}Visión General de Comunicación Entre Procesos (IPC)
IPC permite a procesos comunicarse. Los pipes son los más simples (padre-hijo, unidireccionales). Los named pipes (FIFOs) funcionan entre procesos no relacionados vía una ruta del sistema de archivos. La memoria compartida es la más rápida (zero-copy) pero requiere sincronización (semáforos/mutexes). Los sockets son los más flexibles (bidireccionales, con capacidad de red). Las message queues proporcionan comunicación estructurada con límites de mensaje. Las señales son mínimas (solo un número). Elija según sus necesidades: pipes para padre-hijo simple, memoria compartida para compartir datos de alto rendimiento, sockets para comunicación de red. System V IPC (shmget, semget) es más antiguo; POSIX IPC (shm_open, sem_open) es más limpio pero menos universalmente disponible.
#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 queuesPipes e IPC
Pipes Anónimos (Padre-Hijo)
Los pipes proporcionan comunicación unidireccional entre procesos relacionados (creados por fork). pipe(fd) crea dos descriptores de archivo: fd[0] para leer, fd[1] para escribir. Crítico: cierre el extremo no usado en cada proceso — el padre cierra el extremo de lectura, el hijo cierra el extremo de escritura. Si el extremo de escritura no se cierra, el read() del hijo bloquea para siempre (esperando más datos). read() devuelve 0 (EOF) solo cuando todos los extremos de escritura están cerrados. Los pipes tienen un buffer fijo (típicamente 64KB) — write() bloquea si el buffer está lleno. Los pipes son ideales para comunicación padre-hijo y pipear comandos de shell (ls | grep).
#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)
Los named pipes (FIFOs) son pipes con un nombre del sistema de archivos — funcionan entre procesos no relacionados. mkfifo() crea el archivo pipe; open() bloquea hasta que tanto un lector como un escritor están conectados (sincronización integrada). Los FIFOs persisten hasta unlink()'d (a diferencia de los pipes anónimos que desaparecen cuando los procesos salen). Son útiles para IPC simple entre programas separados. El comportamiento de bloqueo en open() asegura que el escritor no comience hasta que un lector esté listo. Use O_NONBLOCK para aperturas no bloqueantes. Los FIFOs son unidireccionales — para comunicación bidireccional, use dos FIFOs o sockets. Los named pipes se usan comúnmente en scripts de shell y servicios del sistema.
#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)Memoria Compartida (IPC Más Rápido)
La memoria compartida es el IPC más rápido — ambos procesos mapean la misma RAM física, así que la transferencia de datos es zero-copy. shmget() crea un segmento, shmat() lo adjunta al espacio de direcciones del proceso, shmdt() lo desvincula, shmctl(IPC_RMID) lo destruye. La advertencia crítica: la memoria compartida NO proporciona sincronización — si ambos procesos acceden simultáneamente, obtiene data races. DEBE usar semáforos, mutexes u otra sincronización para coordinar el acceso. ftok() genera una clave desde una ruta de archivo (ambos procesos deben acordar la clave). Siempre destruya la memoria compartida cuando termine (persiste después de que los procesos salen, fugando memoria). La memoria compartida POSIX (shm_open/mmap) es una alternativa más limpia.
#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 y Redirección
dup2(oldfd, newfd) hace que newfd sea una copia de oldfd — así es como funciona la redirección de shell. Para redirigir stdout a un pipe: dup2(pipe_write, STDOUT_FILENO) — ahora printf/write a stdout va al pipe. Para redirigir stdin desde un pipe: dup2(pipe_read, STDIN_FILENO) — ahora scanf/read de stdin viene del pipe. Esto es exactamente cómo el shell implementa pipes (ls | sort), redirección (ls > file) y entrada (sort < file). Después de dup2, cierre el fd original (ha sido duplicado). Este patrón es fundamental para construir pipelines Unix programáticamente y es usado por shells, popen() y bibliotecas de gestión de procesos.
#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 Nivel)
popen() es un wrapper de alto nivel alrededor de fork+pipe+exec+shell — ejecuta un comando vía /bin/sh y devuelve un FILE* para leer salida ('r') o escribir entrada ('w'). Es mucho más simple que fork/pipe/exec manual pero se ejecuta a través de un shell, así que NUNCA pase entrada no confiable (riesgo de inyección de shell). Use fgets/fprintf en el FILE* devuelto como un archivo regular. pclose() cierra el pipe y espera a que el hijo salga (devuelve su estado). Para entrada no confiable, use fork+execvp directamente (sin shell). popen es perfecto para scripts rápidos, herramientas de administración del sistema y leer salida de comandos. Para comunicación bidireccional, use socketpair() o dos pipes.
#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 inputMakefile y Herramientas de Build
Estructura Básica de Makefile
Make automatiza la compilación. Un Makefile tiene reglas: target (archivo a construir), prerequisites (dependencias) y recipe (comandos de shell, indentados con TAB). Las variables (CC, CFLAGS) centralizan la configuración. Variables automáticas: $@ (nombre del target), $< (primera dependencia), $^ (todas las dependencias). Las pattern rules (%.o: %.c) generalizan la compilación para todos los archivos fuente. .PHONY declara targets que no son archivos (clean, all, install). La primera regla es el predeterminado (make sin args construye 'all'). Make rastrea timestamps de archivo — solo reconstruye si una dependencia es más nueva que el target. Esta construcción incremental ahorra tiempo en proyectos grandes.
# 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)Variables Automáticas y Pattern Rules
Las variables automáticas hacen los Makefiles concisos y mantenibles. $@ (target), $< (primera dependencia) y $^ (todas las dependencias) son las más comunes. Las pattern rules (%.o: %.c) le permiten escribir una regla para todos los archivos fuente — el % coincide cualquier cadena. $(wildcard) encuentra archivos que coinciden un glob, $(patsubst) transforma cadenas — juntos auto-descubren fuentes. El prefijo @ suprime el echo del comando. Las static pattern rules (target: %.o: %.c) aplican a una lista específica. Entender estas características elimina reglas repetitivas y hace que los Makefiles escalen a proyectos grandes. Siempre use TAB (no espacios) para la indentación del recipe — Make es estricto sobre esto.
# 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 directoriesDependencias y Archivos de Cabecera
Rastrear dependencias de cabecera es crucial — sin ello, cambiar un archivo .h no dispara la recompilación de archivos .c que lo incluyen, llevando a builds obsoletos. La solución: gcc -MMD -MP genera archivos .d listando todas las dependencias (incluyendo cabeceras). -include los incorpora al Makefile. -MP añade targets phony para cabeceras (previene errores si se elimina una cabecera). Este es el enfoque estándar para proyectos C/C++. Sin esto, tendría que listar manualmente cada dependencia de cabecera — inmanejable para proyectos grandes. El primer build no tendrá archivos .d (el '-' en -include suprime el error); se crean durante la compilación y se usan en builds subsiguientes.
# 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 automaticallyMakefile de Proyecto Multi-Directorio
Los proyectos reales abarcan múltiples directorios. Este Makefile auto-descubre fuentes (wildcard), las mapea a un directorio de build (patsubst) y crea directorios según sea necesario. La sintaxis | crea prerrequisitos solo-orden — $(BUILDDIR) se crea antes de la compilación, pero su cambio de timestamp no dispara rebuilds (sin |, crear el directorio haría que todo se reconstruya cada vez). -Iinclude le dice a gcc dónde encontrar cabeceras. -MMD genera archivos de dependencia en el directorio de build. Esta estructura mantiene separados los directorios source, build y binary — fácil de limpiar (rm -rf build) y no contamina el árbol fuente. Para proyectos muy grandes, considere CMake o Meson.
# 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 changesFundamentos de CMake (Alternativa a Make)
CMake es un meta-build system — genera Makefiles (o proyectos Ninja, Visual Studio, Xcode) desde un archivo CMakeLists.txt. Es el estándar de facto para proyectos C/C++ porque maneja compilación multiplataforma, detección de dependencias e integración con IDE. Comandos clave: project() establece el nombre del proyecto, add_executable() define un target de build, target_include_directories() añade rutas de cabecera, target_link_libraries() enlaza bibliotecas. Los builds out-of-source (mkdir build && cd build && cmake ..) mantienen el árbol fuente limpio. CMake auto-detecta compiladores y flags por plataforma. Para nuevos proyectos C/C++, prefiera CMake sobre Makefiles raw — es más mantenible y portable.
# 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 projectsPunteros a Función en Profundidad
Sintaxis y Uso de Punteros a Función
Los punteros a función almacenan la dirección de una función, habilitando dispatch en runtime. La sintaxis int (*fp)(int, int) es notoriamente confusa — léala como 'fp es un puntero a una función que toma (int, int) devolviendo int'. typedef simplifica esto: typedef int (*math_func)(int, int) crea un alias legible. Los nombres de función decaen a punteros (como los nombres de array), así que 'add' y '&add' son equivalentes. Los punteros a función habilitan callbacks, manejadores de eventos, patrones strategy y dispatch tables (arrays de punteros a función para dispatch tipo switch). Son la fundación del comparador de qsort y los sistemas de eventos GUI.
#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 (Ejemplo qsort)
qsort es el ejemplo clásico de punteros a función como callbacks. El comparador recibe punteros const void* (genéricos) y devuelve un entero indicando orden. qsort llama a su comparador para decidir el ordenamiento de elementos — usted controla el comportamiento de ordenación pasando diferentes funciones. Este es el patrón strategy en C: el algoritmo (qsort) es fijo, pero la lógica de comparación se inyecta. void* habilita programación genérica (ordenar cualquier tipo). El comparador debe ser una función pura (sin side effects) y consistente (si a<b y b<c entonces a<c). Este patrón se usa en toda la biblioteca estándar de C (bsearch, atexit, signal).
#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 con Punteros a Función (OOP en C)
C puede simular OOP usando structs con punteros a función — así es como funcionan las vtables de C++ internamente. El struct contiene un puntero a una 'vtable' (tabla de funciones virtuales) que contiene punteros a función. Cada 'subclase' (Circle, Square) tiene su propia vtable con sus implementaciones. Castear un Circle* a Shape* habilita polimorfismo — print_shape() llama a la función area() correcta a través de la vtable. Este patrón se usa en código C real: kernel de Linux (device drivers), GObject (GTK) y SQLite. Proporciona encapsulación, herencia (vía struct embedding) y polimorfismo. Aunque verboso comparado con C++, da control total sobre el layout de memoria y el dispatch virtual.
#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));
}Programación Event-Driven con Callbacks
Los punteros a función habilitan arquitectura event-driven en C — el patrón publish/subscribe. Los handlers se registran vía on_event() (subscribe), y emit_event() llama a todos los handlers registrados (publish). Esto desacopla productores de eventos de consumidores — el emisor no sabe qué hacen los handlers. Este patrón es fundamental para frameworks GUI (click de botón → handler), motores de juegos (colisión → callback) y I/O asíncrono (datos listos → handler de lectura). La firma del handler (nombre de evento + void* data) es lo suficientemente genérica para cualquier tipo de evento. En producción, añada manejo de errores (¿qué si un handler crashea?), ordenamiento por prioridad y la capacidad de desuscribirse. Así es como funcionan libuv, libevent y Node.js bajo el capó.
#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;
}Errores Comunes de Punteros a Función
Los punteros a función tienen varios errores. Llamar un puntero a función NULL crashea (segfault) — siempre verifique NULL antes de llamar. Castear a la firma equivocada es comportamiento indefinido (la convención de llamada puede diferir). Comparar punteros a función por igualdad es válido (misma función), pero el ordenamiento (<, >) es indefinido. Use typedef consistentemente — la sintaxis de puntero a función es propensa a errores, y los typedefs hacen las declaraciones legibles y mantenibles. En C, los punteros a función son la única forma de lograr polimorfismo en runtime y callbacks, así que dominarlos es esencial. C++ añade std::function, lambdas y funciones virtuales como alternativas más seguras.
#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 consistentArgumentos Variables (varargs)
Funciones Variadic Básicas (stdarg)
Las funciones variadic aceptan un número variable de argumentos usando stdarg.h. va_list mantiene la lista de argumentos, va_start la inicializa (requiere el último parámetro nombrado antes de ...), va_arg recupera el siguiente argumento con un tipo especificado, va_end limpia. La función debe saber cuántos argumentos leer — vía un parámetro de conteo (como la cadena de formato de printf) o un valor centinela (terminador NULL). El '...' siempre debe ser el último parámetro. va_arg no hace verificación de tipos — pasar el tipo equivocado es comportamiento indefinido. Así es como funcionan printf, fprintf y execl.
#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.Implementar un printf Personalizado
vprintf/vfprintf/vsprintf son helpers variadic que toman un va_list en lugar de ... — le permiten construir funciones tipo printf personalizadas. El ejemplo log_msg envuelve printf con un prefijo de nivel de log. El ejemplo print_values muestra cómo manejar tipos mixtos: pase un tag de tipo antes de cada valor, luego switch sobre el tag para llamar va_arg con el tipo correcto. Esto es necesario porque va_arg requiere el tipo exacto — no hay información de tipo en runtime. El patrón type-tag se usa en APIs C polimórficas (p. ej., funciones bind de SQLite). Siempre coincida los tipos va_arg exactamente — int vs long, float vs double (los floats se promueven a double en varargs).
#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;
}Funciones Variadic Terminadas por Centinela
Las funciones variadic terminadas por centinela usan un valor especial (usualmente NULL) para marcar el final de los argumentos, en lugar de un conteo. Esto es más limpio para APIs con muchas cadenas — el llamador no necesita contar argumentos. La familia exec (execl, execlp) usa NULL como centinela. El inconveniente: si el llamador olvida el NULL, la función lee memoria basura (comportamiento indefinido). Algunos compiladores (GCC) soportan __attribute__((sentinel)) para advertir sobre centinelas faltantes. Siempre documente que se requiere NULL. El parámetro de tamaño de buffer previene buffer overflows — siempre pase el tamaño del destino y verifique límites antes de strcat.
#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;
}Reenviar Argumentos Variadic
Reenviar argumentos variadic requiere va_copy (no asignación) — va_list puede ser un tipo opaco que no puede copiarse con =. va_copy le permite atravesar la lista de argumentos múltiples veces (p. ej., primero para medir, luego para imprimir). La macro LOG usa __VA_ARGS__ para reenviar todos los argumentos a fprintf. La extensión GCC ##__VA_ARGS__ elimina la coma precedente cuando no se proporcionan args variadic (así LOG("msg") funciona sin coma final). Este patrón es ubicuo en macros de logging de C. C99 requiere al menos un argumento antes de ...; C11/C23 y GCC permiten cero. Para alternativas type-safe en C++, use templates variadic o std::format.
#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 Variadic (C99)
Las macros variadic de C99 usan __VA_ARGS__ para capturar todos los argumentos que coinciden con el ... en la definición de macro. ##__VA_ARGS__ (extensión GCC, ahora estándar en C20) elimina la coma cuando no se pasan args variadic. La macro COUNT usa un truco ingenioso: mapea N argumentos a N, 5, 4, 3, 2, 1 y la posición N-ésima da el conteo. El idiom do { ... } while (0) en ASSERT hace que la macro se comporte como una sola sentencia (segura en if/else sin llaves). #stringifica argumentos de macro. Las macros variadic son esenciales para logging, depuración y programación genérica en C. Son la fundación de muchas APIs de bibliotecas y el sistema de logging del kernel de Linux.
#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;
}Trucos de Manipulación de Bits
Trucos de Bits Comunes
La manipulación de bits opera directamente en representaciones binarias. n & 1 verifica el bit menos significativo para par/impar. Left shift (<<) multiplica por 2; right shift (>>) divide. XOR swap evita una variable temporal pero es menos legible. n & (n-1) limpia el bit establecido más bajo, útil para verificaciones de potencia de 2 y popcount. Estos trucos son rápidos pero priorizan la legibilidad en código de aplicación.
// 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 y Máscaras
Los bit flags empaquetan múltiples opciones booleanas en un único entero, ahorrando memoria. Cada flag es una potencia de 2 (un bit). OR (|) establece flags, AND (&) verifica flags, XOR (^) conmuta, AND NOT (&= ~) limpia. Este patrón es ubicuo en programación de sistemas: permisos de archivo (O_RDONLY, O_CREAT), opciones de socket y estado de GPU. Use constantes con nombre por legibilidad.
#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 en Structs
Los bit fields empaquetan valores pequeños en un número mínimo de bits dentro de structs. La sintaxis de dos puntos especifica el ancho de bits. Esto ahorra memoria para estructuras de datos con muchos campos pequeños (fechas, flags, registros de hardware). Sin embargo, el layout de bit fields es implementation-dependent: orden de bytes, padding y alineación varían entre compiladores. Evite bit fields para formatos de datos portables; use máscaras de bits explícitas en su lugar.
// 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;
}Conversión de Endianness
Endianness determina el orden de bytes: little-endian (x86, ARM predeterminado) almacena LSB primero; big-endian (red, algunos MIPS) almacena MSB primero. Los protocolos de red usan big-endian (network byte order). Use htonl/ntohl para código de red portable. Los byte swaps manuales con shifts y máscaras funcionan en cualquier plataforma. Detecte endianness en tiempo de compilación con __BYTE_ORDER__ para rutas de código optimizadas.
#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");
#endifHacks Bitwise
Los hacks bitwise sin branch evitan saltos condicionales por rendimiento en bucles ajustados. El truco abs usa arithmetic right shift para crear una máscara. next_pow2 llena todos los bits debajo del bit establecido más alto, luego añade 1. La reversión de bits usa divide-and-conquer: intercambia nibbles, luego pares, luego bits individuales. Estos son útiles en criptografía, hashing y DSP. Las CPUs modernas a menudo tienen instrucciones integradas (POPCNT, LZCNT) que son más rápidas.
// 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;
}Manejo de Señales Avanzado
Conjuntos de Señales y Bloqueo
sigprocmask bloquea señales para que se encolen (no se pierdan) y se entreguen más tarde. Esto protege secciones críticas de interrupción. SIG_BLOCK añade a la máscara, SIG_UNBLOCK elimina, SIG_SETMASK reemplaza. Use sigpending para verificar señales encoladas. Solo bloquee señales brevemente; el bloqueo largo puede perder eventos importantes. Las máscaras de señal son por proceso y heredadas a través de fork.
#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");
}Manejadores de Señal Seguros
Los manejadores de señal deben ser async-signal-safe: solo use funciones reentrantes (write, _exit, signal). Evite printf, malloc y la mayoría de funciones de biblioteca — pueden ser interrumpidas a mitad de operación y corromper estado. Use volatile sig_atomic_t para flags establecidos por el manejador. SA_RESTART reinicia automáticamente syscalls interrumpidos. sigaction es preferido sobre signal para comportamiento portable y bien definido.
#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 y signalfd
signalfd (Linux) convierte señales a descriptores de archivo, integrándolas en event loops (epoll, select). Bloquee la señal primero, luego cree el signalfd. El truco self-pipe es portable: el manejador escribe un byte a un pipe, y el bucle principal lo lee. Ambos enfoques mueven el manejo de señales del contexto restringido del manejador a código normal, donde puede llamar cualquier función de forma segura.
#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 y SIGALRM
setitimer entrega SIGALRM a intervalos. ITIMER_REAL usa tiempo real; ITIMER_VIRTUAL usa tiempo de CPU; ITIMER_PROF usa tiempo de CPU + sistema. El timer se repite hasta cancelarse. Para código moderno, prefiera timer_create con SIGEV_THREAD para timers por hilo, o use un fd de timer dedicado (timerfd_create en Linux) para integración con event loops. Siempre maneje la señal para evitar terminación predeterminada.
#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;
}Enviar Señales
kill envía una señal a un proceso por PID. kill(0, sig) envía a todo el grupo de procesos. raise envía una señal al proceso llamador. sigqueue envía una señal con datos adjuntos (siginfo). Siempre use waitpid para reap procesos hijos y verificar el estado de salida. WIFSIGNALED distingue muerte por señal de salida normal. Enviar SIGTERM (no SIGKILL) permite apagado graceful.
#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);Gestión de Procesos Avanzada
Patrones fork y exec
El patrón fork-exec crea un proceso hijo (fork) y reemplaza su imagen con un nuevo programa (exec). fork duplica el proceso; exec carga un nuevo programa. El hijo debe llamar _exit (no exit) en fallo de exec para evitar flushar buffers del padre. waitpid bloquea hasta que el hijo sale. WEXITSTATUS extrae el código de salida. Así es como los shells ejecutan comandos.
#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);Daemonización
setsid crea una nueva sesión y grupo de procesos, desvinculándose del terminal de control. El patrón double-fork previene que el daemon readquiera un terminal. Después de daemonizar, cierre descriptores de archivo estándar y redirija a /dev/null o archivos de log. chdir a / previene bloquear unmounts. umask(0) asegura permisos de archivo predecibles. Este es el patrón estándar de daemon.
#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 es una alternativa más eficiente a fork+exec en sistemas sin MMU (embedded) o con grandes huellas de memoria (fork copia tablas de páginas). Combina creación de proceso y exec en una llamada, con acciones de archivo (redirigir, cerrar) aplicadas atómicamente. Use posix_spawn cuando no necesite modificar el estado del hijo entre fork y exec. Es estándar POSIX y disponible en Linux, macOS y la mayoría de sistemas Unix.
#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;
}Prevención de Zombies
Los procesos zombie ocurren cuando un hijo sale antes de que el padre llame wait. Prevengalos: (1) manejando SIGCHLD con waitpid en un bucle (WNOHANG evita bloqueo), (2) estableciendo SIGCHLD a SIG_IGN (el kernel auto-reaps), o (3) double-forking para que el nieto quede huérfano y sea adoptado por init (PID 1), que lo reaps automáticamente. Siempre guarde y restaure errno en manejadores de señal.
#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);Límites de Recursos
setrlimit impone límites de recursos a un proceso: tiempo de CPU (RLIMIT_CPU), memoria virtual (RLIMIT_AS), tamaño de archivo (RLIMIT_FSIZE), archivos abiertos (RLIMIT_NOFILE), tamaño de pila y tamaño de core dump. El límite soft se aplica; el límite hard es el techo. Exceder el tiempo de CPU envía SIGXCPU; exceder memoria causa que malloc falle. Use límites en procesos hijos para prevenir agotamiento de recursos por bugs o ataques.
#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);
}Pipes e IPC Avanzado
Pipes Anónimos
Los pipes anónimos proporcionan comunicación unidireccional entre procesos padre e hijo. Siempre cierre el extremo no usado: el escritor debe cerrar el extremo de lectura, y viceversa. Cerrar el extremo de escritura señala EOF al lector (read devuelve 0). Los pipes tienen un buffer fijo (típicamente 64KB); las escrituras bloquean cuando están llenas. Los pipes son solo para procesos relacionados (padre-hijo). Para procesos no relacionados, use named pipes (FIFOs) o sockets.
#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)
Los named pipes (FIFOs) son archivos especiales que actúan como pipes entre procesos no relacionados. mkfifo crea el archivo; open bloquea hasta que tanto un lector como un escritor están presentes. Use O_NONBLOCK para aperturas no bloqueantes. Los FIFOs persisten en el sistema de archivos hasta unlink. Son útiles para IPC simple entre programas independientes, pero para comunicación compleja, considere Unix domain sockets o message queues.
#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");Memoria Compartida
La memoria compartida es el IPC más rápido: los procesos mapean la misma memoria física en sus espacios de direcciones. shm_open crea un objeto de memoria compartida POSIX; mmap lo mapea. Los cambios son inmediatamente visibles para todos los mappers. Use semáforos o mutexes (con PTHREAD_PROCESS_SHARED) para sincronización. Siempre unmap y unlink para evitar fugas. La memoria compartida es ideal para datos grandes; la sobrecarga es solo el mapeo inicial.
#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
Los Unix domain sockets proporcionan IPC bidireccional orientado a stream en la misma máquina. Son más rápidos que TCP (sin overhead de red) y soportan pasar descriptores de archivo entre procesos vía SCM_RIGHTS. Use SOCK_STREAM para streams fiables, SOCK_DGRAM para datagramas. La ruta del socket es una entrada del sistema de archivos; unlink antes de bind para evitar errores de dirección en uso. Los Unix sockets son la base de Docker, X11 y comunicación systemd.
#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
Las message queues POSIX proporcionan IPC basado en mensajes con orden por prioridad. Cada mensaje tiene una prioridad; los mensajes de mayor prioridad se reciben primero. mq_send y mq_receive son atómicos para mensajes individuales. Use O_NONBLOCK para operación no bloqueante o mq_timedreceive para timeouts. Las message queues persisten hasta unlink, a diferencia de los pipes que mueren con los procesos. Son ideales para dispatching de tareas y notificación de eventos.
#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");Makefile y Build Avanzado
Variables Automáticas y Patterns
Las variables automáticas hacen los Makefiles concisos y mantenibles. Las pattern rules (%.o: %.c) definen cómo construir cualquier archivo que coincida un patrón. -MM genera archivos de dependencia (.d) que rastrean dependencias de cabecera, así que editar una cabecera dispara la recompilación de archivos .c dependientes. La directiva -include incluye silenciosamente archivos de dependencia si existen. Esta es la fundación de sistemas de build C/C++ robustos.
# 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) programVariables y Condicionales
Use := para evaluación inmediata (más rápido, predecible) y = para evaluación perezosa (permite forward references). ?= establece una variable solo si no está establecida, permitiendo overrides del usuario desde la línea de comandos. Los condicionales (ifeq, ifdef) habilitan builds debug/release. El truco Q silencia el echo de comandos a menos que VERBOSE esté establecido. MAKECMDGOALS contiene los targets de la línea de comandos.
# = 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.cFunciones y Procesamiento de Texto
Las funciones de Make habilitan transformación de texto: patsubst para reemplazo de patrones, filter para seleccionar archivos, wildcard para globbing, foreach para iteración. La función shell ejecuta comandos en tiempo de parseo — útil para embeber información de versión. Las referencias de sustitución ($(VAR:.c=.o)) son una alternativa concisa a patsubst para cambios simples de sufijo.
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)\"Subdirectorios y Make Recursivo
Make recursivo (Makefiles de subdirectorio) es tradicional pero puede ser lento y propenso a errores con builds paralelos. El enfoque no recursivo (único Makefile con vpath) es preferido por corrección y velocidad. Si usa make recursivo, pase variables explícitamente y use MAKECMDGOALS para propagar targets. Para proyectos grandes, considere CMake o Meson en lugar de Make raw para mejor tracking de dependencias y soporte de IDE.
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 $@Integración CMake
CMake genera Makefiles (o proyectos Ninja, VS, Xcode) desde un CMakeLists.txt declarativo. CMake moderno usa comandos basados en targets (target_include_directories, target_link_libraries) en lugar de variables globales. Las generator expressions ($<$<CONFIG:Debug>:...) habilitan flags por configuración. CMake es el estándar de facto para proyectos C/C++, con mejor integración de IDE y soporte multiplataforma que Makefiles raw.
# 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)Depuraci ón con GDB
Inicio y Breakpoints
Compile con -g para embeber símbolos de depuración y -O0 para deshabilitar optimizaciones (de lo contrario las variables pueden optimizarse). break establece breakpoints en funciones, líneas o condiciones. watch (breakpoints de datos) se disparan cuando una variable cambia — potente para encontrar corrupción de memoria. Los breakpoints condicionales (break func if cond) solo se disparan cuando la condición es verdadera, útil para bucles. tbreak es un breakpoint de un solo uso.
# 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 1Stepping e Inspección
next salta llamadas a función; step entra en ellas. finish ejecuta hasta el final de la función actual. Formatos de print: /x (hex), /c (char), /s (cadena), /t (binario). El operador @ imprime slices de array: arr@5 muestra 5 elementos. display auto-imprime variables en cada parada. backtrace muestra la pila de llamadas; frame N cambia el contexto para inspeccionar ese frame.
(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 stackExaminar Memoria
El comando examine (x) inspecciona memoria raw. El formato especifica conteo, formato de visualización y tamaño de unidad. x/10i $pc desensambla 10 instrucciones desde el program counter. x/s trata la memoria como una cadena terminada en null. info proc mappings muestra el layout de memoria virtual (text, data, heap, stack, bibliotecas compartidas). Esto es esencial para depurar buffer overflows y corrupción de memoria.
# 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 layoutCore Dumps
Los core dumps capturan el estado del proceso en el momento del crash para depuración post-mortem. Habilítelos con ulimit -c unlimited. Cargue el archivo core con gdb program core. El backtrace muestra dónde ocurrió el crash; info locals muestra valores de variables. Para programas multihilo, thread apply all bt muestra todos los estados de hilos — esencial para análisis de deadlocks.
# 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 threadsScripts GDB y Automatización
.gdbinit automatiza ajustes comunes al inicio. define crea comandos personalizados para tareas repetitivas. commands adjunta acciones a breakpoints (p. ej., loguear una variable y continuar). GDB soporta scripting Python para análisis complejo: automatizar ejecuciones de tests, visualizar estructuras de datos o extraer estadísticas. Los scripts Python pueden acceder a los internals de GDB vía el módulo gdb. Use scripts para estandarizar flujos de trabajo de depuración entre un equipo.
# .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)}")
endAlineación de Memoria y Bitfields
Alineación y Padding de Struct
El compilador inserta padding para que cada miembro esté naturalmente alineado (típicamente a su tamaño: char=1, short=2, int=4, double=8). Reordenar miembros de mayor a menor minimiza el padding. Use offsetof para inspeccionar el layout. En sistemas de 64 bits, los punteros necesitan alineación de 8 bytes. El padding excesivo desperdicia memoria y daña el rendimiento de caché. Siempre ordene miembros de struct por tamaño decreciente.
#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));Controlar Alineación
C11 alignas especifica alineación mínima para tipos o variables — útil para SIMD (alineación 16/32 bytes) y DMA. Los structs packed (__attribute__((packed)) o #pragma pack) eliminan todo padding, ahorrando espacio pero potencialmente ralentizando el acceso (el acceso a memoria no alineado puede fallar en algunas arquitecturas). Use packed para protocolos de red y formatos de archivo donde el layout exacto importa. Nunca empaquete structs que necesiten acceso rápido.
#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)Miembros de Array Flexible
Los miembros de array flexible (C99) permiten que un struct tenga un array de longitud variable como último miembro. Asigne con malloc(sizeof(struct) + desired_length). El array comparte la única asignación, así que un free libera todo. Esto es más eficiente y limpio que un puntero separado + malloc. Común en arrays dinámicos, cadenas y cabeceras de paquetes de red. sizeof(struct) excluye el array flexible.
#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);Type Punning con Union
Las unions superponen miembros en la misma memoria, habilitando type punning (reinterpretar bits como un tipo diferente). Leer un miembro de union que no sea el último escrito está permitido en C (implementation-defined). El type punning con union es legal bajo strict aliasing, a diferencia del casting de puntero. Las unions anónimas (C11) exponen miembros directamente sin un nombre de miembro. Use unions para variantes etiquetadas y acceder a los internals de floats IEEE 754.
#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 Memoria y Endianness
Endianness determina el orden de bytes en memoria: little-endian (x86, ARM predeterminado) almacena LSB primero; big-endian almacena MSB primero. Al escribir formatos binarios portables, serialice con shifts explícitos en lugar de memcpy. Use dump_hex para inspeccionar memoria raw durante la depuración. Los protocolos de red usan big-endian (network byte order); use htonl/ntohl para código portable. Siempre pruebe en ambos endianness al escribir I/O binario portable.
#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;
}Funciones Variadic Avanzado
Fundamentos de va_list
Las funciones variadic usan va_list para acceder a argumentos variables. va_start inicializa la lista con el último parámetro nombrado. va_arg recupera el siguiente argumento con el tipo especificado. va_end limpia. El llamador debe comunicar el conteo y tipos (p. ej., printf usa especificadores de formato). Las funciones variadic carecen de seguridad de tipos — los tipos desajustados causan comportamiento indefinido.
#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); // 15Variadics Terminados por Centinela
Un valor centinela (a menudo NULL) marca el final de la lista de argumentos, eliminando la necesidad de un parámetro de conteo. Esto es común en APIs C como execl. GCC __attribute__((sentinel)) advierte si el último argumento no es NULL. Siempre documente el centinela esperado. El inconveniente es que el centinela no puede aparecer como un valor de dato válido.
#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 y Cadenas de Formato
vprintf/vfprintf/vsnprintf aceptan un va_list en lugar de ..., habilitando funciones tipo printf personalizadas. Siempre use vsnprintf (acotado) en lugar de vsprintf para prevenir buffer overflows. Reenvíe el va_list directamente. Este patrón se usa en bibliotecas de logging, reporte de errores y formateadores personalizados. La vulnerabilidad de cadena de formato (formato controlado por usuario) es un riesgo de seguridad — nunca pase entrada de usuario como formato.
#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);Punteros a Función y Callbacks
Los punteros a función habilitan callbacks y polimorfismo en C. La sintaxis return_type (*name)(params) declara un puntero a una función. qsort usa un callback de comparación para ordenación genérica. Los arrays de punteros a función implementan dispatch tables (alternativa a switch). Siempre asegure que la firma del callback coincida exactamente. Los punteros a función son la base de manejadores de eventos, plugins y el patrón strategy en 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 Variadic
Las macros variadic (__VA_ARGS__) aceptan argumentos variables, útiles para logging y depuración. __VA_OPT__ (C2x) maneja el caso de cero argumentos incluyendo condicionalmente la coma. La extensión GCC ##__VA_ARGS__ elimina la coma precedente cuando no se pasan argumentos. Las macros de depuración que compilan a nada en builds de release eliminan overhead sin cambios de código. Siempre proteja las cadenas de formato para prevenir ataques de cadena de formato.
// 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);Fragmentos de C relacionados
Copy-paste ready code for common tasks.
Pointer Basics
Declare pointers, dereference, and walk an array with pointer arithmetic.
Memory Management
Allocate, resize, and free heap memory with malloc, realloc, and free.
String Operations
Use string.h helpers for length, copy, concat, compare, and tokenize.
File I/O
Open, read, write, and close files using the stdio FILE API.
Structs
Group related fields with typedef and pass by pointer for mutation.
Function Pointers
Store function addresses for callbacks and dispatch tables.
Preprocessor Macros
Define object-like and function-like macros with conditional compilation.
Bit Operations
Set, clear, toggle, and test bits with bitwise operators and flags.
Was this helpful?