Files
printer-driver/docs/02-pointers-and-strings.md
T
2026-10-02 11:36:59 +02:00

5.7 KiB

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()).

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'".