Arrays y cadenas en C

Lección 4 de 8: arrays y cadenas en C. Aquí aparece lo que hace a C distinto de casi todos los lenguajes: los arrays no saben cuánto miden y nadie comprueba que no te salgas, y el texto no es más que bytes terminados en cero. Verás cómo trabajar con ambos, las funciones de string.h y cómo AddressSanitizer encuentra los desbordamientos de búfer antes de que lo haga otro.

Arrays

Un array es una fila de elementos del mismo tipo, uno detrás de otro en la memoria. Su tamaño se fija al declararlo y los índices empiezan en 0:

#include <stdio.h>

int main(void)
{
    int ventas[7] = {120, 95, 140, 80, 0, 210, 175};
    double medidas[] = {1.5, 2.25, 3.0};
    int ceros[100] = {};

    size_t n = sizeof ventas / sizeof ventas[0];
    int total = 0;
    for (size_t i = 0; i < n; i++) {
        total += ventas[i];
    }
    printf("%zu días, total %d, media %.1f\n", n, total, (double) total / n);
    printf("Primer día %d, último %d\n", ventas[0], ventas[n - 1]);
    printf("%zu medidas, ceros[99] = %d\n", sizeof medidas / sizeof medidas[0], ceros[99]);
    return 0;
}
salida7 días, total 820, media 117.1
Primer día 120, último 175
3 medidas, ceros[99] = 0

Si das los valores, el tamaño puede omitirse (medidas[]), y un inicializador vacío, = {} (C23), pone todo a cero. El truco sizeof ventas / sizeof ventas[0] calcula cuántos elementos hay: bytes del array entre bytes de un elemento. size_t es el tipo sin signo que usa C para tamaños e índices.

Pasar un array a una función

Al pasar un array a una función no se copia: la función recibe la dirección de su primer elemento y pierde la información del tamaño. Por eso en C el tamaño siempre viaja aparte:

#include <stdio.h>

int maximo(const int datos[], size_t n)
{
    int max = datos[0];
    for (size_t i = 1; i < n; i++) {
        if (datos[i] > max) max = datos[i];
    }
    return max;
}

void duplicar(int datos[], size_t n)
{
    for (size_t i = 0; i < n; i++) {
        datos[i] *= 2;
    }
}

int main(void)
{
    int puntos[] = {12, 48, 7, 31};
    size_t n = sizeof puntos / sizeof puntos[0];

    printf("Máximo: %d\n", maximo(puntos, n));
    duplicar(puntos, n);
    printf("Tras duplicar: %d %d %d %d\n", puntos[0], puntos[1], puntos[2], puntos[3]);
    return 0;
}
salidaMáximo: 48
Tras duplicar: 24 96 14 62

Fíjate en dos cosas. duplicar sí cambia el array original, al contrario que con las variables sueltas de la lección 3, porque recibe dónde está. Y maximo declara const int datos[] para prometer que no lo modificará: si lo intentara, no compilaría.

El error que te ahorra: sizeof de un array dentro de una función

#include <stdio.h>

void contar(int datos[])
{
    printf("%zu\n", sizeof datos / sizeof datos[0]);
}

int main(void)
{
    int puntos[10] = {};
    contar(puntos);
    return 0;
}
gcc: avisosizeof_param.c: In function 'contar':
sizeof_param.c:5:28: warning: 'sizeof' on array function parameter 'datos' will return size of 'int *' [-Wsizeof-array-argument]
    5 |     printf("%zu\n", sizeof datos / sizeof datos[0]);
      |                            ^~~~~
sizeof_param.c:3:17: note: declared here
    3 | void contar(int datos[])
      |             ~~~~^~~~~~~

Dentro de la función, datos ya no es un array de 10, sino un puntero de 8 bytes, así que la división daría 2. El truco de sizeof solo funciona donde el array se declaró.

Matrices

#include <stdio.h>

int main(void)
{
    int tablero[3][3] = {
        {1, 0, 2},
        {0, 1, 2},
        {2, 0, 1},
    };

    for (int fila = 0; fila < 3; fila++) {
        for (int col = 0; col < 3; col++) {
            printf("%c ", ".XO"[tablero[fila][col]]);
        }
        printf("\n");
    }
    bool diagonal = tablero[0][0] == tablero[1][1] && tablero[1][1] == tablero[2][2];
    printf("¿Diagonal completa? %s\n", diagonal ? "sí" : "no");
    return 0;
}
salidaX . O 
. X O 
O . X 
¿Diagonal completa? sí

