malloc, free, append

This commit is contained in:
2026-10-02 11:36:59 +02:00
parent b8141311aa
commit 9d517f3607
13 changed files with 960 additions and 13 deletions
+201
View File
@@ -0,0 +1,201 @@
# Structs
## Definir un struct
Definir es dar los campos. Es el equivalente a `type Point struct { ... }` de
Go:
```c
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:
```c
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`
```c
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** (*u*nsigned) 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
```c
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` |