61 KiB
Capítulo 18 — Cadenas a la carta: construí tu propia librería String_View
Capítulo avanzado, pero escrito para que se entienda de verdad. Se apoya en arreglos y cadenas (
06), punteros (07),struct(08), tipos comosize_t(16), memoria y alineación (12), archivos (15) y entrada/salida (14).Acá vas a dejar de pelear con el terminador
'\0'y vas a construir tu propia librería de cadenas: un conjunto de funciones que miran, cortan, recortan y comparan texto sin copiarlo y sin un solomalloc. Al final vas a poder escribir un parser de archivos de configuración entero con esta caja de herramientas.
Cómo leer este capítulo
Este es el capítulo donde "sé usar strlen" se convierte en "entiendo cómo se representa un texto
en memoria y puedo diseñar mi propia forma de tratarlo". No es teoría: cada idea viene con código
para mirar, y cada bloque cierra con práctica para que la ejercites con las manos.
Está dividido en seis partes. Después de cada concepto importante hay uno o varios ejercicios cortos. No los saltees: son la mitad del capítulo.
| Parte | De qué habla | La idea en una frase |
|---|---|---|
| 1 | Los tres pilares y la estructura | Un texto es una calle de bytes; en vez de caminarla entera, guardás dónde empieza y cuánto mide. |
| 2 | Sub-vistas | Mirar un tramo del texto sin copiarlo: solo movés el inicio y acortás el largo. |
| 3 | Recorte (trim) | Sacar espacios de los bordes con solo ajustar el puntero y el contador. |
| 4 | Cortar por delimitador (chop / split) | Partir un texto en pedazos, uno por uno, sin reservar memoria. |
| 5 | Reemplazo en búfer | Como la vista no es dueña de la memoria, el reemplazo escribe en un búfer que vos manejás. |
| 6 | El catálogo completo + jefe | Comparar, buscar subcadenas, construir, cortar… y un parser de verdad para cerrar. |
Los ejercicios se numeran en orden: 18.1, 18.2, 18.3, y así. Cada uno trae su escenario de
prueba (qué entra y qué tiene que salir, nunca el algoritmo) para que compiles y compruebes.
Regla de oro del libro: el capítulo explica y muestra cómo funciona cada cosa (con código ilustrativo), pero los ejercicios no traen la solución. El enunciado te da el problema; la respuesta la escribís vos.
Tu librería objetivo
Antes de arrancar, mirá el plano de la casa que vas a construir. Estas son las funciones que vas a tener escritas vos al terminar el capítulo. Acá van solo las declaraciones (el contrato: qué recibe y qué devuelve cada una), no el cuerpo. Es tu mapa de ruta.
/* ==== sv.h — el contrato de tu librería (solo prototipos) ==== */
#include <stddef.h> /* size_t */
typedef struct {
const char *data; /* dónde empieza el texto */
size_t count; /* cuántos bytes tiene */
} String_View;
/* Impresión */
#define SV_Fmt "%.*s"
#define SV_Arg(sv) (int)(sv).count, (sv).data
/* Construcción */
String_View sv_from_cstr(const char *cstr);
String_View sv_from_parts(const char *data, size_t count);
/* Consultas */
int sv_index_of(String_View sv, char c); /* -1 si no está */
size_t sv_count_char(String_View sv, char c);
/* Sub-vistas (no copian: solo ajustan data y count) */
String_View sv_left(String_View sv, size_t n);
String_View sv_right(String_View sv, size_t n);
String_View sv_chop_left(String_View *sv, size_t n); /* "come" n del frente */
String_View sv_chop_right(String_View *sv, size_t n);
/* Recorte */
void sv_trim_left(String_View *sv);
void sv_trim_right(String_View *sv);
void sv_trim(String_View *sv);
/* Partición */
String_View sv_chop_by_delim(String_View *sv, char delim);
/* Comparación */
int sv_eq(String_View a, String_View b);
int sv_starts_with(String_View sv, String_View prefix);
int sv_ends_with(String_View sv, String_View suffix);
/* Búsqueda de subcadena */
int sv_find(String_View haystack, String_View needle); /* -1 si no está */
int sv_contains(String_View haystack, String_View needle);
/* Reemplazo (escribe en un búfer TUYO, nunca en la vista) */
size_t sv_replace_char(String_View sv, char from, char to,
char *out, size_t outCap);
/* Hacia una cadena C (necesita búfer, para poder agregar '\0') */
size_t sv_to_cstr(String_View sv, char *out, size_t outCap);
Fijate bien en esto. La estructura
String_Viewaparece en todas las firmas. Algunas funciones la reciben por valor (String_View sv) porque no la modifican; otras la reciben por puntero (String_View *sv) porque la avanzan (recortar y cortar mueven el inicio). Esa distinción es el 80% del diseño.
PARTE 1 — Los tres pilares y la estructura String_View
Antes de escribir una línea de tu librería, hay que ver tres cosas sobre cómo C maneja el texto. Si entendés estos tres pilares, todo lo demás cae solo.
1.1 Un texto es una calle, no un objeto
Imaginate una calle con casas pegadas, una al lado de la otra. Cada casa es un byte. Las casas no tienen huecos entre sí: están contiguas. Un texto en C es exactamente eso: un bloque de bytes en memoria, uno pegado al otro.
calle (memoria) ─────────────────────────────►
┌───┬───┬───┬───┬───┐
│ h │ o │ l │ a │ │ ... la calle sigue
└───┴───┴───┴───┴───┘
1ª casa (cada celda = 1 byte)
La palabra clave es contigua: los bytes están uno detrás del otro. Eso es lo que hace que "moverse por el texto" sea tan barato: avanzar una casa es sumar 1 a la dirección.
1.2 El puntero es "la dirección de la primera casa"
Un puntero (char *) no guarda el texto: guarda la dirección donde empieza el texto, o sea
"el número de la primera casa". Cuando escribís "hola", el compilador deja esas cuatro letras en
alguna parte de la memoria y te da la dirección de la h.
#include <stdio.h>
int main(void) {
const char *s = "hola";
/* %p imprime la DIRECCIÓN (el número de la primera casa), no el texto.
s[0] es "la casa 0": la 'h'. s[1] es la siguiente: la 'o'. */
printf("dirección de inicio : %p\n", (const void *)s);
printf("la casa 0 : %c\n", s[0]); /* h */
printf("la casa 1 : %c\n", s[1]); /* o */
return 0;
}
Momento ajá.
s + 1no es "el texto más uno": es "la dirección de la casa siguiente", o sea apunta a lao. La aritmética de punteros (capítulo07) es, literalmente, caminar por la calle. Todo lo que vas a hacer conString_Viewes caminar por la calle sin salirse.
1.3 El cartel de FIN: el terminador '\0'
Acá está la primera gran decisión histórica de C. Como una calle no dice sola dónde termina, C puso
un cartel de FIN al final de cada texto: el byte '\0' (el carácter nulo, valor 0).
┌───┬───┬───┬───┬────┐
│ h │ o │ l │ a │ \0 │ ← el '\0' marca "fin del texto"
└───┴───┴───┴───┴────┘
las 4 letras reales ↑ no es una letra: es el cartel
Ese cartel es cómodo para algunas cosas y un dolor de cabeza para otras. Mirá las dos caras.
Lo bueno: no hace falta saber el largo de antemano. Funciones como printf("%s", s) caminan la
calle hasta encontrar el cartel, y listo. Puede pasar un texto de cualquier largo.
Lo malo (tres consecuencias enormes):
- Para saber el tamaño hay que caminar la calle entera.
strlenno sabe cuánto mide: recorre byte por byte hasta topar el'\0'. Un texto de un millón de letras cuesta un millón de pasos. - No podés mirar un tramo sin copiarlo. Si querés "solo la palabra del medio", no hay forma de
hacerlo apuntando a la mitad: si cortás la calle, quedás sin cartel de FIN. O copiás a otra calle,
o rompés el texto original metiendo un
'\0'en el medio. - Un
\0en el medio te corta el texto. Si en algún lado hay un byte 0 (y en datos binarios pasa), el texto "termina" ahí parastrlen, aunque para vos siga.
#include <stdio.h>
#include <string.h> /* strlen */
int main(void) {
const char *s = "hola mundo";
/* strlen cuenta PASOS hasta el '\0'. No hay un "largo" guardado en ningún lado:
lo descubre caminando. Por eso es O(n). */
printf("strlen = %zu\n", strlen(s)); /* 10 */
/* Y este ejemplo muestra la trampa del '\0' en el medio. */
const char bytes[5] = { 'h', 'o', '\0', 'l', 'a' };
printf("strlen del arreglo = %zu\n", strlen(bytes)); /* 2, ¡no 5! */
return 0;
}
El problema de fondo. C guarda solo la dirección, nunca el largo. El largo hay que descubrirlo (caminando) o adivinarlo. Vos vas a hacer lo obvio: guardar el largo, una vez, y no volver a contarlo.
Nota fina. En este capítulo
strleny compañía viven en<string.h>. Las vas a usar para arrancar (convertir lo viejo a lo nuevo), pero la gracia de tu librería es, justamente, no depender de tener que recorrer el texto de nuevo en cada operación.
1.4 Los literales viven en una vitrina blindada (.rodata)
Cuando escribís "hola" directamente en el código, esas letras no viven en cualquier lado: el
compilador las mete en una sección de solo lectura del binario, llamada .rodata (del inglés
read-only data). Es una vitrina blindada: podés mirar lo que hay adentro, pero si intentás
tocarlo, el sistema operativo te baja el telón con un Segmentation Fault.
#include <stdio.h>
int main(void) {
/* El compilador ADVIERTE con esta línea (const char *). Prestá atención. */
const char *s = "hola";
/* ESTO ES UN ERROR EN TIEMPO DE EJECUCIÓN: s[0] es de solo lectura.
Descomentar la línea de abajo hace que el programa "crash" (Segmentation Fault).
La dejamos comentada a propósito para que puedas compilar y correr. */
/* s[0] = 'H'; */
printf("%s\n", s); /* imprimir (leer) sí se puede, siempre */
return 0;
}
Y si querés un texto que sí podés modificar, no lo definas con un literal: definilo como un
arreglo (char buf[]), que copia las letras a memoria escribible.
#include <stdio.h>
int main(void) {
/* Con corchetes, el compilador crea un ARREGLO con su propia copia del texto:
esa copia SÍ es modificable. */
char buf[] = "hola";
buf[0] = 'H'; /* ahora sí se puede */
printf("%s\n", buf); /* Hola */
return 0;
}
La regla que te salva.
const char *s = "literal";→ solo leer. Si necesitás escribir, usáchar buf[] = "literal";(arreglo) o unaString_View, que es siempre de solo lectura (mirá elconstendata). Por eso tu librería nunca va a modificar la memoria original: solo la mira.
1.5 El nuevo paradigma: la ventana
Junta las tres ideas y tenés la solución. En vez de depender del cartel de FIN y de caminar la calle, guardá dos datos:
data: la dirección donde empieza lo que mirás.count: cuántos bytes querés mirar.
Eso es todo. Es como una ventana (o una foto recortada) apoyada sobre la calle: la calle sigue estando completa; vos solo decidís desde dónde y hasta dónde mirás.
typedef struct {
const char *data; /* Puntero al inicio del texto */
size_t count; /* Cantidad exacta de bytes */
} String_View;
Mirá la calle completa y una ventana sobre ella:
calle: h o l a m u n d o \0
^
│
sv.data ─┘ sv.count = 10 → "hola mundo"
└────────── 10 bytes ──────────┘
la MISMA calle, otra ventana:
calle: h o l a m u n d o \0
^
│
sv.data ─┘ sv.count = 5 → "mundo"
Fijate algo hermoso: ninguna de las dos ventanas cambió un byte. Las dos apuntan a la misma calle.
"hola mundo" y "mundo" son dos vistas de la misma memoria. Eso es imposible de hacer gratis con
cadenas C terminadas en '\0'.
¿Y el
'\0'? ConString_Viewno lo necesitás: vos ya sabés elcount. La vista puede apuntar al medio de un texto sin cartel de FIN alguno. El\0pasa de ser una obligación a ser un detalle que solo te importa cuando tenés que entregarle el texto a una función vieja que sí lo espera (comoprintf).
Cómo se lee
size_t. Es un entero sin signo pensado para tamaños (capítulo16). Es el tipo correcto paracount: un largo nunca es negativo. Guardátelo para cuando quieras devolver un "no encontrado": ahí no vas a poder usarsize_t(no tiene negativos) y vas a necesitar un centinela con signo. Lo ves en la Parte 2.
1.6 Imprimir sin '\0': el truco del %.*s
Acá viene la primera piedra en el zapato. Si tenés una String_View, no podés hacer
printf("%s", sv.data): printf va a caminar hasta encontrar un \0… que quizás no exista donde
vos querés (puede seguir el resto de la calle, o directamente no haber nunca). Necesitás decirle a
printf: "mirá exactamente count bytes, ni uno más".
Para eso existe una combinación del formato de printf: %.*s. Se lee así:
- El
.dice "precisión" (para%s, cuántos caracteres como máximo). - El
*dice "y ese máximo no lo escribo acá: te lo paso como argumento, justo antes del puntero".
#include <stdio.h>
int main(void) {
const char *data = "hola mundo entero";
size_t count = 10; /* queremos solo "hola mundo" */
/* %.*s = "imprimí una cadena, pero mirá a lo sumo N bytes".
El N llega como argumento (int) ANTES del puntero. Por eso van DOS argumentos. */
printf("[%.*s]\n", (int)count, data); /* [hola mundo] (sin el " entero") */
return 0;
}
¿Por qué (int)count y no count a secas? Porque el * de printf espera un int, y count
es size_t (que puede ser más ancho). El cast lo deja del tamaño exacto que printf va a leer. Si
te olvidás el cast, en muchas máquinas "funciona igual", pero es una bomba de tiempo: el tipo que
pasás tiene que coincidir con el que printf espera.
Como esto se usa todo el tiempo, lo natural es envolverlo en un par de macros que no te hagan
escribir el .%*s ni el (int) a mano en cada printf:
#include <stdio.h>
/* SV_Fmt: el "molde" de formato, para pegar DENTRO de un printf junto al resto del texto. */
#define SV_Fmt "%.*s"
/* SV_Arg: expande a los DOS argumentos que %.*s necesita: el largo y el puntero. */
#define SV_Arg(sv) (int)(sv).count, (sv).data
Y se usan así, encajando en cualquier printf:
printf("Nombre: [" SV_Fmt "]\n", SV_Arg(sv));
/* ↑ el molde ↑ los dos argumentos
equivale a:
printf("Nombre: [%.*s]\n", (int)sv.count, sv.data); */
Por qué las macros y no una función. El
%.*stiene que quedar pegado dentro delprintf(es parte del formato), y los dos argumentos tienen que ir en el medio de los demás. Una función no puede hacer eso: devuelve un valor, no "dos argumentos intercalados en unprintf". Las macros sí, porque son texto que se reemplaza. Por eso esta pareja es el idioma estándar.
Pensalo así.
%sle dice aprintf"caminá hasta el cartel de FIN".%.*sle dice "mirá exactamente tanto como yo te diga". Cambiaste "descubrir el largo" por "decir el largo". Es la misma idea de todo el capítulo, aplicada a la impresión.
Práctica de la Parte 1
Ejercicio 18.1 — El conversor (sv_from_cstr)
Conceptos · struct, String_View, strlen, size_t, punteros a const
Contexto · Todo tu código nuevo va a trabajar con String_View, pero el mundo entero está lleno de
cadenas C terminadas en '\0' (literales, argv, lo que devuelve fgets). Necesitás una aduana
que convierta de un mundo al otro. Simplificación: una función que mide una cadena C y la envuelve en
una vista.
Consigna · Implementá sv_from_cstr. Debe recibir un const char *, calcular su largo una sola
vez (con strlen) y devolver una String_View con ese data y ese count. Después, en el main,
creá una vista a partir de un literal y imprimila con SV_Fmt/SV_Arg, mostrando también el
count.
Entrada · Una cadena C (un literal o algo leído con fgets).
Salida · El texto de la vista y su largo en bytes.
Escenario de prueba
entrada : "hola mundo"
salida : texto = [hola mundo]
count = 10
entrada : ""
salida : texto = []
count = 0
Identificadores sugeridos · sv_from_cstr, sv, text
Criterios de aceptación
strlense llama una sola vez (el largo no se recalcula después).dataapunta al mismo texto original (no hay copia).countcoincide constrlen.- La cadena vacía da
count == 0y no se rompe.
Ejercicio 18.2 — La impresora de vistas (macro SV_Fmt + sv_print)
Conceptos · %.*s, printf, macros, const char *
Contexto · Cada vez que depures tu librería vas a querer ver una vista. Como printf("%s", ...) no
sirve (no hay '\0' garantizado), necesitás las macros de formato y una función cómoda. Simplificación:
imprimir una vista sin depender del terminador.
Consigna ·
- Definí las macros
SV_FmtySV_Arg. - Escribí
sv_printque reciba unaString_Viewy la imprima conSV_Fmt/SV_Arg(sin\0). - Comprobá que con una vista que no termina en
'\0'(por ejemplo, un tramo en el medio de una cadena C) la impresión no se pasa decount.
Entrada · Una o varias vistas (algunas a mitad de un texto). Salida · El contenido exacto de cada vista, encerrado entre corchetes.
Escenario de prueba
cadena completa : "hola mundo"
vista : data → "mundo", count = 5 (un tramo del medio, sin '\0' propio)
salida : [mundo] ← NO debe imprimir nada más allá del 'o'
vista vacía (count = 0) → []
Identificadores sugeridos · SV_Fmt, SV_Arg, sv_print
Criterios de aceptación
sv_printusa%.*s(no%s).- Se imprime exactamente
countbytes, sin mirar lo que sigue después. - Una vista en el medio de un texto imprime solo su tramo.
- Una vista vacía imprime los corchetes vacíos, sin basura.
PARTE 2 — Sub-vistas: mirar tramos sin copiar
Ya tenés la ventana. Ahora aprendé a moverla. Esta parte es corta y es el corazón del paradigma: todo se reduce a ajustar dos números.
2.1 Recortar = mover el inicio, achicar el largo
Imaginate que querés la primera palabra de "hola mundo". Con cadenas C: copiás a otro lado o
rompés el original. Con String_View: no copiás nada, solo hacés una vista nueva que empieza
donde mismo y mide menos.
original: h o l a m u n d o \0
^
sv.data, sv.count = 10
sv_left(sv, 4): la MISMA dirección, pero count = 4
h o l a
^
misma data, count = 4 → "hola"
sv_right(sv, 5): esta vez SÍ movemos el inicio
m u n d o
^
data = data + 5, count = 5 → "mundo"
Fijate la diferencia entre las dos: sv_left mantiene el inicio y acorta; sv_right mueve el
inicio hacia adelante y ajusta el largo. Las dos devuelven una vista nueva, sin tocar la original
ni la memoria.
La cuenta que va a reaparecer siempre. Para "los últimos
n": el nuevo inicio esdata + (count - n)y el nuevo largo esn. Para "los primerosn": el inicio no cambia, y el largo pasa a sermin(n, count). Esemines la protección contra pasarte: pedir 100 de un texto de 10 no es un error, es pedir "todo".
2.2 ¿Y si no encuentro lo que busco? El centinela
sv_index_of busca un carácter y devuelve dónde está. Pero, ¿qué devolvés si no está? Esta es
una decisión de diseño que se equivoca todo el mundo la primera vez.
size_tno sirve para el "no está": es sin signo, no tiene-1. Si devolvieras el tamaño del texto como "no encontrado", te confundirías con "está al final".- Necesitás un valor que nunca sea un índice válido. El clásico es
-1, y para eso necesitás un tipo con signo (into, más correcto para índices,ptrdiff_t, capítulo16).
sv_index_of("banana", 'n') → 2 (índice de la primera 'n')
sv_index_of("banana", 'z') → -1 (centinela: "no está")
Regla general de diseño. Cuando una función puede fallar, decidí explícitamente cómo avisa: o devuelve un centinela (como
-1), o separa el "hubo suerte" en unintde retorno (como hacesscanf, capítulo14) y saca el valor por puntero. Nunca dejes el error "implícito": es el bug más caro de esta librería.
Práctica de la Parte 2
Ejercicio 18.3 — El buscador de un carácter (sv_index_of)
Conceptos · recorrido por índice, const char *, centinela -1, tipo con signo
Contexto · Antes de partir texto necesitás saber dónde están las marcas (la coma, el =, el
espacio). Simplificación: ubicar la primera aparición de un carácter.
Consigna · Implementá sv_index_of: recorre la vista y devuelve el índice (base 0) de la
primera aparición del carácter, o -1 si no aparece. No uses strchr (esa necesita '\0'): el
recorrido es tuyo y está acotado por count. Comprobá con un carácter presente, uno ausente y el caso
de la vista vacía.
Entrada · Una String_View y un carácter.
Salida · El índice de la primera aparición, o -1.
Escenario de prueba
sv = "banana"
sv_index_of(sv, 'b') → 0
sv_index_of(sv, 'n') → 2 (la PRIMERA 'n')
sv_index_of(sv, 'a') → 1
sv_index_of(sv, 'z') → -1 (centinela)
sv_index_of("", 'a') → -1
Identificadores sugeridos · sv_index_of, i, found
Criterios de aceptación
- El recorrido respeta
count(no se pasa ni necesita'\0'). - Se devuelve el índice base 0 de la primera aparición.
- Ausente y vacío devuelven
-1. - El tipo de retorno es con signo (para poder representar
-1).
Ejercicio 18.4 — Los primeros n (sv_left)
Conceptos · sub-vista, min, protección de límites, size_t
Contexto · "Dame el prefijo de este texto" es lo que hacés para quedarte con una etiqueta, un código
de país, un encabezado. Simplificación: una vista con los primeros n bytes.
Consigna · Implementá sv_left: devuelve una vista nueva con los primeros n bytes. No
copies memoria: la data es la misma, solo cambia el count. Si n es mayor que el largo, devolvé
la vista completa (protección por min). n = 0 da una vista vacía.
Entrada · Una String_View y un n (size_t).
Salida · Una nueva vista con la parte pedida.
Escenario de prueba
sv = "hola mundo"
sv_left(sv, 4) → "hola"
sv_left(sv, 0) → "" (count = 0)
sv_left(sv, 100) → "hola mundo" (se recorta al total, no es error)
Identificadores sugeridos · sv_left, n, result
Criterios de aceptación
- No se copia ningún byte (misma
data). nmayor que el largo devuelve la vista completa (usamin).n = 0dacount == 0.- La vista original no se modifica (se pasa por valor).
Ejercicio 18.5 — Los últimos n (sv_right)
Conceptos · sub-vista, aritmética de punteros (data + offset), protección de límites
Contexto · "Dame la extensión de este archivo" (los últimos 4 caracteres de .txt) es un sv_right
de manual. Simplificación: una vista con los últimos n bytes.
Consigna · Implementá sv_right: devuelve una vista nueva con los últimos n bytes. Acá sí
hay que mover el inicio: data + (count - n). Protegé el caso n mayor que el largo (devolvé la
vista completa) y n = 0 (vista vacía), sin que la resta se dé vuelta (ojo con size_t, que no
tiene negativos).
Entrada · Una String_View y un n (size_t).
Salida · Una nueva vista con la parte pedida.
Escenario de prueba
sv = "hola mundo"
sv_right(sv, 5) → "mundo"
sv_right(sv, 0) → ""
sv_right(sv, 100) → "hola mundo"
La trampa del
size_t.count - nconsize_tyn > countno da un negativo: da un número gigante (le da la vuelta). Por eso primero hay que asegurarn <= count; recién después restar.
Identificadores sugeridos · sv_right, n, start, result
Criterios de aceptación
- El nuevo inicio es
data + (count - n)cuandon <= count. nmayor que el largo devuelve la vista completa.n = 0da una vista vacía.- No hay restas que "den la vuelta" por el
size_t.
PARTE 3 — Recorte (trim): sacar los bordes
Los archivos de configuración, las líneas de un CSV, lo que teclea un usuario… todo viene con
espacios de más. El trim los saca. Con String_View es casi gratis: recortar es mover dos
números.
3.1 isspace: qué cuenta como "espacio"
No es solo el espacio de la barra espaciadora. <ctype.h> te da isspace, que reconoce seis
caracteres en blanco:
| Carácter | Nombre | Cómo se escribe |
|---|---|---|
| espacio | space | ' ' |
| tabulación | tab | '\t' |
| salto de línea | line feed | '\n' |
| retorno de carro | carriage return | '\r' |
| tabulación vertical | vertical tab | '\v' |
| avance de formulario | form feed | '\f' |
isspace(c) devuelve distinto de cero ("verdadero") si c es uno de esos, y 0 ("falso") si no.
Es la forma portable de preguntar "¿es un blanco?", sin enumerar los seis a mano.
El detalle que casi nadie sabe. A
isspacehay que pasarle un valor que sea representable comounsigned charoEOF. Si le pasás uncharcon signo y el byte vale, por ejemplo,0xC3(el primer byte de unaáen UTF-8), uncharcon signo lo ve como negativo, y eso es comportamiento indefinido. La forma segura esisspace((unsigned char)c). Guardátelo.
3.2 Recortar a la izquierda: avanzar y achicar
sv_trim_left mira desde el frente y va descartando blancos. Cada blanco que saca avanza data
y resta 1 a count. Cuando topa con un byte que no es blanco, para.
antes : " hola "
^ data, count = 9
mira el 1º: ' ' → blanco → data++, count--
mira el 1º (ahora): ' ' → blanco → data++, count--
...
después:"hola "
^ data ya movido hasta la 'h', count = 6
Fijate que el texto original no cambia: sigue teniendo los tres espacios al principio en memoria. Lo que cambió es qué tramo mira la vista.
3.3 Recortar a la derecha: achicar el contador
sv_trim_right mira desde el fondo y da el mismo tratamiento, pero acortando count (el inicio no
se mueve).
antes : " hola "
^ mirá los últimos
mira el último: ' ' → blanco → count--
...
después:" hola"
^ count ya achicado
El mismo dato, dos preguntas.
trim_leftpregunta "¿el primero es blanco?";trim_right, "¿el último es blanco?". El primero essv.data[0]; el último essv.data[sv.count - 1]. ¡Cuidado concount == 0! (una vista vacía no tiene "último": hay que chequear el largo antes de mirar el último byte).
3.4 trim = trim_left + trim_right
sv_trim es literalmente llamar a las dos, en cualquier orden (no se pisan: una toca el frente y la
otra el fondo). El resultado: el texto "pelado", sin blancos en ninguno de los dos extremos.
" hola mundo " ──trim──► "hola mundo"
" " ──trim──► "" (una vista vacía: count = 0)
3.5 ¿Por qué estas reciben la vista por puntero?
Acá está la regla de oro del diseño de tu librería:
- Si una función solo mira y devuelve algo nuevo → recibe
String_Viewpor valor (una copia del pardata/count; cómodo y seguro). - Si una función modifica la vista misma (la avanza) → recibe
String_View *por puntero.
trim_left cambia la vista que le pasás ("consumila"), así que va por puntero: sv_trim_left(&sv).
Así, quien llama ve el cambio. (Alternativa válida: devolver la vista nueva. Elegí una convención y
mantenela en toda la librería.)
Práctica de la Parte 3
Ejercicio 18.6 — Recorte a la izquierda (sv_trim_left)
Conceptos · isspace, (unsigned char), punteros a struct, avance de inicio
Contexto · Al leer una línea de un archivo de configuración, el lado izquierdo casi siempre trae indentación o tabs. Simplificación: descartar los blancos del frente, avanzando la vista.
Consigna · Implementá sv_trim_left(String_View *sv): mientras el primer byte sea blanco
(isspace, con el cuidado del unsigned char), avanzá data y restá count. Detenete al
primer byte que no sea blanco o cuando la vista quede vacía. La función no devuelve nada: modifica
la vista recibida.
Entrada · Una vista, por puntero.
Salida · La misma vista, ya recortada por la izquierda (se ve en el main).
Escenario de prueba
antes : " hola " (4 espacios al frente)
después: "hola "
antes : "hola" (sin blancos)
después: "hola" (no cambia)
antes : " " (solo blancos)
después: "" (count = 0)
Identificadores sugeridos · sv_trim_left, sv, c
Criterios de aceptación
- Usa
isspacecon(unsigned char)(no con uncharcon signo). - Cada blanco del frente avanza
datay decrementacount. - Para en el primer byte no blanco (o al vaciarse).
- Modifica la vista por puntero; el texto original no se toca.
Ejercicio 18.7 — Recorte a la derecha (sv_trim_right)
Conceptos · isspace, count - 1, chequear count antes de mirar, achicar el largo
Contexto · Las líneas de un archivo suelen venir con '\r' (retorno de carro) o espacios al final.
Simplificación: descartar los blancos del fondo, achicando el largo.
Consigna · Implementá sv_trim_right(String_View *sv): mientras la vista no esté vacía y el
último byte (data[count - 1]) sea blanco, restá 1 a count. Chequeá siempre count > 0
antes de mirar el último byte (mirar data[count - 1] con count == 0 es leer fuera de límites).
Entrada · Una vista, por puntero. Salida · La misma vista, recortada por la derecha.
Escenario de prueba
antes : " hola " (espacios al fondo)
después: " hola"
antes : "hola\r" (un retorno de carro al final)
después: "hola"
antes : "" (vista vacía)
después: "" (no se accede a data[-1])
Identificadores sugeridos · sv_trim_right, sv
Criterios de aceptación
- Chequea
count > 0antes de leerdata[count - 1]. - Cada blanco del fondo decrementa
count(no muevedata). - Reconocen como blancos el
'\r'y demás (víaisspace). - Una vista vacía no produce accesos fuera de rango.
Ejercicio 18.8 — Recorte total (sv_trim) y la trampa del doble recorte
Conceptos · composición de funciones, orden de operaciones, vistas que quedan vacías
Contexto · Un parser quiere el campo "pelado": sin blancos de ningún lado. Simplificación: recortar frente y fondo de una vez.
Consigna · Implementá sv_trim llamando a sv_trim_left y a sv_trim_right (en el orden que
prefieras, y explicá en un comentario por qué el orden no importa). Comprobá el caso extremo: una
vista hecha solo de blancos tiene que terminar con count == 0, sin romperse.
Entrada · Varias vistas, con distinta cantidad de blancos. Salida · Cada vista ya recortada, entre corchetes.
Escenario de prueba
" hola mundo " → [hola mundo]
"hola" → [hola]
" " → [] (count = 0)
" " → [] (count = 0)
"a b" → [a b] (¡los espacios INTERNOS no se tocan!)
Ojo con esto.
trimsaca los blancos de los bordes, no los de adentro."a b"sigue siendo"a b". Confundir "recortar los bordes" con "sacar todos los blancos" es un bug clásico de parseo.
Identificadores sugeridos · sv_trim
Criterios de aceptación
sv_trimreusasv_trim_leftysv_trim_right(no reimplementa).- La vista "solo blancos" termina con
count == 0. - Los espacios internos se conservan.
- El comentario explica por qué el orden entre los dos recortes es indiferente.
PARTE 4 — Cortar por delimitador (chop y split)
Esta es la parte donde el paradigma se saca la foto. Con chop vas a partir un texto en pedazos
uno por uno, avanzando la vista, sin reservar un solo byte de memoria nueva.
4.1 La tijera: sv_chop_by_delim
Imaginate una tira larga de chicle y una tijera en la coma. chop hace tres cosas, en orden:
- Busca el delimitador (
sv_index_of). - Devuelve el trozo de antes del delimitador.
- Avanza la vista original para que arranque justo después del delimitador.
Pensalo como que le cortás el primer pedazo a la tira, y la tira que te queda (en *sv) empieza
después del corte.
antes: sv ──► "ana,luis,pepe"
chop(sv, ',')
devuelve: "ana" (el trozo de antes)
y deja: sv ──► "luis,pepe" (arranca DESPUÉS de la coma)
Y el caso especial que siempre hay que resolver: ¿y si ya no queda delimitador? (el último
pedazo). Entonces chop devuelve todo lo que queda y deja la vista vacía:
antes: sv ──► "pepe"
devuelve: "pepe"
y deja: sv ──► "" (count = 0 → el bucle terminó)
Ese vaciado es lo que hace que el bucle de split corte solo, sin necesitar un contador aparte.
Fijate que
chopcombina todo lo anterior. Usasv_index_of(Parte 2) para ubicar la coma y mueve el inicio (comosv_chop_left, Parte 2) para consumirla. Lo que ya construiste se vuelve a usar. Eso es una librería: piezas pequeñas que se combinan.
4.2 El bucle de split: partir sin malloc
Con chop, partir una línea es un bucle de tres líneas. La condición de corte es elegante: cuando la
vista queda vacía, se terminó.
línea: "ana,luis,pepe"
vuelta 1: chop → "ana" sv = "luis,pepe"
vuelta 2: chop → "luis" sv = "pepe"
vuelta 3: chop → "pepe" sv = "" ← sin coma, devuelve el resto
fin: sv vacía → el bucle corta
Cada pedazo es una vista nueva que apunta al mismo texto original. No copiaste nada, no pediste memoria, no liberaste nada. Eso es un parser de velocidad absurda.
El detalle de la coma final. Si el texto es
"a,b,", el últimochopdevuelve la vista vacía (entre la última coma y el final no hay nada). Un split correcto suele incluir ese último campo vacío. Decidí explícitamente qué querés y dejalo documentado: es una fuente inagotable de discusiones (mirá cómo"a,b,".split(',')se comporta distinto en cada lenguaje).
Práctica de la Parte 4
Ejercicio 18.9 — La tijera (sv_chop_by_delim)
Conceptos · sv_index_of, sub-vistas, pasar por puntero para consumir, avance de inicio
Contexto · Un tokenizador de CSV, de paths o de líneas clave=valor necesita sacar el siguiente
pedazo y dejar la vista lista para el siguiente. Simplificación: cortar por un delimitador.
Consigna · Implementá sv_chop_by_delim(String_View *sv, char delim):
- busca
delimconsv_index_of; - si lo encuentra en el índice
i, devolvésv_left(*sv, i)y dejá*svempezando después del delimitador (data + i + 1,count - i - 1); - si no lo encuentra (
-1), devolvé todo*svy dejá*svvacía (count = 0).
Entrada · Una vista por puntero y un delimitador. Salida · El pedazo de antes del delimitador; la vista original queda avanzada.
Escenario de prueba
sv = "ana,luis,pepe", delim = ','
chop → "ana" ; sv = "luis,pepe"
chop → "luis" ; sv = "pepe"
chop → "pepe" ; sv = "" (sin delimitador: devuelve el resto y vacía)
chop → "" ; sv = "" (ya estaba vacía)
sv = "sin,comas,aqui", delim = ';'
chop → "sin,comas,aqui" ; sv = "" (el delimitador no aparece)
Identificadores sugeridos · sv_chop_by_delim, sv, delim, i, piece
Criterios de aceptación
- Reusa
sv_index_of(no reimplementa la búsqueda). - Avanza la vista a
data + i + 1concount - i - 1cuando encuentra el delimitador. - Sin delimitador, devuelve el resto y deja la vista vacía.
- El delimitador no aparece en ningún pedazo devuelto.
Ejercicio 18.10 — El split completo (bucle, sin malloc)
Conceptos · bucle while con condición sobre count, chop, impresión de tokens
Contexto · El split es el pan de cada día: partir una línea de campos. Simplificación: imprimir
cada campo de una línea separada por comas, sin reservar memoria.
Consigna · Escribí un programa que tome una línea separada por comas (fija en el main, o leída con
fgets) y, con un bucle que llame a sv_chop_by_delim, imprima cada campo entre corchetes, uno
por línea. Al terminar, mostrá cuántos campos salieron. Agregá al menos un campo con espacios
alrededor para forzar un trim por campo (reusá sv_trim).
Entrada · Una línea con campos separados por comas. Salida · Un campo por línea y el total.
Escenario de prueba
entrada : "ana, luis ,pepe"
salida :
[ana]
[luis]
[pepe]
campos: 3
entrada : "solo"
salida :
[solo]
campos: 1
Regla de oro del bucle. La condición es
sv.count > 0. Cadachopachica o vacía la vista, así que el bucle siempre termina. Si te quedás en un bucle infinito, es porque olvidaste que elchopsin delimitador vacía la vista (revisá 18.9).
Identificadores sugeridos · sv, field, count, line
Criterios de aceptación
- El bucle usa
sv_chop_by_delimy corta cuando la vista queda vacía. - Cada campo se recorta con
sv_trim(los espacios de alrededor se van). - Se imprime un campo por línea.
- No hay ninguna llamada a
malloc(ni astrtok, que además rompe el texto original).
PARTE 5 — Reemplazar: mutación segura en un búfer
Llegó el momento incómodo: reemplazar. Acá la String_View muestra su límite, y entender por qué
es la clave de esta parte.
5.1 El problema: la vista no es dueña de la memoria
Una String_View es una ventana, no una casa: apunta a memoria que no le pertenece y que
normalmente es de solo lectura (recordá .rodata, Parte 1). Por eso no puede "crecer" el texto,
ni escribir en él.
¿Y cuando el reemplazo cambia el tamaño? Pasa siempre:
reemplazar "o" por "oo":
"hola" → "hoola" el texto CRECIÓ: ¿dónde metés el byte de más?
reemplazar "aa" por "b":
"baaanana" → "banana" el texto ENCOGIÓ
Si el resultado ocupa distinto que la fuente, no hay forma de escribirlo "en el mismo lugar". La
vista no tiene lugar propio. La solución honesta es: el que llama provee un búfer destino, y la
función escribe ahí. La String_View sigue siendo solo fuente de lectura.
Nadie miente en esta librería. Una operación que no cambia el tamaño (como reemplazar un carácter de 1 byte por otro de 1 byte) sí podría escribirse en el lugar… pero la vista es
const: no se toca. La regla clara y segura es: todas las transformaciones escriben en un búfer destino que vos manejás. Menos sorpresas, menos Segmentation Faults.
5.2 El contrato: "escribí en MI búfer, sin desbordarlo"
Toda función de reemplazo de tu librería sigue el mismo contrato, y conviene que lo memorices:
entradas:
sv → de dónde LEO (mi vista, const)
from → qué busco reemplazar
to → por qué lo cambio
out → dónde ESCRIBO (búfer que el que llama reservó)
outCap → cuántos bytes entran en out (SU tamaño)
salida:
cuántos bytes escribí realmente (para que sepas el largo del resultado)
Las tres reglas de oro de este contrato:
- Nunca escribas más de
outCapbytes. Si el resultado no entra, tenés que avisar (devolver un valor especial) o cortar. Jamás desbordar. - No confíes en que
outes "suficientemente grande". El que llama puede equivocarse; vos tenés que ser el guardián. - La vista de entrada es de solo lectura. Escribís solo en
out.
sv: "banana" from='a' to='o'
│
▼ (leo, nunca escribo acá)
"banana"
│
▼ (escribo el resultado, con cuidado de no pasarme)
out:│ b | o | n | o | n | o | \0? |
└───────────────────────────┘
outCap = tamaño REAL de out
¿Y el
'\0'al final del búfer? Depende del contrato que elijas. Si querés que el resultado sea también una cadena C usable con%s, tenés que reservar un byte para el terminador y escribirlo. Por eso el contrato desv_to_cstrpide "como máximooutCap - 1caracteres + el\0". Decidí y documentá: ¿el resultado lleva'\0'o no? (ConString_View, no lo necesita; paraprintf("%s"), sí.)
5.3 sv_replace_char: reemplazar un solo carácter
El caso más simple y el más común (cambiar comas por puntos, normalizar separadores). Recorrés la vista
byte a byte y, por cada uno, decidís qué escribís en out:
- si el byte es
from→ escribísto; - si no → escribís el byte tal cual.
Contás cuántos reemplazaste (para informar) y cuidás no pasarte de outCap.
Práctica de la Parte 5
Ejercicio 18.11 — Reemplazo de un carácter (sv_replace_char)
Conceptos · búfer destino, contrato out/outCap, recorrido acotado, no desbordar
Contexto · Normalizar un CSV (cambiar ; por ,), tapar separadores, sanear texto: reemplazar un
carácter es una operación de todos los días. Simplificación: reemplazar un carácter por otro,
escribiendo en un búfer provisto por quien llama.
Consigna · Implementá sv_replace_char(String_View sv, char from, char to, char *out, size_t outCap)
que copie sv a out, cambiando cada from por to, y devuelva cuántos bytes escribió. Reglas:
nunca escribir más de outCap bytes (si el resultado no entra, avisá devolviendo un valor imposible,
por ejemplo (size_t)-1, o cortá y documentalo). La vista sv no se modifica. Comprobá con un
texto que entre justo, y con uno que no entre.
Entrada · Una vista, el carácter origen, el destino, y un búfer con su capacidad. Salida · El texto reemplazado en el búfer y la cantidad de bytes escritos.
Escenario de prueba
sv = "banana", from='a', to='o', outCap = 10 (búfer grande)
resultado → "bonono"
bytes escritos → 6
sv = "a;b;c", from=';', to=',', outCap = 10
resultado → "a,b,c"
bytes escritos → 5
sv = "aaaaaaaaaa" (10 'a'), from='a', to='o', outCap = 4 (¡no entra!)
resultado → (error/corte) : NO se desborda el búfer
Identificadores sugeridos · sv_replace_char, from, to, out, outCap, written
Criterios de aceptación
- Escribe solo en
out;sv(o su memoria) no se toca. - Nunca escribe más de
outCapbytes (probado con el caso que no entra). - Devuelve la cantidad de bytes realmente escritos.
- Si no entra, avisa de forma predecible (nunca desborda ni crashea).
Ejercicio 18.12 — Reemplazo de una subcadena (sv_replace_all)
Conceptos · búsqueda de subcadena, búfer destino acotado, cambio de longitud, cálculo de tamaño
Contexto · Reemplazar todas las apariciones de una subcadena es el "buscar y reemplazar" de un
editor, o la limpieza de plantillas ({{NAME}}). Es más difícil porque cambia el largo.
Simplificación: reemplazar una subcadena needle por otra with, escribiendo en un búfer destino.
Consigna · Implementá sv_replace_all(String_View sv, String_View needle, String_View with, char *out, size_t outCap)
que recorra sv de izquierda a derecha; cada vez que encuentre needle, escriba with en out; si no,
copie el byte y avance uno. Nunca desbordes outCap. Devolvé cuántos bytes escribiste.
Entrada · La vista fuente, la subcadena a buscar, la subcadena de reemplazo, y el búfer destino con su capacidad. Salida · El texto con todas las apariciones reemplazadas y la cantidad de bytes escritos.
Escenario de prueba
sv = "ababab", needle = "ab", with = "X", outCap = 16
resultado → "XXX"
bytes escritos → 3
sv = "aaabaaa", needle = "aaa", with = "Z", outCap = 16
resultado → "ZbZ"
sv = "hola", needle = "xy", with = "!!", outCap = 16
resultado → "hola" (no hay coincidencias: se copia tal cual)
El caso que te muerde: coincidencias solapadas. En
"aaa"buscando"aa", ¿es una coincidencia o dos (con solape)? Decidilo y documentalo. La convención más común es no solapar: consumís laneedlecompleta y seguís después de ella. Definí tu comportamiento y respetalo.
Identificadores sugeridos · sv_replace_all, needle, with, out, outCap, written, i
Criterios de aceptación
- Reemplaza todas las apariciones (no solo la primera).
- Nunca escribe más de
outCapbytes. - Reusa
sv_find(Parte 6) osv_index_of(Parte 2) para ubicar las coincidencias. - Documenta qué hace con las coincidencias solapadas.
PARTE 6 — El catálogo completo: comparar, buscar, construir
Ya tenés el esqueleto. Esta parte redondea la librería con las operaciones que faltan para que sea de verdad usable. Ninguna es difícil, pero todas son indispensables.
6.1 Comparar sin strcmp: sv_eq
Comparar dos vistas es distinto que comparar dos cadenas C. Con cadenas C, strcmp camina hasta el
'\0' de cada una. Con vistas, vos ya sabés los largos: podés hacer dos chequeos rápidos.
La idea del contrato de sv_eq:
- Si los
countson distintos → ya son distintas (no compares nada más: un cortocircuito gratis). - Si los
countson iguales → recién ahí comparás byte a byte (conmemcmpo con un bucle).
Esto es más rápido y más seguro que strcmp, y funciona con textos que no terminan en '\0'.
Ojo con el
'\0'embebido.strcmp("a\0b", "a\0c")diría "iguales" (para en el primer\0).sv_eqlos ve distintos, porque compara loscount(3 vs 3) y byte a byte (el'\0'del medio también cuenta). Ese es un beneficio: tu librería sirve para datos, no solo para texto.
Y los "empieza con" / "termina con" (starts_with / ends_with) son comparaciones parciales:
dicen si el prefijo o el sufijo de una vista coincide con otra. Se usan a montones: reconocer
una extensión (.txt), una etiqueta (http://), un comentario (#).
6.2 Buscar una subcadena: sv_find y sv_contains
sv_index_of busca un carácter. sv_find busca una subcadena completa: "¿en qué posición
aparece needle dentro de haystack?". Es el "buscar" de un editor.
La forma directa: en cada posición i desde donde todavía quepa needle, compará needle contra la
sub-vista que arranca en i (¡reusás sv_left/sv_eq!). Si coincide, devolvés i. Si no aparece en
ninguna, -1.
haystack: b a n a n a
needle : a n a
posiciones a probar: i=0..3 (no más allá: no queda lugar)
i=0: "ban" vs "ana" → no
i=1: "ana" vs "ana" → ¡SÍ! → devuelve 1
sv_contains es solo sv_find(...) != -1: la pregunta "¿está?" como int (booleano).
6.3 Construir y cortar: sv_from_parts, sv_take/sv_drop
Para cerrar la librería, dos familias que vas a usar todo el tiempo:
sv_from_parts(data, count): envuelve un puntero y un largo sin llamar astrlen. Es el equivalente "crudo" desv_from_cstr, y la forma de crear una vista desde un tramo de memoria que ya conocés (por ejemplo, de unmalloc, o de un archivo leído confread).sv_take/sv_drop: la pareja canónica de los slice.sv_take(sv, n)= "quedate con los primerosn" (essv_left).sv_drop(sv, n)= "avanzán, tirando lo del frente" y devolvé el resto. Con estas dos, "moverse por el texto" se vuelve una receta: tomá lo que quieras, tirá el resto.
Por qué
from_partses clave. Cuando leas un archivo entero a memoria (capítulo15) y quieras partirlo en líneas, tenés el puntero y el largo en la mano: no hay'\0'ni razón para llamar astrlen.sv_from_parts(buf, n)te deja toda la librería disponible sobre datos crudos, sin copiar y sin perder tiempo midiendo.
Práctica de la Parte 6
Ejercicio 18.13 — Comparaciones (sv_eq, sv_starts_with, sv_ends_with)
Conceptos · cortocircuito por count, comparación byte a byte, prefijo/sufijo
Contexto · Un clasificador de archivos decide por la extensión; un enrutador, por el prefijo de una URL; un intérprete, por la primera palabra. Simplificación: tres comparaciones sobre vistas.
Consigna · Implementá:
sv_eq(a, b): devuelve distinto de cero si son iguales. Cortocircuito: si loscountdifieren, ya son distintas; si no, compará byte a byte.sv_starts_with(sv, prefix): ¿svempieza conprefix?sv_ends_with(sv, suffix): ¿svtermina consuffix?
Probá casos verdaderos y falsos, incluido el prefix/suffix más largo que la vista (debe dar
falso, sin romperse) y el caso vacío.
Entrada · Pares de vistas. Salida · El veredicto de cada comparación.
Escenario de prueba
sv_eq("hola", "hola") → sí
sv_eq("hola", "hola ") → no (count distinto: 4 vs 5)
sv_eq("hola", "HOla") → no (distingue mayúsculas)
sv_starts_with("informe.txt", "inf") → sí
sv_starts_with("informe.txt", "txt") → no
sv_starts_with("txt", "informe") → no (prefijo más largo que la vista)
sv_ends_with("informe.txt", ".txt") → sí
sv_ends_with("informe.txt", ".doc") → no
Identificadores sugeridos · sv_eq, sv_starts_with, sv_ends_with
Criterios de aceptación
sv_eqcortocircuita porcountantes de comparar bytes.- Las comparaciones son sensibles a mayúsculas.
prefix/suffixmás largos que la vista devuelven falso sin leer fuera de límites.- El caso vacío (
count == 0) se maneja bien.
Ejercicio 18.14 — Búsqueda de subcadena (sv_find, sv_contains)
Conceptos · búsqueda subcadena, reusar sv_eq/sub-vistas, centinela, complejidad
Contexto · "¿Este log contiene ERROR?", "¿en qué columna queda la clave?" — buscar subcadenas está
por todos lados. Simplificación: ubicar la primera aparición de una aguja en un pajar.
Consigna · Implementá:
sv_find(haystack, needle): devuelve el índice de la primera aparición deneedleenhaystack, o-1si no aparece. Reusásv_eqcomparando cada sub-vista candidata (omemcmp).sv_contains(haystack, needle): devuelve "verdadero" sineedleestá.
Casos a cubrir: coincidencia al principio, en el medio, al final, ausente, needle vacío (¿da 0?),
needle más largo que haystack (debe dar -1).
Entrada · Pajar y aguja (vistas).
Salida · El índice encontrado, o -1; y el veredicto de contains.
Escenario de prueba
sv_find("banana", "ana") → 1 (la primera)
sv_find("banana", "ban") → 0 (al principio)
sv_find("banana", "na") → 2
sv_find("banana", "xyz") → -1
sv_find("abc", "abcd") → -1 (aguja más larga que el pajar)
sv_find("abc", "") → 0 (aguja vacía: decidí y documentá)
sv_contains("hola mundo", "mundo") → sí
sv_contains("hola mundo", "Mundo") → no
Identificadores sugeridos · sv_find, sv_contains, haystack, needle, i
Criterios de aceptación
- El recorrido no prueba posiciones donde la aguja ya no entra.
- Reusa
sv_eq(omemcmp) para comparar; no reimplementa la igualdad. - Devuelve la primera aparición;
-1si no está. - El caso "aguja vacía" está decidido y documentado.
Ejercicio 18.15 — Construir y cortar (sv_from_parts, sv_take, sv_drop)
Conceptos · construir sin strlen, take/drop, avanzar la vista
Contexto · Al partir un archivo entero en memoria tenés el puntero y el largo: no querés medir de nuevo. Y para "caminar" por el texto, querés las dos operaciones de slice. Simplificación: construir una vista "cruda" y moverse por ella.
Consigna · Implementá:
sv_from_parts(const char *data, size_t count): envuelve puntero y largo sinstrlen.sv_take(sv, n): devuelve los primerosn(podés reusarsv_left).sv_drop(sv, n): avanza la vistanbytes (tirando el frente) y devuelve el resto (protegén > count).
Demostralo con un texto: creá la vista con from_parts, hacé un take de la primera palabra y un
drop para saltarla, e imprimí los pedazos.
Entrada · Un puntero y un largo (o un literal), y un n.
Salida · Los pedazos pedidos.
Escenario de prueba
sv = sv_from_parts("hola mundo", 10) (sin llamar a strlen)
sv_take(sv, 4) → "hola"
sv_drop(sv, 5) → "mundo" (saltea "hola " — 5 bytes)
sv_drop(sv, 100)→ "" (protegido: no da la vuelta)
además: sv_take(sv, 4) seguido de sv_left(sv,4) dan lo mismo (coherencia)
Identificadores sugeridos · sv_from_parts, sv_take, sv_drop, n
Criterios de aceptación
sv_from_partsno llama astrlen.sv_takecoincide consv_left.sv_dropconn > countdeja la vista vacía (sin resta que dé la vuelta).- Los pedazos apuntan al texto original (no hay copias).
Ejercicio 18.16 — El ejercicio jefe: un parser de configuración
Conceptos · todo: vistas, trim, chop, index_of, eq, starts_with, sin malloc
Contexto · Un archivo de configuración estilo INI tiene líneas clave = valor, comentarios que
empiezan con # o ;, y líneas en blanco que se ignoran. Simplificación: un parser que lea el
contenido y muestre cada par clave = valor ya limpio, sin reservar memoria.
Consigna · Escribí un programa que:
- Lea la entrada completa de
stdina un búfer acotado (por ejemplo 4 KB) leyendo confreadofgetsen bucle (capítulos14/15). - Cree una vista del contenido con
sv_from_parts. - Recorra la vista línea por línea (usá
\ncomo delimitador dechop). - Por cada línea:
sv_trim; si queda vacía, ignorala; si empieza con#o;, es un comentario (ignorala o mostrala aparte). - Partila en
claveyvalorconsv_chop_by_delim(line, '='); recortá ambos consv_trim. - Imprimí cada par como
clave→valor, separando la salida normal de los avisos (líneas raras) porstdout/stderr. - Al final, mostrá cuántas claves válidas se leyeron.
Entrada · Un archivo INI por stdin (con comentarios, líneas en blanco y espacios de más).
Salida · Los pares clave → valor, más un resumen.
Escenario de prueba
entrada (stdin):
# configuración del servidor
host = 127.0.0.1
; puerto por defecto
port= 8080
nombre = Servidor de Pruebas
salida (stdout):
host → 127.0.0.1
port → 8080
nombre → Servidor de Pruebas
claves válidas: 3
entrada (stdin):
sin igual aca
salida:
(stderr) línea sin '=' (ignorada): "sin igual aca"
claves válidas: 0
Identificadores sugeridos · sv, line, key, value, validCount, buffer, BUFFER_SIZE
Restricciones/pistas
- Prohibido
mallocy prohibidostrtok(rompe el texto original metiendo'\0'). Todo el parseo sale de tu librería. - Reusá lo que ya escribiste:
sv_from_parts,sv_chop_by_delim,sv_trim,sv_starts_with,sv_eq. - Acotá siempre el búfer de lectura y detectá si el archivo no entra.
- La vista del contenido es de solo lectura: no modifiques el búfer mientras parseás.
- Ojo con una línea con más de un
=: ¿te quedás con la primera parte como clave y el resto como valor? Decidilo y documentalo. - Avisos por
stderr, resultado porstdout.
Criterios de aceptación
- No hay
mallocnistrtoken ninguna parte. - El contenido se envuelve con
sv_from_parts(sinstrlen). - Se recorre línea por línea con
sv_chop_by_delim(sv, '\n'). - Cada línea se recorta con
sv_trim; las vacías y los comentarios se ignoran. - Clave y valor se recortan por separado.
- Una línea sin
=se reporta porstderry no rompe el parser. - El conteo de claves válidas coincide con el escenario.
- El búfer de lectura está acotado y no hay desbordamientos.
18.17 Errores comunes
- Recorrer sin acotar por
count. Tentador usarwhile (*p) ..., pero tu vista no promete un'\0'. Siempre iterá de0acount - 1. - Devolver
-1en unsize_t.size_tno tiene negativos: el "no encontrado" va en un tipo con signo (int,ptrdiff_t). - Restar restar con
size_tcuandon > count.count - nda la vuelta y se vuelve gigante. Chequeán <= countantes de restar. - Usar
%scon una vista.%scamina hasta un'\0'que puede no existir. Usá%.*s(SV_Fmt/SV_Arg). - Pasar
charcon signo aisspace. Un byte>= 0x80se vuelve negativo → comportamiento indefinido. Pasá(unsigned char)c. - Modificar memoria de solo lectura. Un literal (
"...") vive en.rodata: escribir ahí es Segmentation Fault. Tu vista esconstpor eso. - Escribir a
data[count - 1]concount == 0. Leer/escribir "un byte antes del inicio" es acceso fuera de límites. Chequeá el largo antes de tocar el último byte. - Confundir trim con "sacar todos los blancos".
trimtoca solo los bordes; los internos quedan. - Reemplazar sin controlar
outCap. Un reemplazo que crece el texto desborda el búfer si no chequeás el tamaño. Sé el guardián. - Usar
strtok. Rompe el texto original (mete'\0') y no es reentrante. Conchopno lo necesitás. - Casos solapados en
replace_all("aaa"con"aa"): definí la convención y documentala. sv_findcon aguja vacía: decidí (y documentá) si devuelve0,count, o es un error.
18.18 Cierre
Si tuvieras que quedarte con una sola idea, que sea esta: un texto es una calle de bytes, y una
String_View es una ventana apoyada sobre esa calle. Todo lo demás se deduce de ahí.
- Dejaste de depender del
'\0': en vez de caminar hasta el cartel, guardás el largo (count) y listo. - Aprendiste a mirar y cortar sin copiar: mover
datay ajustarcountes toda tu librería. Por eso funciona sinmallocy a velocidad de vértigo. - Diseñaste contratos claros: quién pasa por valor, quién por puntero, dónde va cada centinela, dónde vive el resultado de un reemplazo (en tu búfer, nunca en la vista).
- Escribiste una librería completa: conversión, impresión, búsqueda, sub-vistas, recorte, corte por delimitador, reemplazo, comparación y un parser de configuración sin reservar un solo byte.
Resumen de la caja de herramientas.
| Quiero… | Función | Idea en una frase |
|---|---|---|
| Convertir una cadena C | sv_from_cstr |
Mide una vez con strlen y envuelve. |
| Construir sin medir | sv_from_parts |
Envolvé puntero + largo que ya tenés. |
Imprimir sin '\0' |
SV_Fmt / SV_Arg |
%.*s: decile a printf cuánto mirar. |
| Buscar un carácter | sv_index_of |
Índice, o -1. |
| Buscar una subcadena | sv_find / sv_contains |
Comparar la aguja en cada posición. |
| Quedarme con el principio | sv_left / sv_take |
Misma data, count más chico. |
| Quedarme con el final | sv_right |
Mover data, ajustar count. |
| Saltear el frente | sv_drop / sv_chop_left |
Avanzar data. |
| Sacar blancos | sv_trim_left / _right / sv_trim |
Ajustar inicio y/o largo. |
| Partir por delimitador | sv_chop_by_delim |
Devolver el trozo y avanzar la vista. |
| Comparar | sv_eq |
Si count difiere, ya son distintas. |
| Prefijo / sufijo | sv_starts_with / sv_ends_with |
Comparación parcial. |
| Reemplazar | sv_replace_char / sv_replace_all |
Escribir en tu búfer, sin desbordar. |
| Hacia cadena C | sv_to_cstr |
Copiar y agregar el '\0'. |
El puente a lo que sigue. Ahora que tenés vistas, el texto deja de ser un enemigo. Con la misma idea (puntero + largo) podés construir un arena de strings (capítulo
12), un tokenizador de un lenguaje, o el parser de una línea de comandos sin reservar memoria. Y cuando una función vieja te pida una cadena C, sabés cerrar el círculo:sv_to_cstry el\0vuelven a escena, pero solo cuando vos querés. La calle y la ventana ahora las manejás vos.