Una matriz es un array de arrays, guardado fila tras fila. El ".XO"[n] es un pequeño truco: una cadena es un array de caracteres, así que se puede indexar directamente para traducir 0, 1 y 2 en un símbolo.

Nadie comprueba los límites

Esta es la diferencia más importante entre C y casi cualquier otro lenguaje: acceder fuera de un array no da error. El programa lee o escribe en la memoria que haya al lado, que puede ser otra variable, la dirección de vuelta de la función o nada. Es comportamiento indefinido: puede funcionar, dar basura o romperse, y cambiar al cambiar de compilador.

Para encontrar estos errores existe AddressSanitizer (ASan), incluido en GCC y Clang. Se activa con -fsanitize=address: el programa va algo más lento y gasta más memoria, pero se detiene en el primer acceso indebido y dice dónde:

#include <stdio.h>

int main(void)
{
    int notas[5] = {7, 8, 6, 9, 10};
    int suma = 0;

    for (int i = 0; i <= 5; i++) {
        suma += notas[i];
    }
    printf("Suma: %d\n", suma);
    return 0;
}
gcc -Wall -Wextra -g -fsanitize=address fuera_limites.c -o fuera_limites
./fuera_limites
salida con error==66==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x7a3ef0800034 at pc 0x0000004013a2 bp 0x7ffd9edc00d0 sp 0x7ffd9edc00c8
READ of size 4 at 0x7a3ef0800034 thread T0
    #0 0x0000004013a1 in main /curso/fuera_limites.c:9
…
    #0 0x0000004011a5 in main /curso/fuera_limites.c:4
…
    [32, 52) 'notas' (line 5) <== Memory access at offset 52 overflows this variable
…
SUMMARY: AddressSanitizer: stack-buffer-overflow /curso/fuera_limites.c:9 in main

El informe, del que dejamos aquí las líneas importantes, dice el tipo de fallo (stack-buffer-overflow: desbordamiento en un array de la pila), si fue lectura o escritura y de cuántos bytes, la línea exacta (fuera_limites.c:9) y qué variable era: notas, de 20 bytes, y el acceso cayó justo en el byte 52, al terminar. El culpable es el <=: con 5 elementos, los índices válidos van de 0 a 4. Sin ASan, este programa suele terminar «bien» con una suma equivocada. Compila con -fsanitize=address,undefined siempre que estés desarrollando o probando.

Cadenas: arrays de char con un nulo al final

C no tiene un tipo para el texto. Una cadena es un array de char que termina con el carácter nulo, '\0' (el byte 0), y todas las funciones de texto recorren la memoria hasta encontrarlo:

Un array de 8 caracteres que guarda Ana: las posiciones 0 a 2 tienen A, n y a; la 3 tiene el carácter nulo; el resto no se usa. strlen da 3 porque cuenta hasta el nulo; sizeof da 8, el tamaño del arraychar nombre[8] = "Ana"; en memoria'A'[0]'n'[1]'a'[2]'\0'[3]?[4]?[5]?[6]?[7]strlen(nombre) es 3: cuenta hasta el \0sizeof(nombre) es 8: el tamaño del array, se use o noSin el \0 final, ninguna función sabría dónde termina el texto.

Una cadena ocupa sus letras más un byte para el '\0'. El array puede ser más grande que el texto. Esquema: decodigo.com.
#include <stdio.h>
#include <string.h>

int main(void)
{
    char nombre[8] = "Ana";
    const char *saludo = "Hola";

    printf("%s tiene %zu letras en un array de %zu\n", nombre, strlen(nombre), sizeof nombre);
    printf("\"%s\" ocupa %zu bytes\n", saludo, sizeof "Hola");

    for (size_t i = 0; nombre[i] != '\0'; i++) {
        printf("[%c]", nombre[i]);
    }
    printf("\n");

    nombre[0] = 'E';
    printf("%s\n", nombre);
    return 0;
}
salidaAna tiene 3 letras en un array de 8
"Hola" ocupa 5 bytes
[A][n][a]
Ena

Hay dos formas de tener una cadena. char nombre[8] = "Ana" crea un array propio y copia el texto dentro, así que se puede modificar. const char *saludo = "Hola" apunta a un literal que el programa guarda en memoria de solo lectura: intentar cambiarlo es comportamiento indefinido, y el const hace que el compilador lo impida.

