Files
2026-10-02 11:36:59 +02:00

8.4 KiB

Structs

Definir un struct

Definir es dar los campos. Es el equivalente a type Point struct { ... } de Go:

struct point {
    int x;
    int y;
};              /* ← punto y coma obligatorio */

Diferencias con Go:

Go C
X int (nombre, tipo) int x; (tipo, nombre)
Sin ; Cada campo acaba en ;, y la llave final también: };
El nombre del tipo es Point El nombre del tipo es struct point: la palabra struct forma parte del nombre
Mayúscula = exportado No hay campos privados: o ves la definición entera o no ves nada

Declarar un struct (tipo incompleto)

Declarar es decir "existe un struct con este nombre" sin dar sus campos:

struct point;

Hasta que aparezca la definición, struct point es un tipo incompleto. El compilador no conoce su tamaño ni sus campos, así que:

Con un tipo incompleto ¿Se puede?
Declarar un puntero: struct point *p; Sí. Todos los punteros a struct miden lo mismo
Pasar o devolver punteros en funciones Sí
Crear una variable: struct point p; No: no sabe cuántos bytes reservar
sizeof(struct point) No
Acceder a un campo: p->x No: no sabe que existe x

Esto es justo lo que hace funcionar el tipo opaco: la cabecera pública declara, el .c define. El código cliente solo maneja punteros y llama a funciones de la librería. Dentro del .c, donde está la definición, el tipo está completo y se usan los campos con normalidad.

Es parecido a devolver una interfaz en Go para ocultar el tipo concreto, pero sin coste de llamada indirecta: es solo un puntero.

Struct público frente a struct opaco

Definir el struct completo en la cabecera es perfectamente válido. Es una decisión de diseño:

Struct definido en el .h (público) Struct opaco
Crear uno En la pila, sin malloc: struct point p; Solo con la función _new de la librería (montón)
Campos Cualquiera puede leerlos y cambiarlos Solo a través de funciones
Invariantes (len <= cap) El cliente puede romperlas: b.len = 9999 y luego un desbordamiento La librería las garantiza
Cambiar los campos Rompe a quien ya compiló contra la versión anterior (el tamaño y los desplazamientos van dentro de su .o) Libre: el cliente nunca supo cómo era
Coste Acceso directo, sin llamadas Una llamada a función por cada acceso

Se usa público para datos simples sin reglas internas, que el usuario rellena: struct tm (fechas) o struct sockaddr_in (direcciones de red, fase 4). Se usa opaco para objetos con estado e invariantes: FILE en la práctica, o nuestro escpos_buffer.

En Go es la diferencia entre type Point struct { X, Y int } (campos exportados) y type Buffer struct { data []byte } (campos sin exportar, con constructor y métodos). La diferencia es que en Go el tamaño del struct no importa entre paquetes, porque todo se recompila junto.

Para este proyecto el handle opaco es obligatorio: los bindings de cgo (fase 12) necesitan una API que no cambie aunque cambien las tripas.

typedef: un alias para no escribir struct

typedef struct point point;

Se lee así: "point es otro nombre para struct point". A partir de ahí, point *p equivale a struct point *p. Puede ir antes de la definición: sirve igual para un tipo incompleto. Es lo más parecido a cómo nombra Go los tipos.

Es habitual usar el mismo nombre para el struct y para el alias. No chocan, porque C guarda los nombres de struct en un espacio de nombres aparte.

La sintaxis es typedef <tipo existente> <nombre nuevo>;. El nombre nuevo va al final, como en la declaración de una variable. Error típico: escribir typedef struct escpos_buffer; sin el nombre nuevo. gcc solo avisa (useless storage class specifier in empty declaration): declara el struct pero no crea ningún alias.

En Go, el alias equivalente es type Nuevo = Existente, con el orden al revés.

Tipos que necesitan un #include

En Go, byte, int o uint8 existen siempre. En C solo vienen de serie los tipos básicos (char, int, long...). Los de tamaño fijo y los de tamaños son typedef declarados en cabeceras estándar:

