wandres.dev
DIESTRO · Funciones a fondo

Funciones, scope y storage classes

Cómo se definen y llaman las funciones, el paso por valor, las clases de almacenamiento (static, extern), inline, y los atributos de C23 como [[nodiscard]].

⏱ 14 min

Las funciones organizan el código, pero en C hay matices que definen tu dominio del lenguaje: todo se pasa por valor, las clases de almacenamiento controlan visibilidad y duración, e inline y los atributos afinan el comportamiento. Aquí se construye la base de la modularidad.

🎯 Al terminar esta lección sabrás
  • Declaración vs definición y el paso por valor.
  • Scope y clases de almacenamiento: static, extern.
  • inline y su verdadero significado.
  • Atributos de C23: [[nodiscard]], [[maybe_unused]].

Declaración, definición y paso por valor

Una declaración (prototipo) dice que la función existe; la definición la implementa:

int cuadrado(int x);          // declaración (suele ir en un .h)

int cuadrado(int x) {         // definición
    return x * x;
}

Regla fundamental: C pasa todo por valor. La función recibe una copia del argumento; modificarla no afecta al original:

void incrementar(int n) { n++; }     // no cambia nada fuera
int a = 5;
incrementar(a);                       // a sigue siendo 5
Todo por valor: por eso existen los punteros

Esta es una de las claves para entender C entero. Como todo se copia al pasarlo, ¿cómo modifica una función una variable del que la llama? Pasándole su dirección (un puntero), que también se copia — pero la copia apunta al mismo sitio. void inc(int *n) { (*n)++; } sí modifica el original porque recibe dónde vive, no una copia del valor. Los punteros no son un capricho: son la respuesta de C a “quiero que una función cambie algo del llamador” y a “quiero pasar algo grande sin copiarlo entero”. Cuando entiendes “todo por valor”, entiendes por qué los punteros están en todas partes.

Scope y clases de almacenamiento

Dónde vive una variable y cuánto dura lo controlan las storage classes:

Palabra Efecto
(auto) local, vive mientras dura el bloque (por defecto)
static (local) conserva su valor entre llamadas
static (global/función) limita la visibilidad al archivo actual (linkage interno)
extern declara algo definido en otro archivo (linkage externo)
register sugerencia (hoy ignorada) de usar un registro
int contador(void) {
    static int n = 0;    // se inicializa una vez; persiste entre llamadas
    return ++n;          // 1, 2, 3, … en llamadas sucesivas
}

static int oculto = 42;  // solo visible en ESTE archivo (linkage interno)
extern int global;       // definido en OTRO archivo
💡
static al nivel de archivo = privacidad

El doble significado de static confunde. En una variable local, significa “recuerda tu valor entre llamadas”. En una variable o función global, significa “no te exportes: solo este archivo te ve”. Este segundo uso es tu herramienta de encapsulación en C: marca static todo lo que no forme parte de la API pública de tu módulo. Menos símbolos globales = menos colisiones de nombres y una interfaz más clara (lo verás a fondo en el nivel 16).

inline

inline sugiere al compilador que incruste el cuerpo de la función en el sitio de la llamada, evitando el coste de llamar. Es una sugerencia, no una orden:

inline int max(int a, int b) { return a > b ? a : b; }

En la práctica, con -O2 el compilador ya decide qué incrustar mejor que tú. Usa inline sobre todo para funciones pequeñas en headers (evita errores de definición múltiple), no como técnica de optimización manual.

Atributos de C23

C23 estandariza atributos [[...]] que ayudan al compilador a avisarte:

[[nodiscard]] int abrir(const char *ruta);  // avisa si ignoras el retorno
void usar([[maybe_unused]] int x);           // silencia "no usado" a propósito
[[noreturn]] void abortar(void);             // esta función no vuelve
[[deprecated("usa nueva_api")]] void vieja(void);

[[nodiscard]] es especialmente valioso: si una función devuelve un código de error o un recurso que hay que liberar, marca su retorno para que el compilador te avise cuando lo ignores por descuido.

⚔️ Funciones con intención
  1. Escribe una función que intente modificar su argumento por valor y confirma que no cambia el original; luego hazlo con un puntero.
  2. Crea un contador con una variable static local.
  3. Marca una función y una variable como static a nivel de archivo (privadas del módulo).
  4. Añade [[nodiscard]] a una función y comprueba el aviso al ignorar su retorno.