Las funciones de string.h

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

int main(void)
{
    const char *a = "manzana";
    const char *b = "mango";

    printf("strlen: %zu\n", strlen(a));
    printf("strcmp: %d\n", strcmp(a, b) > 0);
    printf("strncmp 3: %d\n", strncmp(a, b, 3) == 0);
    printf("strchr: %s\n", strchr(a, 'z'));
    printf("strstr: %s\n", strstr("tutorial de C en español", "de C"));

    char ruta[32];
    snprintf(ruta, sizeof ruta, "%s/%s.txt", "datos", b);
    printf("snprintf: %s\n", ruta);
    return 0;
}
salidastrlen: 7
strcmp: 1
strncmp 3: 1
strchr: zana
strstr: de C en español
snprintf: datos/mango.txt
  • FunciónHaceOjo
  • strlen(s)cuenta los caracteres hasta el \0cuenta bytes, no letras (ver UTF-8)
  • strcmp(a, b)compara: 0 si iguales, <0 o >0 según el ordennunca compares cadenas con ==
  • strchr, strstrbuscan un carácter o un textodevuelven NULL si no está
  • snprintf(buf, tam, ...)escribe con formato sin pasarse de tamla forma segura de montar cadenas
  • strcpy, strcat, sprintfcopian, concatenan, formateanno saben cuánto cabe: evítalas
  • strncpycopia como mucho n bytespuede dejar la cadena sin \0

El error que te ahorra: comparar cadenas con ==

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

