Files
printer-driver/docs/02-pointers-and-strings.md
2026-10-07 03:45:29 +02:00

10 KiB
Raw Permalink Blame History

Punteros y cadenas

En C no hay tipo string

En Go, string es un tipo propio: por dentro guarda un puntero y una longitud, y lo manejas como un valor.

En C, una cadena es solo una secuencia de bytes en memoria terminada en '\0'. La longitud no se guarda en ningún sitio: para saberla hay que recorrer la cadena hasta encontrar el '\0'.

"1.0.0"  →  '1' '.' '0' '.' '0' '\0'     (6 bytes, no 5)

Los literales como "1.0.0" viven en memoria de solo lectura. Si intentas escribir en ellos, el comportamiento es indefinido; en la práctica, el programa se cierra.

'A' frente a "A"

Literal Qué es Tipo Ocupa
'A' (comillas simples) Un carácter: el número 65 int (cabe en un uint8_t) 1 byte guardado en un uint8_t
"A" (comillas dobles) Una cadena: los bytes 'A' y '\0' en memoria de solo lectura char *: un puntero a esos bytes 2 bytes, más el puntero

En Go es la misma diferencia que entre 'A' (un rune) y "A" (un string).

Pasar "A" donde se espera un byte es pasar una dirección donde se espera un número. gcc lo detecta: makes integer from pointer without a cast o differ in signedness.

const char *

Una función que "devuelve una cadena" en realidad devuelve la dirección de su primer byte:

const char *version = escpos_version();
  • char: en esa dirección hay caracteres.
  • *: version es un puntero; guarda una dirección, no un carácter.
  • const: a través de este puntero no se pueden modificar los caracteres.

char es un entero de 1 byte. Si guardas un puntero en un char, pierdes la dirección. Por eso el error dice makes pointer from integer.

Los dos significados de *

Dónde aparece Qué significa
En una declaración: const char *p "p es un puntero"
En una expresión: *p "el valor que hay en la dirección de p" (desreferenciar)

Si p apunta a "1.0.0", *p es solo el '1'.

En Go: es lo mismo que var p *T frente a *p.

Regla práctica: ¿quiero la dirección o la cosa?

Un puntero es un papel con la dirección de una casa:

  • buffer es el papel, la dirección.
  • *buffer es la casa, el struct que está en esa dirección.
  • buffer->len es "ve a la casa y coge len". La flecha ya va a la casa, así que no lleva *. Equivale a (*buffer).len.
  • &x es lo contrario de *: "dame la dirección de x".

Antes de escribir, pregúntate: ¿esta función o esta operación necesita la dirección o la cosa?

Aplicado a buffer.c:

Código ¿*? Por qué
escpos_buffer *buffer = ... Sí Declaración: dice que buffer es un puntero
malloc(sizeof *buffer) Sí Quiero el tamaño de la casa (el struct, 24 bytes). sizeof buffer sería el tamaño del papel (el puntero, 8 bytes)
if (buffer == NULL) No Compruebo si el papel tiene una dirección válida
buffer->data = NULL No -> ya va a la casa
return buffer; No Devuelvo el papel; la función devuelve escpos_buffer *
free(buffer) No free necesita la dirección del bloque

En Go las reglas son las mismas, pero Go oculta casi todos los *: p.X desreferencia solo, y los métodos con receptor puntero también. Por eso en Go casi solo ves * en los tipos (*Buffer) y rara vez en expresiones (*p = v). En C la diferencia entre papel y casa siempre se escribe.

Dónde va el *: siempre con el tipo

Los espacios alrededor del * dan igual. Estas tres líneas son idénticas:

escpos_buffer* escpos_buffer_new(void);
escpos_buffer * escpos_buffer_new(void);
escpos_buffer *escpos_buffer_new(void);

En todas, el tipo de retorno es escpos_buffer * ("puntero a escpos_buffer"). El * no es de la función, es del tipo que devuelve. En Go se lee igual: func New() *Buffer.

La costumbre en C es pegarlo al nombre por esta trampa:

int *a, b;    /* a es int *, pero b es int, NO un puntero */

En una declaración múltiple, el * solo afecta al nombre que tiene al lado. Pegarlo al nombre lo deja a la vista. Mejor aún: una variable por línea.

Nombres de parámetros en las declaraciones

En una declaración (el prototipo de la cabecera), el nombre del parámetro es opcional: void escpos_buffer_free(escpos_buffer *); es válido. El compilador solo necesita los tipos.

En la definición (en el .c) sí hace falta, porque el cuerpo lo usa.

Aun así, conviene ponerlo también en la cabecera: es documentación. Con void copy(char *, const char *) no sabes cuál es el destino; con void copy(char *dst, const char *src) sí.

En Go pasa lo mismo en los tipos función y en las interfaces: func(int) error es válido sin nombres.

Paso por valor: un struct entero frente a un puntero

C copia siempre los argumentos, igual que Go. Si un parámetro es escpos_buffer (sin *), la función recibiría una copia del struct entero. Eso tiene dos problemas:

  1. Con un tipo opaco ni siquiera compila: el struct está incompleto fuera de su .c y no se sabe cuánto copiar.
  2. Liberar una copia no sirve de nada: el bloque que reservó malloc es el original.