Tipo Cabecera Equivalente Go
uint8_t, int32_t, uint64_t... <stdint.h> uint8, int32, uint64
size_t (tamaños y longitudes) <stddef.h> (también la traen <stdlib.h>, <stdio.h>, <string.h>) el int de len(), pero sin signo
bool, true, false <stdbool.h> bool
NULL <stddef.h> (y las mismas que size_t) nil

Sin el #include: error: unknown type name 'uint8_t'. El editor (IntelliSense de VS Code) lo muestra como identifier "uint8_t" is undefined.

uint8_t

Entero sin signo (unsigned) de exactamente 8 bits: valores de 0 a 255. Es el byte/uint8 de Go. El sufijo _t es la convención para los nombres de tipos creados con typedef.

¿Por qué no char, que también mide un byte? Porque el estándar no fija si char tiene signo. En x86 lo tiene (de -128 a 127), y en ARM, el procesador de casi todos los Android, no. Un byte 0xE9 (la é en algunas páginas de códigos) valdría -23 en tu PC y 233 en el móvil. uint8_t es 0-255 en todas partes.

size_t

Entero sin signo pensado para tamaños en bytes y número de elementos:

  • Es lo que devuelve sizeof y lo que recibe malloc.
  • Mide lo mismo que un puntero: 64 bits en x86-64 y 32 bits en un ARM de 32 bits. Así siempre cabe el tamaño de cualquier bloque que puedas direccionar.
  • En printf se imprime con %zu.

En Go su papel lo hace int, que es lo que devuelven len() y cap(). Pero int tiene signo y size_t no.

Trampa de los tipos sin signo: no hay negativos. size_t n = 0; n - 1 no da -1: da la vuelta al valor máximo (18446744073709551615 en 64 bits). Un bucle for (size_t i = n - 1; i >= 0; i--) no termina nunca, porque i >= 0 siempre es verdad. Go, con int, no tiene este problema.

Qué tipo usar

Se elige por lo que representa el valor, no por ahorrar memoria:

Representa Tipo Go
Un número cualquiera, un contador o un código de resultado (puede ser negativo: -1) int int
Un tamaño, una longitud o un índice en memoria size_t int (len, cap)
Un byte de datos crudos (lo que se envía a la impresora) uint8_t byte
Un valor con anchura fija por protocolo o formato (2 bytes de un comando, 4 de una cabecera) uint16_t, uint32_t... uint16, uint32
Sí o no bool (<stdbool.h>) bool
Texto char / char * string

Usar un tipo pequeño para una variable local o un valor de retorno no ahorra nada. El valor viaja en un registro de 64 bits igualmente, y en las operaciones C lo convierte a int (promoción entera). Solo complica las cosas.

Ejemplo real de fallo: una función que devuelve uint8_t y hace return -1;. Un uint8_t no tiene negativos, así que -1 se convierte en 255. Después, if (err == -1) compara 255 con -1: siempre es falso, y el error no se detecta nunca. gcc con los flags del proyecto no avisa. Clang sí (comparison of constant -1 with expression of type 'uint8_t' is always false), y gcc también lo hace con -Wconversion.

Cada fichero incluye lo que usa él mismo. No hay que fiarse de que otra cabecera lo traiga de rebote.

Acceder a los campos: . y ->

Tienes C Go
Un valor: struct point p p.x p.X
Un puntero: struct point *p p->x p.X (Go desreferencia solo)

p->x es una abreviatura de (*p).x: "sigue el puntero y coge el campo x". Si usas . con un puntero o -> con un valor, el compilador da error.

Inicializar

struct point a = { .x = 1, .y = 2 };   /* inicializadores designados */
struct point b = { 0 };                /* todo a cero */

Los campos que no nombras quedan a cero. Como en un literal de Go (Point{X: 1}).

Pero esto solo vale al crear una variable. Un struct reservado con malloc es basura. Hay que rellenar cada campo a mano con ->, o usar calloc, que lo pone todo a cero.

Dónde va cada cosa en el proyecto

Fichero Contenido Quién lo ve
include/escpos.h typedef struct escpos_buffer escpos_buffer; (declaración + alias) Todo el mundo
src/buffer.c struct escpos_buffer { ... }; (definición) Solo buffer.c