</>waridocu

C++

Arrays y cadenas de estilo C

Arrays de tamaño fijo, cómo se recorren, qué son las cadenas terminadas en cero y por qué son el puente hacia los punteros.

cpparrayscadenasc-stringsmemoria

De dónde venimos

Hasta ahora cada dato tenía su propia variable: nota1, nota2. Si quisieras el promedio de 300 notas con lo visto en Variables y tipos, tendrías que declarar 300 variables. Evidentemente no es así como se hace.

Esta página introduce la primera estructura para guardar muchos valores del mismo tipo bajo un solo nombre, y combina directamente dos cosas que ya sabes: el bucle for para recorrerlos y las funciones para pasarlos a otro lado. Ese último punto trae una sorpresa que es, literalmente, la puerta de entrada a los punteros.

Qué es un array

Un array (arreglo) es un bloque de memoria contiguo que contiene un número fijo de elementos del mismo tipo. “Contiguo” significa que los elementos están pegados uno detrás de otro en memoria, sin huecos: el elemento 5 está inmediatamente después del 4.

Ejemplo: declarar y usar un array de enteroscpp
#include <iostream>

int main() {
    int notas[5] = {7, 4, 6, 3, 5};   // 5 enteros, inicializados

    std::cout << "Primera nota: " << notas[0] << "\n";   // índice 0
    std::cout << "Última nota:  " << notas[4] << "\n";   // índice 4

    notas[2] = 10;                                        // modificar un elemento
    std::cout << "Tercera nota modificada: " << notas[2] << "\n";
    return 0;
}
bashbash
g++ -std=c++17 -Wall -o arrays arrays.cpp
./arrays
texttext
Primera nota: 7
Última nota:  5
Tercera nota modificada: 10

Dos reglas que no se negocian:

  1. Los índices empiezan en 0. Un array de 5 elementos tiene índices válidos 0, 1, 2, 3, 4. No existe notas[5].
  2. El tamaño es fijo y se decide al compilar. No puedes agrandar un array después. int notas[n]; con n leído por consola no es C++ estándar (aunque GCC lo acepte como extensión).
Advertencia

C++ no comprueba los límites de un array. Escribir notas[5] o notas[100] compila sin queja y accede a memoria que no te pertenece: puedes leer basura, corromper otra variable, o provocar un crash — y las tres cosas pueden pasar en ejecuciones distintas del mismo programa. Esto se llama buffer overflow (desbordamiento de búfer) y es el origen de una parte enorme de las vulnerabilidades de seguridad históricas escritas en C y C++. El compilador no te salvará: la responsabilidad de no salirte del rango es 100% tuya.

Recorrer un array

Con el for clásico, usando el índice:

Ejemplo: sumar y promediar un array con for clásicocpp
#include <iostream>

int main() {
    const int TOTAL = 5;
    int notas[TOTAL] = {7, 4, 6, 3, 5};

    int suma = 0;
    for (int i = 0; i < TOTAL; ++i) {    // ojo: i < TOTAL, no i <= TOTAL
        suma += notas[i];
    }

    std::cout << "Suma: " << suma << "\n";
    std::cout << "Promedio: " << static_cast<double>(suma) / TOTAL << "\n";
    return 0;
}
texttext
Suma: 25
Promedio: 5

Dos detalles heredados de páginas anteriores: la condición es i < TOTAL (con <= te pasarías un elemento, el error clásico llamado off-by-one), y el static_cast<double> está ahí para evitar la división entera que convertiría 25 / 5 en un entero.

El for basado en rango

Desde C++11 puedes recorrer el array sin índices, que es la forma preferida cuando no necesitas saber en qué posición estás:

Ejemplo: recorrer con for basado en rangocpp
#include <iostream>

int main() {
    int notas[5] = {7, 4, 6, 3, 5};

    for (int nota : notas) {          // "para cada nota dentro de notas"
        std::cout << nota << " ";
    }
    std::cout << "\n";
    return 0;
}
texttext
7 4 6 3 5 

Al no haber índice, es imposible salirse del rango. Por eso se prefiere siempre que sea posible.

Nota

Los arrays de estilo C que ves en esta página no son lo que usarías en C++ moderno para una lista de datos. Lo idiomático hoy es std::vector<int>, de la Standard Library: crece dinámicamente, conoce su propio tamaño (.size()), y libera su memoria sola. Se explican aquí porque siguen apareciendo en todas partes (código C, APIs de sistema, librerías como Dear ImGui) y porque entender su relación con los punteros es imprescindible para entender C++ por dentro. Toma esta página como “cómo funciona la maquinaria”, no como “cómo escribir código nuevo”.