Por eso las funciones de un tipo opaco reciben siempre un puntero. Es lo mismo que los métodos con receptor puntero de Go (func (b *Buffer) Free()).

Un puntero no sabe cuántos elementos hay

Un slice de Go lleva dentro tres cosas: puntero, len y cap. Un puntero de C lleva solo la dirección del primer elemento. No hay forma de preguntarle cuántos hay detrás.

Por eso las funciones de C que reciben un bloque de datos piden siempre dos parámetros: el puntero y la cantidad.

int escpos_buffer_append(escpos_buffer *buffer, const uint8_t *bytes, size_t count);

En Go sería un solo parámetro: func (b *Buffer) Append(bytes []byte).

Si el que llama pasa un count mayor que lo que hay de verdad, la función lee fuera del bloque. El compilador no puede detectarlo. Es la fuente de los desbordamientos de memoria más famosos.

Las cadenas son la excepción: su longitud se deduce porque acaban en '\0'. Pero los datos binarios (un comando ESC/POS, una imagen) pueden contener bytes 0x00 en medio, así que necesitan su cantidad aparte.

const en parámetros puntero: una promesa

const uint8_t *bytes

Se lee "puntero a uint8_t que no voy a modificar". Es un contrato con el que llama: le garantizas que sus datos salen intactos. Si dentro de la función intentas escribir bytes[0] = 0;, el compilador da error.

  • Pasar un uint8_t * normal donde se pide const uint8_t * está permitido: solo añades una restricción.
  • Al revés da aviso: estarías quitando una promesa.

En Go no existe. Un slice que recibes lo puedes modificar, y el que llama no tiene ninguna garantía.

Arrays

uint8_t init[] = { 0x1B, 0x40 };   /* ESC @ */
  • Un array de tamaño fijo. Si no pones el tamaño entre corchetes, el compilador lo cuenta a partir de los valores.
  • sizeof init da el tamaño en bytes del array completo (aquí 2). Solo funciona con el array en su ámbito, no con un puntero.
  • Al pasar un array a una función, se convierte en un puntero a su primer elemento (decay). La función recibe la dirección, no una copia, y pierde el tamaño. Por eso hay que pasar sizeof init aparte.

En Go [2]byte{0x1B, 0x40} es un array con su longitud dentro del tipo. Se pasa por copia, y para pasarlo sin copiar se usa un slice: init[:].

Aritmética de punteros: [] también lleva un * escondido

Con dibujos interactivos y preguntas: 02-pointers-visual.html.

Sumar un entero a un puntero da otra dirección: p + i avanza i elementos, es decir, i * sizeof *p bytes. No va a la casa. Solo calcula qué casa es.

p[i] es una abreviatura de *(p + i): calcula la dirección y va a ella. Igual que ->, los corchetes llevan el * dentro.

La cadena completa con el buffer:

Escribes Equivale a Tipo Qué es
buffer escpos_buffer * El papel (8 bytes)
*buffer struct escpos_buffer La casa (24 bytes)
buffer->data (*buffer).data uint8_t * Un papel guardado dentro de la casa, con la dirección del bloque de bytes
buffer->data + i &buffer->data[i] uint8_t * La dirección de la casilla i
buffer->data[i] *(buffer->data + i) uint8_t El byte de la casilla i

Para saber si una expresión lleva *, cuenta: *, -> y [] desreferencian una vez cada uno, y & quita una desreferencia.

Qué viaja cuando llamas a una función

En C, a una función le llega siempre una copia del valor que le pasas.

  • f(buffer): viaja una copia del papel, 8 bytes. La función llega a la misma casa, así que lo que cambie en ella lo ves tú.
  • f(*buffer): viaja una copia de la casa, 24 bytes. Lo que cambie la función se queda en su copia.
  • f(data), con data un array de 300 bytes: el array decae a la dirección de su primera casilla. Viaja un papel de 8 bytes, no los 300 bytes. Dentro de la función, sizeof *data es 1, el tamaño de una casilla.

Restar direcciones

Las direcciones se escriben en hexadecimal, en base 16: cada posición vale 16 veces la de su derecha, y a–f valen 10–15.

Hex Decimal
0x08 8
0x10 16
0x18 24
0x20 32
0x40 64
0x80 128
0x100 256

0x20 es 2 × 16 + 0 = 32. Para no hacer la cuenta a mano:

  • En gdb: p 0xd210 - 0xd1f0 da 32, y p/x 32 da 0x20.
  • En bash: echo $((0xd210 - 0xd1f0)).

Si restas dos punteros del mismo tipo (p &b - &a), el resultado no son bytes: son elementos, igual que en la suma. Dos escpos_buffer * separados 32 bytes dan 4, porque cada uno ocupa 8.

En Go pasa lo mismo, todo se copia: un *Buffer copia el puntero y un slice copia la cabecera (puntero, len y cap). La diferencia es que en el slice viaja la longitud, y en C tienes que pasarla tú.

printf y el formato

El primer argumento de printf es el formato. Nunca pases ahí una cadena variable:

printf(s);          /* mal */
printf("%s\n", s);  /* bien */

Si s contuviera un %, printf iría a leer argumentos que no existen, lo que es comportamiento indefinido y un agujero de seguridad clásico (format string). %s significa "imprime los bytes desde esta dirección hasta el '\0'".