Cadenas en C: trim, split, unir y convertir a número sin desbordar

Las operaciones con cadenas que otros lenguajes traen hechas y C no: quitar los espacios de los extremos, dividir una cadena sin perder los campos vacíos, unir varias sin desbordar el búfer, convertir texto a número de forma segura y cambiar a mayúsculas, con las trampas de cada una.

C no tiene un tipo de texto ni métodos como split o trim: las cadenas son arrays de char terminados en '\0' y hay que trabajar con las funciones de string.h o escribir las propias. Estas son las operaciones que más se necesitan, escritas de forma que no se salgan nunca del búfer. Lo básico (el carácter nulo, strlen, strcmp) está en la lección 4 del curso.

Quitar los espacios de los extremos (trim)

#include <stdio.h>
#include <string.h>
#include <ctype.h>

/* Quita los espacios del principio y del final, en el mismo búfer. */
char *recortar(char *s)
{
    while (isspace((unsigned char) *s)) s++;
    size_t n = strlen(s);
    while (n > 0 && isspace((unsigned char) s[n - 1])) n--;
    s[n] = '\0';
    return s;
}

int main(void)
{
    char a[] = "   hola mundo  \n";
    char b[] = "\t\t";
    printf("[%s]\n", recortar(a));
    printf("[%s]\n", recortar(b));
    return 0;
}
salida[hola mundo]
[]

La función no copia nada: avanza el puntero de inicio y escribe un nulo después del último carácter útil. Por eso devuelve un puntero, que puede no ser el principio del array original (si el array se reservó con malloc, el free se hace sobre el puntero original, no sobre el devuelto). El cast a unsigned char es obligatorio con isspace y compañía: con un byte de una letra acentuada, que es negativo como char, sería comportamiento indefinido.

Dividir una cadena (split)

strtok divide una cadena, pero guarda su posición en una variable global (no sirve para dos cadenas a la vez ni con hilos) y se salta los campos vacíos. Su versión reentrante, strtok_r de POSIX, resuelve lo primero. Para no perder los campos vacíos, lo más claro es buscar el separador con strchr:

#include <stdio.h>
#include <string.h>

/* Divide s por el separador sep, sin perder los campos vacíos. Escribe los punteros en partes
   (como mucho max) y devuelve cuántas hay. Modifica s: cambia cada separador por un nulo. */
size_t dividir(char *s, char sep, char *partes[], size_t max)
{
    size_t n = 0;
    while (n < max) {
        partes[n++] = s;
        char *p = strchr(s, sep);
        if (p == nullptr) break;
        *p = '\0';
        s = p + 1;
    }
    return n;
}

int main(void)
{
    char ruta[] = "/usr/local//bin";
    char *partes[8];
    size_t n = dividir(ruta, '/', partes, 8);
    for (size_t i = 0; i < n; i++) {
        printf("%zu: [%s]\n", i, partes[i]);
    }
    return 0;
}
salida0: []
1: [usr]
2: [local]
3: []
4: [bin]

Los campos vacíos (el del principio y el de //) se conservan, que es lo que se espera al dividir rutas, CSV sencillos o líneas de configuración. Las partes son punteros dentro de la cadena original: no hay que liberarlas, pero dejan de ser válidas si la cadena se libera o se modifica.

Unir cadenas sin desbordar

#include <stdio.h>
#include <string.h>

/* Une las cadenas con el separador en destino, sin pasarse de tam. Devuelve false si se recortó. */
bool unir(char *destino, size_t tam, const char *partes[], size_t n, const char *sep)
{
    size_t usado = 0;
    destino[0] = '\0';
    for (size_t i = 0; i < n; i++) {
        int escrito = snprintf(destino + usado, tam - usado, "%s%s", i ? sep : "", partes[i]);
        if (escrito < 0 || (size_t) escrito >= tam - usado) return false;
        usado += (size_t) escrito;
    }
    return true;
}

int main(void)
{
    const char *etiquetas[] = {"c", "c23", "gcc", "seguridad"};
    char buf[32];
    if (unir(buf, sizeof buf, etiquetas, 4, ", ")) printf("[%s]\n", buf);

    char corto[12];
    if (!unir(corto, sizeof corto, etiquetas, 4, ", ")) printf("No cabe; queda [%s]\n", corto);
    return 0;
}
salida[c, c23, gcc, seguridad]
No cabe; queda [c, c23, gcc]

En vez de strcat, que recorre la cadena desde el principio en cada llamada y no sabe cuánto cabe, se lleva la cuenta de lo usado y se escribe con snprintf justo detrás. Si no cabe, el resultado queda recortado pero siempre terminado en nulo, y la función lo dice.

Convertir texto a número sin sorpresas

atoi es la forma que aparece en todos los tutoriales, y la peor: si el texto no es un número devuelve 0, sin forma de distinguirlo de un «0» de verdad, y si el número no cabe el comportamiento es indefinido. strtol informa de todo:

#include <stdio.h>
#include <stdlib.h>
#include <errno.h>
#include <limits.h>

bool a_entero(const char *s, int *resultado)
{
    char *fin;
    errno = 0;
    long v = strtol(s, &fin, 10);
    if (fin == s || *fin != '\0') return false;
    if (errno == ERANGE || v < INT_MIN || v > INT_MAX) return false;
    *resultado = (int) v;
    return true;
}

int main(void)
{
    const char *pruebas[] = {"42", "-7", "12abc", "", "99999999999", "0"};
    for (int i = 0; i < 6; i++) {
        int n;
        if (a_entero(pruebas[i], &n)) {
            printf("%-12s -> %d\n", pruebas[i], n);
        } else {
            printf("%-12s -> no es un int válido (atoi daría %d)\n", pruebas[i], atoi(pruebas[i]));
        }
    }
    return 0;
}
salida42           -> 42
-7           -> -7
12abc        -> no es un int válido (atoi daría 12)
             -> no es un int válido (atoi daría 0)
99999999999  -> no es un int válido (atoi daría 1215752191)
0            -> 0

Tres comprobaciones: que se haya leído algo (fin == s significa que no había ningún dígito), que no sobre nada detrás (*fin != '\0') y que el número quepa (errno vale ERANGE si no cabe en un long, y luego el rango de int). atoi devuelve 12 con «12abc», 0 con la cadena vacía y un número sin sentido con el que no cabe.

Mayúsculas, minúsculas y buscar sin distinguirlas

#include <stdio.h>
#include <ctype.h>
#include <strings.h>

void a_mayusculas(char *s)
{
    for (; *s != '\0'; s++) {
        *s = (char) toupper((unsigned char) *s);
    }
}

int main(void)
{
    char texto[] = "Hola, señor";
    a_mayusculas(texto);
    printf("%s\n", texto);
    printf("%d\n", strcasecmp("Decodigo", "DECODIGO") == 0);
    return 0;
}
salidaHOLA, SEñOR
1

toupper convierte letra a letra según el locale, y con el locale «C» por omisión solo conoce el ASCII: la ñ y las vocales con tilde se quedan igual, porque en UTF-8 son dos bytes que no son letras para él. strcasecmp (de POSIX, en strings.h) compara sin distinguir mayúsculas, con la misma limitación. Para texto en español con acentos, hacen falta funciones de caracteres anchos o una biblioteca como ICU.

Todo el código de esta página se compiló con GCC 15.3 (C23, -Wall -Wextra, sin avisos) y se ejecutó en Linux, en un contenedor limpio sin acceso a la red, antes de publicarla; las salidas están copiadas de esa ejecución.