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
sizeofy lo que recibemalloc. - 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
printfse 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 |