int main(void)
{
    char respuesta[10] = "si";

    if (respuesta == "si") {
        printf("Iguales con ==\n");
    }
    if (strcmp(respuesta, "si") == 0) {
        printf("Iguales con strcmp\n");
    }
    return 0;
}
gcc: avisocomparar_mal.c: In function 'main':
comparar_mal.c:8:19: warning: comparison with string literal results in unspecified behavior [-Waddress]
    8 |     if (respuesta == "si") {
      |                   ^~
salidaIguales con strcmp

== compara dónde están las dos cadenas en memoria, no su contenido, y un array propio nunca está en el mismo sitio que un literal. Para el contenido, strcmp.

Cadenas y UTF-8

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

int main(void)
{
    const char *palabras[] = {"ano", "año", "corazón", "€"};
    for (size_t i = 0; i < 4; i++) {
        printf("%-10s strlen %zu\n", palabras[i], strlen(palabras[i]));
    }
    return 0;
}
salidaano        strlen 3
año       strlen 4
corazón   strlen 8
€        strlen 3

Hoy casi todo el texto está en UTF-8, donde la ñ o las vocales con tilde ocupan 2 bytes y el símbolo del euro, 3. Para C son simplemente bytes, así que strlen cuenta bytes y no letras, y cortar una cadena por la mitad puede partir un carácter. Fíjate además en la columna: %-10s rellena hasta 10 bytes, no letras, y por eso las palabras con acentos la descuadran. Para tratar texto con acentos en serio hacen falta bibliotecas como ICU, o como mínimo contar solo los bytes que no empiezan por 10 en binario, que son los que inician un carácter.

Ojo con la seguridad: el desbordamiento de búfer

strcpy(destino, origen) copia hasta encontrar el \0 de origen, sin saber cuánto cabe en destino. Si el origen viene de fuera y es más largo, se escribe sobre la memoria de al lado. Es el fallo de seguridad más famoso de C. AddressSanitizer lo detecta así:

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

int main(int argc, char *argv[])
{
    char usuario[8];
    if (argc > 1) {
        strcpy(usuario, argv[1]);
        printf("Hola, %s\n", usuario);
    }
    return 0;
}
salida con error (./strcpy_mal maria_fernanda_lopez)==189==ERROR: AddressSanitizer: stack-buffer-overflow on address 0x72de14500028 at pc 0x76de16c02fc4 bp 0x7ffefa2ea340 sp 0x7ffefa2e9b00
WRITE of size 21 at 0x72de14500028 thread T0
…
    #1 0x00000040124a in main /curso/strcpy_mal.c:8
…
    #0 0x0000004011a5 in main /curso/strcpy_mal.c:5
…
    [32, 40) 'usuario' (line 6) <== Memory access at offset 40 overflows this variable
…
SUMMARY: AddressSanitizer: stack-buffer-overflow /curso/strcpy_mal.c:8 in main

La versión segura limita la copia al tamaño del destino y comprueba si el texto cabía. snprintf devuelve cuántos caracteres habría escrito, así que se puede detectar el recorte:

#include <stdio.h>

int main(int argc, char *argv[])
{
    char usuario[8];
    if (argc > 1) {
        int n = snprintf(usuario, sizeof usuario, "%s", argv[1]);
        if (n < 0 || (size_t) n >= sizeof usuario) {
            fprintf(stderr, "Nombre demasiado largo (máximo %zu)\n", sizeof usuario - 1);
            return 1;
        }
        printf("Hola, %s\n", usuario);
    }
    return 0;
}
salida (./strcpy_bien maria_fernanda_lopez)Nombre demasiado largo (máximo 7)

Más en Los fallos de seguridad típicos de C.

Resumen

  • ParaEscribeRecuerda
  • Cuántos elementossizeof a / sizeof a[0]solo donde se declaró el array
  • Pasar un arrayf(a, n)el tamaño viaja aparte
  • Prometer no modificarconst int a[]el compilador lo vigila
  • Detectar accesos fuera-fsanitize=addressC no comprueba nada
  • Longitud de una cadenastrlen(s)en bytes, sin el \0
  • Comparar cadenasstrcmp(a, b) == 0nunca ==
  • Montar o copiar textosnprintf(buf, sizeof buf, ...)comprueba si se recortó

Retos

Reto 1. Escribe una función void invertir(char *s) que dé la vuelta a una cadena en su sitio, sin usar otro array: "decodigo" debe quedar "ogidoced".

Ver la solución
#include <stdio.h>
#include <string.h>

void invertir(char *s)
{
    size_t n = strlen(s);
    if (n < 2) return;
    for (size_t i = 0, j = n - 1; i < j; i++, j--) {
        char tmp = s[i];
        s[i] = s[j];
        s[j] = tmp;
    }
}

int main(void)
{
    char palabra[] = "decodigo";
    invertir(palabra);
    printf("%s\n", palabra);

    char vacia[] = "";
    invertir(vacia);
    printf("[%s]\n", vacia);
    return 0;
}
salidaogidoced
[]

Dos índices que avanzan desde los extremos hacia el centro. El if (n < 2) no es un adorno: con una cadena vacía, n - 1 sobre un size_t daría la vuelta hasta el número más grande posible.

Reto 2. Cuenta cuántas veces aparece cada vocal (sin tildes) en una frase, con un array de 5 contadores.

Ver la solución
#include <stdio.h>
#include <ctype.h>
#include <string.h>

int main(void)
{
    const char *frase = "En un lugar de la Mancha, de cuyo nombre no quiero acordarme";
    const char *vocales = "aeiou";
    int cuenta[5] = {};

    for (size_t i = 0; frase[i] != '\0'; i++) {
        const char *p = strchr(vocales, tolower((unsigned char) frase[i]));
        if (p != nullptr && *p != '\0') {
            cuenta[p - vocales]++;
        }
    }
    for (int v = 0; v < 5; v++) {
        printf("%c: %d\n", vocales[v], cuenta[v]);
    }
    return 0;
}
salidaa: 6
e: 6
i: 1
o: 5
u: 4

strchr devuelve dónde está la letra dentro de "aeiou", y restar las dos direcciones da su posición, de 0 a 4. La comprobación *p != '\0' hace falta porque strchr también encuentra el nulo final. El cast a unsigned char antes de tolower evita un comportamiento indefinido con bytes de letras acentuadas.

Reto 3. ¿Qué escribe? char s[] = "hola"; printf("%zu %zu\n", sizeof s, strlen(s));

Ver la solución
#include <stdio.h>
#include <string.h>

int main(void)
{
    char s[] = "hola";
    printf("%zu %zu\n", sizeof s, strlen(s));
    return 0;
}
salida5 4

El array mide 5 bytes, las cuatro letras más el \0, pero la cadena tiene 4 caracteres.

Todo el código de esta lección se compiló con GCC 15.3 (C23, -Wall -Wextra) y se ejecutó en un contenedor limpio antes de publicarla; los avisos del compilador y las salidas están copiados de esa ejecución. ¿Algo no te compila? Revisa tu versión con gcc --version: hace falta GCC 15 o Clang 19 para todo lo de C23.