Arrays y funciones: la primera sorpresa

Aquí es donde el modelo mental de la página anterior se rompe. En Funciones quedó establecido que los argumentos se pasan por valor, es decir, se copian. Con los arrays, no:

Ejemplo: un array pasado a una función NO se copiacpp
#include <iostream>

void ponerEnCero(int datos[], int cantidad) {
    for (int i = 0; i < cantidad; ++i) {
        datos[i] = 0;             // modifica el array ORIGINAL
    }
}

int main() {
    int notas[5] = {7, 4, 6, 3, 5};
    ponerEnCero(notas, 5);

    std::cout << notas[0] << " " << notas[4] << "\n";   // imprime 0 0
    return 0;
}
texttext
0 0

No hicimos nada especial —no hay ningún &— y aun así la función modificó el original. La razón es que un array, al pasarlo a una función, se degrada (decay) a un puntero a su primer elemento. La función no recibe 5 enteros: recibe una dirección de memoria. Y con esa dirección puede tocar los datos reales.

Importante

De ahí viene el segundo parámetro int cantidad. Como la función solo recibe una dirección, pierde por completo la información del tamaño: dentro de ponerEnCero, no existe forma de saber cuántos elementos hay. Por eso, en C y en C++ de estilo C, el tamaño siempre viaja como un parámetro aparte. Si te equivocas al pasarlo, la función escribirá fuera del array sin enterarse. Esta es la razón práctica más fuerte para preferir std::vector, que lleva su tamaño consigo.

Dentro de main(), en cambio, el array sí conoce su tamaño, y puedes obtenerlo con sizeof:

Ejemplo: calcular el número de elementos con sizeofcpp
int notas[5] = {7, 4, 6, 3, 5};
int cantidad = sizeof(notas) / sizeof(notas[0]);   // 20 bytes / 4 bytes = 5
Advertencia

Ese truco de sizeof funciona solo donde el array fue declarado. Si lo usas dentro de una función que recibió el array como parámetro, sizeof(datos) te dará el tamaño de un puntero (típicamente 8 bytes), no el del array — y el cálculo dará un resultado sin sentido, normalmente 2. Es un error muy frecuente y silencioso.

Cadenas de estilo C

Una cadena de estilo C (C-string) es simplemente un array de char que termina con un carácter especial: el carácter nulo '\0'. Ese terminador es la única marca de dónde acaba el texto.

Ejemplo: cadena de estilo C vs std::stringcpp
#include <iostream>
#include <cstring>   // funciones de C-strings: strlen, strcpy...
#include <string>    // std::string

int main() {
    const char saludo[] = "Hola";     // ocupa 5 chars: 'H','o','l','a','\0'
    std::string moderno = "Hola";     // la versión de la Standard Library

    std::cout << "C-string: " << saludo  << " (largo " << std::strlen(saludo) << ")\n";
    std::cout << "std::string: " << moderno << " (largo " << moderno.size() << ")\n";
    return 0;
}
bashbash
g++ -std=c++17 -Wall -o cadenas cadenas.cpp
./cadenas
texttext
C-string: Hola (largo 4)
std::string: Hola (largo 4)

Aunque "Hola" tiene 4 letras, el array saludo ocupa 5 bytes: el '\0' invisible del final cuenta. strlen devuelve 4 porque cuenta caracteres hasta el terminador, sin incluirlo.

Advertencia

Si el '\0' falta o se sobrescribe, funciones como std::strlen o std::cout << saludo siguen leyendo memoria hacia adelante hasta encontrar por casualidad un byte cero. El resultado puede ser texto basura o un crash. Este es el motivo por el que std::string (que guarda su longitud explícitamente y gestiona su propia memoria) es la opción correcta en C++ salvo que estés obligado a interoperar con una API de C.

Tip

Cuando una librería en C te pida un const char* y tú tengas un std::string, no reescribas nada: miString.c_str() te devuelve la versión C-string equivalente. Es exactamente lo que harás al pasar textos a funciones de Dear ImGui.

Siguiente paso

Este capítulo dejó dos cabos sueltos deliberados: qué es exactamente eso a lo que “se degrada” un array, y por qué una función puede modificar datos ajenos sin usar &. La respuesta a ambos es la misma, y es el tema de la próxima página: Punteros.